diff --git a/.dockerignore b/.dockerignore index 7ffe9ef..a64a4c2 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,5 +1,4 @@ .venv/ .idea/ .deploy/ -logs/ ./mp4/* diff --git a/docker-compose.yaml b/docker-compose.yaml index 6201fe1..c045e31 100644 --- a/docker-compose.yaml +++ b/docker-compose.yaml @@ -8,7 +8,7 @@ services: container_name: m3u8_download image: registry.cn-hangzhou.aliyuncs.com/yinzhou_docker_hub/m3u8_download:latest ports: - - "1314:1314" + - "8778:8778" volumes: - ./mp4:/opt/m3u8_download/mp4 diff --git a/dockerfile b/dockerfile index e72fc9e..e30d288 100644 --- a/dockerfile +++ b/dockerfile @@ -17,4 +17,7 @@ RUN pip install --no-cache-dir -r requirements.txt -i https://pypi.tuna.tsinghua COPY . . # 运行应用程序 -ENTRYPOINT ["python3", "m3u8_download.py"] \ No newline at end of file +# ENTRYPOINT ["python3", "m3u8_download.py"] + + +CMD ["sh", "-c", "python3 m3u8_download.py & python3 m3u8_ui.py & tail -f logs/m3u8_download.log"] \ No newline at end of file diff --git a/m3u8_download.py b/m3u8_download.py index 2d278e1..1d87661 100644 --- a/m3u8_download.py +++ b/m3u8_download.py @@ -1,46 +1,9 @@ -import m3u8_to_mp4 -from utils.MySqlUtil import MySqlUtil -from apscheduler.schedulers.blocking import BlockingScheduler +from utils.Download import download_m3u8 import time -from utils.Log import Log -from pathlib import Path - - -def download_m3u8(download_path='./mp4/'): - try: - log = Log().getlog() - # 初始化数据库连接 - movie_config = MySqlUtil("movie") - - # 获取未处理的电影记录 - movie_message = MySqlUtil.get_one(movie_config, 'SELECT * FROM `movie` WHERE is_ok=0 LIMIT 1') - if not movie_message or len(movie_message) < 3: # 校验结果是否有效 - log.info("没有找到电影记录或无效数据。") - return - - id, name, url = movie_message[0], movie_message[1], movie_message[2] - - # 构造目标文件路径 - file_path = Path(download_path).joinpath(f"{name}.mp4") - - # 更新数据库状态,使用参数化查询防止 SQL 注入 - sql = f'UPDATE `movie`.`movie` SET `is_ok` = 1 WHERE `id` = {id}' - MySqlUtil.update(movie_config, sql=sql) - - log.info(f"任务下载中,正在下载 {name}...") - - # 下载 m3u8 文件并转换为 MP4 - m3u8_to_mp4.multithread_download(url, file_path=str(file_path)) - log.info(f"成功下载并转换 {name} to {file_path}.") - - except Exception as e: - log.error(f"下载过程中出现错误: {e}") - - if __name__ == '__main__': - download_m3u8() - # str_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) - # sch = BlockingScheduler(timezone='Asia/Shanghai') - # sch.add_job(download_m3u8, 'cron', minute='*/2') - # sch.start() + # 循环执行download_m3u8() 在执行完后休眠1分钟 + + while True: + download_m3u8() + time.sleep(60) diff --git a/m3u8_ui.py b/m3u8_ui.py new file mode 100644 index 0000000..4394335 --- /dev/null +++ b/m3u8_ui.py @@ -0,0 +1,85 @@ +from flask import Flask, render_template, request, jsonify, send_file, current_app +from flask_cors import CORS # 导入 Flask-CORS 扩展 +from utils.Log import Log +from utils.Db_Execute import get_movie_list,movie_options,movie_add +from werkzeug.exceptions import BadRequest + +app = Flask(__name__) +log = Log() + +# 允许所有源访问接口,可以根据需要进行更细粒度的控制 +CORS(app) + +@app.route('/') +def index(): + return send_file('templates/index.html') + +@app.route('/movie_list', methods=['GET']) +def convert_sql(): + try: + # 调用 get_movie_list() 获取电影列表 + movie_list = get_movie_list() + + # 如果列表为空,返回空数组 + if not movie_list: + return jsonify([]), 200 + + # 正常返回电影列表 + return jsonify(movie_list), 200 + except Exception as e: + # 捕获异常并返回错误信息 + log.error(f"Error in fetching movie list: {str(e)}", exc_info=True) # 添加 exc_info=True 以便记录完整的堆栈跟踪 + return jsonify({"error": "Failed to fetch movie list"}), 500 + + + +@app.route('/movie/', methods=['GET', 'PUT']) +def movie_markers(movie_id): + try: + if request.method == 'GET': + # 获取当前状态逻辑(示例) + current_status = movie_options(movie_id) # 替换为实际数据库查询 + return jsonify({"status": current_status}), 200 + + elif request.method == 'PUT': + # 更新状态逻辑(示例) + movie_options(movie_id) # 替换为实际数据库操作 + return jsonify(success=True), 200 + else: + return jsonify({"message": "无效的请求方法"}), 400 + + except Exception as e: + return jsonify({"message": "操作失败", "error": str(e)}), 500 + +@app.route('/add_movie', methods=['POST']) +def add_movie(): + try: + # 修改点1:获取JSON格式数据 + data = request.get_json() + if not data: + raise BadRequest("请求体必须为JSON格式") + + # 修改点2:使用正确的字段名 + movie_name = data.get('name') + movie_path = data.get('path') + + # 输入验证(字段名同步修改) + if not movie_name or not movie_path: + raise BadRequest("缺少必要的参数: name 或 path") + if len(movie_name) > 255 or len(movie_path) > 255: + raise BadRequest("参数过长: name 或 path 超过255字符") + + # 数据库操作(保持原逻辑) + movie_add(movie_name, movie_path) + + return jsonify(success=True), 200 + + except BadRequest as e: + return jsonify({"message": e.description}), 400 + except Exception as e: + return jsonify({"message": "操作失败", "error": str(e)}), 500 + + +if __name__ == '__main__': + # 指定host和port,这里使用0.0.0.0可以让服务器被外部访问 + app.run(host='0.0.0.0', port=8778, debug=True) diff --git a/requirements.txt b/requirements.txt index 104fc87..144b8c8 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,4 +3,6 @@ m3u8>=0.9.0 pycryptodome>=3.10.1 pandas pymysql -apscheduler \ No newline at end of file +apscheduler +flask +flask_cors \ No newline at end of file diff --git a/sqlite/movies.db b/sqlite/movies.db new file mode 100644 index 0000000..c5410c9 Binary files /dev/null and b/sqlite/movies.db differ diff --git a/templates/index.html b/templates/index.html new file mode 100644 index 0000000..956091d --- /dev/null +++ b/templates/index.html @@ -0,0 +1,125 @@ + + + + + 电影下载管理系统 + + + + +
+

