added cancel download feature

This commit is contained in:
cool.gitter.choco
2025-02-01 10:04:47 -06:00
parent c5a044fbd6
commit ee86a06c76
5 changed files with 245 additions and 9 deletions

View File

@@ -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'
)

View File

@@ -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'
)

View File

@@ -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'
)

View File

@@ -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);
}

View File

@@ -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 cant 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;
}