diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a9f2545 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +.history +__pycache__/ +app.log diff --git a/ffmpeg_utils.py b/ffmpeg_utils.py new file mode 100644 index 0000000..2caeec7 --- /dev/null +++ b/ffmpeg_utils.py @@ -0,0 +1,117 @@ +import os +import uuid +import subprocess +import requests +import tempfile +import shutil +import logging +import threading +import time +from contextlib import contextmanager + +UPLOAD_FOLDER = 'temp_files' + +@contextmanager +def temp_directory(dir=None): + temp_dir = tempfile.mkdtemp(dir=dir) + try: + yield temp_dir + finally: + # 不再在这里自动删除临时目录 + pass + +def download_file(url, temp_dir): + try: + headers = { + 'User-Agent': 'Mozilla/5.0', + 'Accept': '*/*' + } + + logging.info(f"开始从URL下载文件: {url}") + response = requests.get(url, headers=headers, stream=True, timeout=30) + response.raise_for_status() + + file_path = os.path.join(temp_dir, 'input_audio') + + with open(file_path, 'wb') as f: + for chunk in response.iter_content(chunk_size=8192): + if chunk: + f.write(chunk) + + file_size = os.path.getsize(file_path) + if file_size < 1024: + error_msg = f"下载的文件太小(仅{file_size}字节),可能无效" + logging.error(error_msg) + raise Exception(error_msg) + + logging.info(f"文件下载成功,保存到: {file_path} (大小: {file_size}字节)") + return file_path + except Exception as e: + logging.error(f"从 {url} 下载文件时出错: {e}") + raise + +def execute_ffmpeg(input_path, output_path, ffmpeg_args): + try: + filtered_args = [arg for arg in ffmpeg_args if not (arg.lower() in ['-i', '-input'] or + (ffmpeg_args.index(arg) > 0 and ffmpeg_args[ffmpeg_args.index(arg) - 1].lower() == '-i'))] + + cmd = ['lib/ffmpeg/bin/ffmpeg', '-i', input_path] + filtered_args + [output_path] + logging.info(f"执行FFmpeg命令: {' '.join(cmd)}") + + result = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + encoding='utf-8', + errors='replace' + ) + + if result.returncode != 0: + error_msg = f"FFmpeg处理失败,返回码: {result.returncode}, 错误: {result.stderr}" + logging.error(error_msg) + raise Exception(error_msg) + + logging.info("FFmpeg处理成功完成") + return True + except Exception as e: + logging.error(f"执行FFmpeg时出错: {e}") + raise + + +def process_ffmpeg(data, file_registry, file_lock): + try: + temp_dir = tempfile.mkdtemp(dir=UPLOAD_FOLDER) + logging.info(f"创建临时目录: {temp_dir}") + + input_url = data.get('input_url') + ffmpeg_args = data.get('args', []) + input_path = download_file(input_url, temp_dir) + + output_id = str(uuid.uuid4())[:8] + output_format = data.get('output_format', 'mp4') + output_filename = f"{output_id}.{output_format}" + output_path = os.path.join(temp_dir, output_filename) + + execute_ffmpeg(input_path, output_path, ffmpeg_args) + + with file_lock: + file_registry[output_id] = { + 'path': os.path.abspath(output_path), + 'filename': output_filename, + 'last_access': time.time(), + 'download_count': 0 + } + logging.info(f"已注册新文件ID: {output_id}, 路径: {output_path}") + + # 返回临时目录路径以便在主函数中删除 + return { + 'status': 'success', + 'download_url': f"http://ffmpeg.liulikeji.cn/download/{output_id}/{output_filename}", + 'file_id': output_id, + 'temp_dir': temp_dir # 返回临时目录路径 + } + + except Exception as e: + logging.error(f"处理过程中出错: {e}") + return {'error': str(e)} diff --git a/file_cleanup.py b/file_cleanup.py new file mode 100644 index 0000000..9ebead5 --- /dev/null +++ b/file_cleanup.py @@ -0,0 +1,52 @@ +import os +import shutil +import threading +import time +import logging + +UPLOAD_FOLDER = 'temp_files' +CLEANUP_INTERVAL = 3600 # 清理临时文件的间隔(秒) +FILE_EXPIRY = 7200 # 文件过期时间(秒) + +file_registry = {} +file_lock = threading.Lock() + +def cleanup_temp_files(): + current_time = time.time() + expired_folders = [] + + for folder_name in os.listdir(UPLOAD_FOLDER): + folder_path = os.path.join(UPLOAD_FOLDER, folder_name) + + if not os.path.isdir(folder_path): + continue + + try: + folder_mtime = os.path.getmtime(folder_path) + + if current_time - folder_mtime > FILE_EXPIRY: + expired_folders.append(folder_path) + logging.info(f"检测到过期文件夹: {folder_path} (修改时间: {time.ctime(folder_mtime)})") + except Exception as e: + logging.error(f"获取文件夹 {folder_path} 信息失败: {e}") + + for folder_path in expired_folders: + try: + shutil.rmtree(folder_path) + logging.info(f"已删除过期文件夹: {folder_path}") + except Exception as e: + logging.error(f"删除文件夹 {folder_path} 失败: {e}") + + logging.info(f"清理完成,共删除 {len(expired_folders)} 个过期文件夹") + +def start_cleanup_thread(): + def cleanup_loop(): + while True: + logging.info("开始执行定期清理任务...") + cleanup_temp_files() + time.sleep(CLEANUP_INTERVAL) + + thread = threading.Thread(target=cleanup_loop) + thread.daemon = True + thread.start() + logging.info("已启动后台清理线程") \ No newline at end of file diff --git a/lib/ffmpeg/bin/ffmpeg b/lib/ffmpeg/bin/ffmpeg new file mode 100644 index 0000000..6e8bf62 Binary files /dev/null and b/lib/ffmpeg/bin/ffmpeg differ diff --git a/lib/ffmpeg/bin/ffmpeg.exe b/lib/ffmpeg/bin/ffmpeg.exe new file mode 100644 index 0000000..bb4632a Binary files /dev/null and b/lib/ffmpeg/bin/ffmpeg.exe differ diff --git a/lib/ffmpeg/bin/ffplay b/lib/ffmpeg/bin/ffplay new file mode 100644 index 0000000..ea9a697 Binary files /dev/null and b/lib/ffmpeg/bin/ffplay differ diff --git a/lib/ffmpeg/bin/ffplay.exe b/lib/ffmpeg/bin/ffplay.exe new file mode 100644 index 0000000..2cf53f0 Binary files /dev/null and b/lib/ffmpeg/bin/ffplay.exe differ diff --git a/lib/ffmpeg/bin/ffprobe b/lib/ffmpeg/bin/ffprobe new file mode 100644 index 0000000..fb66b7a Binary files /dev/null and b/lib/ffmpeg/bin/ffprobe differ diff --git a/lib/ffmpeg/bin/ffprobe.exe b/lib/ffmpeg/bin/ffprobe.exe new file mode 100644 index 0000000..991261e Binary files /dev/null and b/lib/ffmpeg/bin/ffprobe.exe differ diff --git a/logging_config.py b/logging_config.py new file mode 100644 index 0000000..f950b56 --- /dev/null +++ b/logging_config.py @@ -0,0 +1,26 @@ +import logging +from logging.handlers import RotatingFileHandler + +def setup_logging(): + log_format = '%(asctime)s - %(levelname)s - %(message)s' + + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.INFO) + console_handler.setFormatter(logging.Formatter(log_format)) + + file_handler = RotatingFileHandler( + 'app.log', + maxBytes=10 * 1024 * 1024, + backupCount=5, + encoding='utf-8' + ) + file_handler.setLevel(logging.INFO) + file_handler.setFormatter(logging.Formatter(log_format)) + + root_logger = logging.getLogger() + root_logger.setLevel(logging.INFO) + root_logger.addHandler(console_handler) + root_logger.addHandler(file_handler) + + werkzeug_logger = logging.getLogger('werkzeug') + werkzeug_logger.setLevel(logging.WARNING) diff --git a/main.py b/main.py index e69de29..b243d0e 100644 --- a/main.py +++ b/main.py @@ -0,0 +1,104 @@ +import logging +import json +from flask import Flask, request, jsonify +import threading +import queue +import time +from ffmpeg_utils import process_ffmpeg +from file_cleanup import start_cleanup_thread + +app = Flask(__name__) + +# 假设这些是主文件中已定义的变量 +file_registry = {} +file_lock = threading.Lock() + +#输入参数列表 +enter_parameter_table = { + "ffmpeg": { + "input_url": str, + "output_format": str, + "args": list + } +} + +# 检测输入参数是否符合要求 +def validate_request(data,api_name): + for key in enter_parameter_table[api_name]: + if key not in data: + logging.warning(f"请求中没有提供{key}参数") + return jsonify({'error': f"未提供{key}参数"}), 400 + if isinstance(data[key], enter_parameter_table[api_name][key]) == False: + logging.warning(f"请求中{key}参数类型错误,应为{enter_parameter_table[api_name][key]}") + return jsonify({'error': f"{key}参数类型错误,您输入为{type(data[key])},应为{enter_parameter_table[api_name][key]}" }), 400 + return None, None + + + + +@app.route('/api/ffmpeg', methods=['POST']) +def ffmpeg_api(): + logging.info("收到FFmpeg API请求") + data = request.get_json() + + # 检测参数类型 + error_response, status_code = validate_request(data,"ffmpeg") + if error_response: return error_response, status_code + + # 创建处理进程 + result_queue = queue.Queue() + def run_process(data, file_registry, file_lock, result_queue): + with app.app_context(): # 设置应用上下文 + result = process_ffmpeg(data, file_registry, file_lock) + result_queue.put(result) + + thread = threading.Thread(target=run_process, args=(data, file_registry, file_lock, result_queue)) + thread.start() + + # 等待处理结果 + result = result_queue.get() + if 'error' in result: + logging.error(f"处理过程中出错: {result['error']}") + return jsonify({'status': 'error','error': result['error']}), 500 + else: + logging.info(f"处理成功,返回下载URL: {result['download_url']}") + return jsonify({ + 'status': 'success', + 'download_url': result['download_url'], + 'file_id': result['file_id'] + }), 200 + + +@app.route('/api/sanjuuni', methods=['POST']) +def sanjuuni_api(): + + return jsonify({'status': 'error','error': '暂未开放'}), 403 + + + + + +@app.route('/download//', methods=['GET']) +def download_file_endpoint(file_id, filename): + logging.info(f"收到文件下载请求 - 文件ID: {file_id}, 文件名: {filename}") + with file_lock: + if file_id not in file_registry: + logging.warning(f"文件ID: {file_id} 不存在") + return jsonify({'error': '文件不存在'}), 404 + file_info = file_registry[file_id] + file_info['last_access'] = time.time() + file_info['download_count'] += 1 + + try: + with open(file_info['path'], 'rb') as f: + file_data = f.read() + return file_data, 200, {'Content-Disposition': f'attachment; filename={filename}'} + except Exception as e: + logging.error(f"下载文件时出错: {e}") + return jsonify({'status': 'error','error': str(e)}), 500 + + +if __name__ == '__main__': + logging.basicConfig(level=logging.INFO) + start_cleanup_thread() + app.run(debug=True)