from flask import Blueprint, Response, request import json import os import random import string import sys import traceback 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)) + '.track.prg' class FlushingFileWrapper: def __init__(self, file): self.file = file def write(self, text): # Write only lines that start with a JSON object for line in text.split('\n'): if line.startswith('{'): self.file.write(line + '\n') self.file.flush() def flush(self): self.file.flush() def download_task(service, url, main, fallback, quality, fall_quality, real_time, prg_path): try: from routes.utils.track import download_track with open(prg_path, 'w') as f: flushing_file = FlushingFileWrapper(f) original_stdout = sys.stdout sys.stdout = flushing_file # Redirect stdout for this process try: download_track( service=service, url=url, main=main, fallback=fallback, quality=quality, fall_quality=fall_quality, real_time=real_time ) flushing_file.write(json.dumps({"status": "complete"}) + "\n") except Exception as e: error_data = json.dumps({ "status": "error", "message": str(e), "traceback": traceback.format_exc() }) flushing_file.write(error_data + "\n") finally: sys.stdout = original_stdout # Restore original stdout except Exception as e: with open(prg_path, 'w') as f: error_data = json.dumps({ "status": "error", "message": str(e), "traceback": traceback.format_exc() }) f.write(error_data + "\n") @track_bp.route('/download', methods=['GET']) def handle_download(): service = request.args.get('service') url = request.args.get('url') main = request.args.get('main') fallback = request.args.get('fallback') quality = request.args.get('quality') fall_quality = request.args.get('fall_quality') # Retrieve and normalize the real_time parameter; defaults to False. real_time_arg = request.args.get('real_time', 'false') real_time = real_time_arg.lower() in ['true', '1', 'yes'] # Sanitize main and fallback to prevent directory traversal if main: main = os.path.basename(main) if fallback: fallback = os.path.basename(fallback) if not all([service, url, main]): return Response( json.dumps({"error": "Missing parameters"}), status=400, mimetype='application/json' ) # Validate credentials based on service and fallback try: if service == 'spotify': if fallback: # Validate Deezer main credentials and Spotify fallback credentials deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json')) if not os.path.isfile(deezer_creds_path): return Response( json.dumps({"error": "Invalid Deezer credentials directory"}), status=400, mimetype='application/json' ) spotify_fallback_path = os.path.abspath(os.path.join('./creds/spotify', fallback, 'credentials.json')) if not os.path.isfile(spotify_fallback_path): return Response( json.dumps({"error": "Invalid Spotify fallback credentials directory"}), status=400, mimetype='application/json' ) else: # Validate Spotify main credentials spotify_creds_path = os.path.abspath(os.path.join('./creds/spotify', main, 'credentials.json')) if not os.path.isfile(spotify_creds_path): return Response( json.dumps({"error": "Invalid Spotify credentials directory"}), status=400, mimetype='application/json' ) elif service == 'deezer': # Validate Deezer main credentials deezer_creds_path = os.path.abspath(os.path.join('./creds/deezer', main, 'credentials.json')) if not os.path.isfile(deezer_creds_path): return Response( json.dumps({"error": "Invalid Deezer credentials directory"}), status=400, mimetype='application/json' ) else: return Response( json.dumps({"error": "Unsupported service"}), status=400, mimetype='application/json' ) except Exception as e: return Response( json.dumps({"error": f"Credential validation failed: {str(e)}"}), status=500, mimetype='application/json' ) filename = generate_random_filename() prg_dir = './prgs' os.makedirs(prg_dir, exist_ok=True) prg_path = os.path.join(prg_dir, filename) process = Process( target=download_task, args=(service, url, main, fallback, quality, fall_quality, real_time, prg_path) ) 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' )