added cancel download feature
This commit is contained in:
@@ -9,6 +9,9 @@ from multiprocessing import Process
|
||||
|
||||
album_bp = Blueprint('album', __name__)
|
||||
|
||||
# Global dictionary to keep track of running download processes.
|
||||
download_processes = {}
|
||||
|
||||
def generate_random_filename(length=6):
|
||||
chars = string.ascii_lowercase + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
||||
@@ -34,7 +37,7 @@ def download_task(service, url, main, fallback, quality, fall_quality, real_time
|
||||
flushing_file = FlushingFileWrapper(f)
|
||||
original_stdout = sys.stdout
|
||||
sys.stdout = flushing_file # Redirect stdout
|
||||
|
||||
|
||||
try:
|
||||
download_album(
|
||||
service=service,
|
||||
@@ -145,13 +148,64 @@ def handle_download():
|
||||
os.makedirs(prg_dir, exist_ok=True)
|
||||
prg_path = os.path.join(prg_dir, filename)
|
||||
|
||||
Process(
|
||||
# Create and start the download process, and track it in the global dictionary.
|
||||
process = Process(
|
||||
target=download_task,
|
||||
args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path)
|
||||
).start()
|
||||
|
||||
)
|
||||
process.start()
|
||||
download_processes[filename] = process
|
||||
|
||||
return Response(
|
||||
json.dumps({"prg_file": filename}),
|
||||
status=202,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@album_bp.route('/download/cancel', methods=['GET'])
|
||||
def cancel_download():
|
||||
"""
|
||||
Cancel a running download process by its process id (prg file name).
|
||||
"""
|
||||
prg_file = request.args.get('prg_file')
|
||||
if not prg_file:
|
||||
return Response(
|
||||
json.dumps({"error": "Missing process id (prg_file) parameter"}),
|
||||
status=400,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
process = download_processes.get(prg_file)
|
||||
prg_dir = './prgs'
|
||||
prg_path = os.path.join(prg_dir, prg_file)
|
||||
|
||||
if process and process.is_alive():
|
||||
# Terminate the running process
|
||||
process.terminate()
|
||||
process.join() # Wait for process termination
|
||||
# Remove it from our global tracking dictionary
|
||||
del download_processes[prg_file]
|
||||
|
||||
# Append a cancellation status to the log file
|
||||
try:
|
||||
with open(prg_path, 'a') as f:
|
||||
f.write(json.dumps({"status": "cancel"}) + "\n")
|
||||
except Exception as e:
|
||||
# If writing fails, we log the error in the response.
|
||||
return Response(
|
||||
json.dumps({"error": f"Failed to write cancel status to file: {str(e)}"}),
|
||||
status=500,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
return Response(
|
||||
json.dumps({"status": "cancel"}),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
json.dumps({"error": "Process not found or already terminated"}),
|
||||
status=404,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@@ -9,6 +9,9 @@ from multiprocessing import Process
|
||||
|
||||
playlist_bp = Blueprint('playlist', __name__)
|
||||
|
||||
# Global dictionary to track running playlist download processes
|
||||
playlist_processes = {}
|
||||
|
||||
def generate_random_filename(length=6):
|
||||
chars = string.ascii_lowercase + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
||||
@@ -89,13 +92,63 @@ def handle_download():
|
||||
os.makedirs(prg_dir, exist_ok=True)
|
||||
prg_path = os.path.join(prg_dir, filename)
|
||||
|
||||
Process(
|
||||
process = Process(
|
||||
target=download_task,
|
||||
args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path)
|
||||
).start()
|
||||
)
|
||||
process.start()
|
||||
# Track the running process using the generated filename.
|
||||
playlist_processes[filename] = process
|
||||
|
||||
return Response(
|
||||
json.dumps({"prg_file": filename}),
|
||||
status=202,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@playlist_bp.route('/download/cancel', methods=['GET'])
|
||||
def cancel_download():
|
||||
"""
|
||||
Cancel a running playlist download process by its process id (prg file name).
|
||||
"""
|
||||
prg_file = request.args.get('prg_file')
|
||||
if not prg_file:
|
||||
return Response(
|
||||
json.dumps({"error": "Missing process id (prg_file) parameter"}),
|
||||
status=400,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
process = playlist_processes.get(prg_file)
|
||||
prg_dir = './prgs'
|
||||
prg_path = os.path.join(prg_dir, prg_file)
|
||||
|
||||
if process and process.is_alive():
|
||||
# Terminate the running process and wait for it to finish
|
||||
process.terminate()
|
||||
process.join()
|
||||
# Remove it from our tracking dictionary
|
||||
del playlist_processes[prg_file]
|
||||
|
||||
# Append a cancellation status to the log file
|
||||
try:
|
||||
with open(prg_path, 'a') as f:
|
||||
f.write(json.dumps({"status": "cancel"}) + "\n")
|
||||
except Exception as e:
|
||||
return Response(
|
||||
json.dumps({"error": f"Failed to write cancel status to file: {str(e)}"}),
|
||||
status=500,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
return Response(
|
||||
json.dumps({"status": "cancel"}),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
json.dumps({"error": "Process not found or already terminated"}),
|
||||
status=404,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@@ -9,6 +9,9 @@ from multiprocessing import Process
|
||||
|
||||
track_bp = Blueprint('track', __name__)
|
||||
|
||||
# Global dictionary to track running track download processes.
|
||||
track_processes = {}
|
||||
|
||||
def generate_random_filename(length=6):
|
||||
chars = string.ascii_lowercase + string.digits
|
||||
return ''.join(random.choice(chars) for _ in range(length)) + '.prg'
|
||||
@@ -145,13 +148,63 @@ def handle_download():
|
||||
os.makedirs(prg_dir, exist_ok=True)
|
||||
prg_path = os.path.join(prg_dir, filename)
|
||||
|
||||
Process(
|
||||
process = Process(
|
||||
target=download_task,
|
||||
args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path)
|
||||
).start()
|
||||
)
|
||||
process.start()
|
||||
# Track the running process using the generated filename.
|
||||
track_processes[filename] = process
|
||||
|
||||
return Response(
|
||||
json.dumps({"prg_file": filename}),
|
||||
status=202,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@track_bp.route('/download/cancel', methods=['GET'])
|
||||
def cancel_download():
|
||||
"""
|
||||
Cancel a running track download process by its process id (prg file name).
|
||||
"""
|
||||
prg_file = request.args.get('prg_file')
|
||||
if not prg_file:
|
||||
return Response(
|
||||
json.dumps({"error": "Missing process id (prg_file) parameter"}),
|
||||
status=400,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
process = track_processes.get(prg_file)
|
||||
prg_dir = './prgs'
|
||||
prg_path = os.path.join(prg_dir, prg_file)
|
||||
|
||||
if process and process.is_alive():
|
||||
# Terminate the running process and wait for it to finish
|
||||
process.terminate()
|
||||
process.join()
|
||||
# Remove it from our tracking dictionary
|
||||
del track_processes[prg_file]
|
||||
|
||||
# Append a cancellation status to the log file
|
||||
try:
|
||||
with open(prg_path, 'a') as f:
|
||||
f.write(json.dumps({"status": "cancel"}) + "\n")
|
||||
except Exception as e:
|
||||
return Response(
|
||||
json.dumps({"error": f"Failed to write cancel status to file: {str(e)}"}),
|
||||
status=500,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
return Response(
|
||||
json.dumps({"status": "cancel"}),
|
||||
status=200,
|
||||
mimetype='application/json'
|
||||
)
|
||||
else:
|
||||
return Response(
|
||||
json.dumps({"error": "Process not found or already terminated"}),
|
||||
status=404,
|
||||
mimetype='application/json'
|
||||
)
|
||||
|
||||
@@ -970,4 +970,28 @@ html {
|
||||
padding: 12px;
|
||||
font-size: 15px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
background: none;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
padding: 5px;
|
||||
outline: none;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.cancel-btn img {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
filter: invert(1);
|
||||
transition: transform 0.3s ease;
|
||||
}
|
||||
|
||||
.cancel-btn:hover img {
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.cancel-btn:active img {
|
||||
transform: scale(0.9);
|
||||
}
|
||||
|
||||
@@ -477,11 +477,52 @@ function createQueueItem(item, type, prgFile, queueId) {
|
||||
<div class="title">${item.name}</div>
|
||||
<div class="type">${type.charAt(0).toUpperCase() + type.slice(1)}</div>
|
||||
<div class="log" id="log-${queueId}-${prgFile}">Initializing download...</div>
|
||||
<button class="cancel-btn" data-prg="${prgFile}" data-type="${type}" data-queueid="${queueId}" title="Cancel Download">
|
||||
<img src="https://www.svgrepo.com/show/488384/skull-head.svg" alt="Cancel Download">
|
||||
</button>
|
||||
`;
|
||||
|
||||
// Attach cancel event listener
|
||||
const cancelBtn = div.querySelector('.cancel-btn');
|
||||
cancelBtn.addEventListener('click', async (e) => {
|
||||
e.stopPropagation();
|
||||
// Hide the cancel button immediately so the user can’t click it again.
|
||||
cancelBtn.style.display = 'none';
|
||||
|
||||
const prg = e.target.closest('button').dataset.prg;
|
||||
const type = e.target.closest('button').dataset.type;
|
||||
const queueId = e.target.closest('button').dataset.queueid;
|
||||
// Determine the correct cancel endpoint based on the type.
|
||||
// For example: `/api/album/download/cancel`, `/api/playlist/download/cancel`, `/api/track/download/cancel`
|
||||
const cancelEndpoint = `/api/${type}/download/cancel?prg_file=${encodeURIComponent(prg)}`;
|
||||
try {
|
||||
const response = await fetch(cancelEndpoint);
|
||||
const data = await response.json();
|
||||
if (data.status === "cancel") {
|
||||
const logElement = document.getElementById(`log-${queueId}-${prg}`);
|
||||
logElement.textContent = "Download cancelled";
|
||||
// Mark the entry as ended and clear its monitoring interval.
|
||||
const entry = downloadQueue[queueId];
|
||||
if (entry) {
|
||||
entry.hasEnded = true;
|
||||
clearInterval(entry.intervalId);
|
||||
}
|
||||
// Remove the queue item after 5 seconds, same as when a download finishes.
|
||||
setTimeout(() => cleanupEntry(queueId), 5000);
|
||||
} else {
|
||||
alert("Cancel error: " + (data.error || "Unknown error"));
|
||||
}
|
||||
} catch (error) {
|
||||
alert("Cancel error: " + error.message);
|
||||
}
|
||||
});
|
||||
|
||||
return div;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
async function loadCredentials(service) {
|
||||
try {
|
||||
const response = await fetch(`/api/credentials/${service}`);
|
||||
@@ -677,6 +718,17 @@ function getStatusMessage(data) {
|
||||
return 'Download completed successfully';
|
||||
case 'skipped':
|
||||
return `Track ${data.song} skipped, it already exists!`;
|
||||
case 'real_time': {
|
||||
// Convert milliseconds to minutes and seconds.
|
||||
const totalMs = data.time_elapsed;
|
||||
const minutes = Math.floor(totalMs / 60000);
|
||||
const seconds = Math.floor((totalMs % 60000) / 1000);
|
||||
// Optionally pad seconds with a leading zero if needed:
|
||||
const paddedSeconds = seconds < 10 ? '0' + seconds : seconds;
|
||||
|
||||
return `Real-time downloading track ${data.song} by ${data.artist} (${(data.percentage * 100).toFixed(1)}%). Time elapsed: ${minutes}:${paddedSeconds}`;
|
||||
}
|
||||
|
||||
default:
|
||||
return data.status;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user