# -*- coding: utf-8 -*- """ down-video 视频下载蓝图 """ import os import json import queue from flask import Blueprint, render_template, request, jsonify, send_file, Response from config import DEFAULT_OUTPUT_DIR, PROXY_URL from utils.down_video_utils import ( check_ffmpeg, check_deno, is_twitter_url, is_bilibili_url, is_instagram_url, is_youtube_url, download_video, ) from utils.stats_db import add_recent_path, get_recent_paths, delete_recent_path, clear_recent_paths bp = Blueprint('dv_cookies', __name__, url_prefix='/down-video') # 用于存储下载进度的队列 progress_queues = {} @bp.route('/') def page(): ffmpeg_ok = check_ffmpeg() deno_ok = check_deno() return render_template('down_video.html', ffmpeg_ok=ffmpeg_ok, deno_ok=deno_ok) @bp.route('/file') def serve_file(): filepath = request.args.get("path", "") as_download = request.args.get("download", "0") == "1" if not filepath: return "Missing path", 400 filepath = os.path.abspath(filepath) if not os.path.exists(filepath) or not os.path.isfile(filepath): return "File not found", 404 ext = os.path.splitext(filepath)[1].lower() if ext not in ('.mp4', '.m4a', '.webm', '.mkv', '.mov', '.avi'): return "File type not allowed", 403 mime_types = { '.mp4': 'video/mp4', '.m4a': 'audio/mp4', '.webm': 'video/webm', '.mkv': 'video/x-matroska', '.mov': 'video/quicktime', '.avi': 'video/x-msvideo', } mimetype = mime_types.get(ext, 'application/octet-stream') response = send_file( filepath, mimetype=mimetype, conditional=True, ) response.headers['Accept-Ranges'] = 'bytes' if not as_download: response.headers['Content-Disposition'] = 'inline' return response @bp.route('/download', methods=["POST"]) def download(): data = request.get_json() url = data.get("url", "").strip() if data else "" output_dir = data.get("output_dir", DEFAULT_OUTPUT_DIR).strip() if data else DEFAULT_OUTPUT_DIR download_id = data.get("download_id", "") if data else "" use_proxy = data.get("use_proxy", False) if data else False proxy_url_input = (data.get("proxy_url") or "").strip() if data else "" if not url: return jsonify({"success": False, "message": "请输入视频链接"}), 400 if is_twitter_url(url): platform = 'twitter' elif is_bilibili_url(url): platform = 'bilibili' elif is_instagram_url(url): platform = 'instagram' elif is_youtube_url(url): platform = 'youtube' else: return jsonify({"success": False, "message": "仅支持 Twitter/X、Bilibili 和 Instagram 视频链接"}), 400 if not output_dir: output_dir = DEFAULT_OUTPUT_DIR # 确定使用的代理地址 proxy_url = proxy_url_input if use_proxy and proxy_url_input else (PROXY_URL if use_proxy else None) # 创建进度队列 progress_queue = queue.Queue() if download_id: progress_queues[download_id] = progress_queue def progress_callback(data): progress_queue.put(data) try: # 发送开始合并的消息 def send_merge_status(): progress_queue.put({ 'type': 'merging', 'message': '正在合并视频和音频...' }) success, message, title, filepath = download_video(url, output_dir, platform, progress_callback, proxy_url=proxy_url) # 如果是bilibili或youtube,可能有合并过程,发送合并状态 if platform in ('bilibili', 'youtube') and filepath and '_merged' in filepath: send_merge_status() return jsonify({"success": success, "message": message, "title": title, "filepath": filepath}) finally: # 清理队列 if download_id and download_id in progress_queues: del progress_queues[download_id] @bp.route('/progress/') def progress_stream(download_id): """SSE端点 - 实时推送下载进度""" def generate(): progress_queue = progress_queues.get(download_id) if not progress_queue: yield f"data: {json.dumps({'type': 'error', 'message': '下载任务不存在'})}\n\n" return while True: try: data = progress_queue.get(timeout=30) yield f"data: {json.dumps(data)}\n\n" if data.get('type') == 'progress' and data.get('percent', 0) >= 100: break except queue.Empty: # 发送心跳保持连接 yield f":\n\n" continue return Response( generate(), mimetype='text/event-stream', headers={ 'Cache-Control': 'no-cache', 'Connection': 'keep-alive', 'X-Accel-Buffering': 'no' } ) # ===== 保存路径历史 ===== @bp.route('/save-paths', methods=['GET']) def get_save_paths(): paths = get_recent_paths(limit=10) return jsonify({'paths': paths}) @bp.route('/save-paths', methods=['POST']) def add_save_path(): data = request.get_json() path = data.get('path', '').strip() if data else '' if not path: return jsonify({'success': False, 'message': '路径不能为空'}), 400 add_recent_path(path) return jsonify({'success': True}) @bp.route('/save-paths/delete', methods=['POST']) def delete_save_path(): data = request.get_json() path = data.get('path', '').strip() if data else '' if not path: return jsonify({'success': False, 'message': '路径不能为空'}), 400 delete_recent_path(path) return jsonify({'success': True})