电影下载列表

+ + + + +
ID电影名称m3u8地址下载状态操作
+
+ +
+

添加新电影

+ + +
+ + +
+
+ +
更新中...
+ + + + \ No newline at end of file diff --git a/utils/Db_Execute.py b/utils/Db_Execute.py new file mode 100644 index 0000000..818be57 --- /dev/null +++ b/utils/Db_Execute.py @@ -0,0 +1,56 @@ +# -*- coding:utf-8 -*- + +import os +from utils.SQLiteDB import SQLiteDB +from utils.MySqlUtil import MySqlUtil + + +def get_config(): + mode= os.environ.get('mode1',1) + if mode==1: + return 'sqlite' + else: + return 'mysql' + +def get_movie_list(): + if get_config()=='sqlite': + db = SQLiteDB() + db.connect() + movie_list = db.get_undownloaded() + db.close() + return movie_list + else: + + db = MySqlUtil('movies') + movie_list = db.get_all('select * from movies where is_downloaded=0') + return movie_list + + +def movie_options(movie_id): + if get_config()=='sqlite': + db = SQLiteDB() + db.connect() + result=db.mark_downloaded(movie_id) + db.close() + return result + else: + + db = MySqlUtil('movies') + movie_list = db.get_all('select * from movies where is_downloaded=0') + return movie_list + +def movie_add(movie_name,movie_path): + if get_config()=='sqlite': + db = SQLiteDB() + db.connect() + result=db.insert_movie(movie_name,movie_path) + db.close() + return result + else: + + db = MySqlUtil('movies') + movie_list = db.get_all('select * from movies where is_downloaded=0') + return movie_list + +if __name__ == '__main__': + print(get_movie_list()) \ No newline at end of file diff --git a/utils/Download.py b/utils/Download.py new file mode 100644 index 0000000..97c8a06 --- /dev/null +++ b/utils/Download.py @@ -0,0 +1,52 @@ +import m3u8_to_mp4 +from utils.MySqlUtil import MySqlUtil +from apscheduler.schedulers.blocking import BlockingScheduler +import time +from utils.Log import Log +from pathlib import Path +from utils.Db_Execute import get_movie_list, movie_options, movie_add +import os + +log = Log() + + +def download_m3u8(): + try: + current_directory = os.path.dirname(os.path.abspath(__file__)) + root_path = os.path.abspath(os.path.dirname(current_directory) + os.path.sep + ".") + project_name = root_path.split(os.path.sep)[-1] + project_root_path = os.path.abspath(os.path.dirname(__file__)).split(project_name)[0] + project_name + '/mp4/' + + # 调用 get_movie_list() 获取电影列表 + movie_list = get_movie_list() + for movie in movie_list: + + if not movie or len(movie) < 3: # 校验结果是否有效 + log.info("没有找到电影记录或无效数据。") + return + + id, name, url = movie[0], movie[1], movie[2] + + # 构造目标文件路径 + file_path = Path(project_root_path).joinpath(f"{name}.mp4") + + # 更新数据库状态,使用参数化查询防止 SQL 注入 + movie_options(id) + + log.info(f"任务下载中,正在下载 {name}...") + log.info(file_path) + + # 下载 m3u8 文件并转换为 MP4 + m3u8_to_mp4.multithread_download(url, file_path=file_path) + log.info(f"成功下载并转换 {name} to {file_path}.") + + except Exception as e: + log.error(f"下载过程中出现错误: {e}") + + +if __name__ == '__main__': + download_m3u8() + # str_time = time.strftime('%Y-%m-%d %H:%M:%S', time.localtime()) + # sch = BlockingScheduler(timezone='Asia/Shanghai') + # sch.add_job(download_m3u8, 'cron', minute='*/2') + # sch.start() diff --git a/utils/LoadConfig.py b/utils/LoadConfig.py index 8ed93a6..495cdf4 100644 --- a/utils/LoadConfig.py +++ b/utils/LoadConfig.py @@ -1,5 +1,5 @@ import json, os -from utils.Log import log +from utils.Log import Log current_directory = os.path.dirname(os.path.abspath(__file__)) root_path = os.path.abspath(os.path.dirname(current_directory) + os.path.sep + ".") @@ -9,7 +9,7 @@ project_name = root_path.split(os.path.sep)[-1] project_root_path = os.path.abspath(os.path.dirname(__file__)).split(project_name)[0] + project_name -# log.info(str(project_root_path)) +log = Log() def loadconfig(config_key): diff --git a/utils/Log.py b/utils/Log.py index 4b64d58..b109fab 100644 --- a/utils/Log.py +++ b/utils/Log.py @@ -2,10 +2,6 @@ import logging import os from datetime import datetime -# 定义全局变量 log_path -cur_path = os.path.dirname(os.path.realpath(__file__)) -log_path = os.path.join(os.path.dirname(cur_path), 'logs') - class Log(): def __init__(self, logger_name='my_logger'): @@ -14,24 +10,26 @@ class Log(): self.logger.handlers.clear() self.logger.setLevel(logging.INFO) - if not os.path.exists(log_path): - os.makedirs(log_path) + # 定义固定日志路径 + self.log_dir = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs') + self.log_file = os.path.join(self.log_dir, 'm3u8_download.log') - self.update_log_file() + if not os.path.exists(self.log_dir): + os.makedirs(self.log_dir) - def update_log_file(self): - current_date = datetime.now().strftime("%Y_%m_%d") - self.log_name = os.path.join(log_path, f'{current_date}.log') + self._setup_handlers() - for handler in self.logger.handlers[:]: - self.logger.removeHandler(handler) - - fh = logging.FileHandler(self.log_name, 'a', encoding='utf-8') + def _setup_handlers(self): + """初始化日志处理器""" + # 文件处理器(固定文件名) + fh = logging.FileHandler(self.log_file, 'a', encoding='utf-8') fh.setLevel(logging.INFO) + # 控制台处理器 ch = logging.StreamHandler() ch.setLevel(logging.INFO) + # 统一格式器 formatter = logging.Formatter( '[%(asctime)s] %(filename)s line:%(lineno)d [%(levelname)s]%(message)s', datefmt="%Y-%m-%d %H:%M:%S" @@ -39,28 +37,19 @@ class Log(): fh.setFormatter(formatter) ch.setFormatter(formatter) + # 添加处理器 self.logger.addHandler(fh) self.logger.addHandler(ch) - def getlog(self): - current_date = datetime.now().strftime("%Y_%m_%d") - log_date = os.path.basename(self.log_name).split('.')[0] - if current_date != log_date: - self.update_log_file() - return self.logger + # 移除日期检查相关方法 + def info(self, msg, *args, ** kwargs): + self.logger.info(msg, *args, ** kwargs) - def info(self, msg, *args, **kwargs): - logger = self.getlog() - logger.info(msg, *args, **kwargs) - - def error(self, msg, *args, **kwargs): - logger = self.getlog() - logger.error(msg, *args, **kwargs) - - def warning(self, msg, *args, **kwargs): - logger = self.getlog() - logger.warning(msg, *args, **kwargs) + def error(self, msg, *args, ** kwargs): + self.logger.error(msg, *args, ** kwargs) + def warning(self, msg, *args, ** kwargs): + self.logger.warning(msg, *args, ** kwargs) if __name__ == "__main__": log = Log() diff --git a/utils/MySqlUtil.py b/utils/MySqlUtil.py index 3b36f5f..c376a90 100644 --- a/utils/MySqlUtil.py +++ b/utils/MySqlUtil.py @@ -1,12 +1,12 @@ #!/usr/bin/python # -*- coding:utf-8 -*- import pymysql -from utils.Log import log -import os +from utils.Log import Log import platform import pandas as pd from utils.LoadConfig import loadconfig +log = Log() class MySQLError(Exception): def __init__(self, message): @@ -14,13 +14,14 @@ class MySQLError(Exception): class MySqlUtil: + + """mysql util""" db = None cursor = None def get_section(db_name): """根据系统环境变量获取section""" - platform_ = platform.system() if platform_ == "Windows" or platform_ == "Darwin": section = db_name + '_test' diff --git a/utils/SQLiteDB.py b/utils/SQLiteDB.py index f5f8a35..ac3f67e 100644 --- a/utils/SQLiteDB.py +++ b/utils/SQLiteDB.py @@ -1,10 +1,14 @@ import sqlite3 from typing import Optional, List, Tuple, Any - +import os class SQLiteDB: - def __init__(self, db_path: str = "./sqlite/movies.db"): - self.db_path = db_path + def __init__(self, ): + current_directory = os.path.dirname(os.path.abspath(__file__)) + root_path = os.path.abspath(os.path.dirname(current_directory) + os.path.sep + ".") + project_name = root_path.split(os.path.sep)[-1] + project_root_path = os.path.abspath(os.path.dirname(__file__)).split(project_name)[0] + project_name+'/sqlite/movies.db' + self.db_path = project_root_path self.conn: Optional[sqlite3.Connection] = None self.cursor: Optional[sqlite3.Cursor] = None self._init_db() @@ -28,7 +32,7 @@ class SQLiteDB: """建立数据库连接(基于网页2、网页5的连接方式)[2,5](@ref)""" try: self.conn = sqlite3.connect(self.db_path) - self.cursor = self.ursor.cursor() + self.cursor = self.conn.cursor() # 修正拼写错误 self.conn.execute("PRAGMA foreign_keys = ON") # 启用外键约束 except sqlite3.Error as e: raise ConnectionError(f"数据库连接失败: {e}") @@ -101,10 +105,8 @@ if __name__ == "__main__": with SQLiteDB() as db: # 批量插入演示(集成网页5的executemany方法)[5](@ref) movies = [("泰坦尼克号", "/movies/titanic"), ("阿凡达", "/movies/avatar")] - db.executemany( - "INSERT INTO movie (name, path) VALUES (?, ?)", - movies - ) + db.insert_movie("泰坦尼克号","/movies/titanic") + # 查询未下载记录(基于网页6的查询模式)[6](@ref) print("待下载电影:", db.get_undownloaded())