From 8f19cc48bdfd9347cbc98456e016cd0feacbafd0 Mon Sep 17 00:00:00 2001 From: Makise42 Date: Thu, 23 Jan 2025 09:00:22 +0800 Subject: [PATCH 1/3] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=89=8B=E5=8A=A8?= =?UTF-8?q?=E8=BF=90=E8=A1=8CAni2Alist=E5=92=8CAlist2Strm=E7=9A=84?= =?UTF-8?q?=E9=80=89=E9=A1=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/main.py | 51 ++++++++++++++++++++++++++-- app/modules/alist2strm/alist2strm.py | 10 ++++++ app/modules/ani2alist/ani2alist.py | 11 ++++++ 3 files changed, 69 insertions(+), 3 deletions(-) diff --git a/app/main.py b/app/main.py index 297b1e6e..2a4dd1be 100644 --- a/app/main.py +++ b/app/main.py @@ -1,3 +1,4 @@ +import argparse from asyncio import get_event_loop from sys import path from os.path import dirname @@ -11,23 +12,63 @@ from app.extensions import LOGO from app.modules import Alist2Strm, Ani2Alist - def print_logo() -> None: """ 打印 Logo """ - print(LOGO) print(f" {settings.APP_NAME} {settings.APP_VERSION} ".center(65, "=")) print("") -if __name__ == "__main__": +def main(): + # 创建命令行参数解析器 + parser = argparse.ArgumentParser(description="AutoFilm 自动化工具") + + # 添加手动运行 Ani2Alist 的选项 + parser.add_argument( + "-ani2alist", + action="store_true", + help="手动运行 Ani2Alist 任务" + ) + # 添加手动运行 Alist2Strm 的选项 + parser.add_argument( + "-alist2strm", + action="store_true", + help="手动运行 Alist2Strm 任务" + ) + + args = parser.parse_args() + print_logo() logger.info(f"AutoFilm {settings.APP_VERSION} 启动中...") logger.debug(f"是否开启 DEBUG 模式: {settings.DEBUG}") + # 如果命令行参数中包含手动运行 Ani2Alist + if args.ani2alist: + # 手动运行 Ani2Alist + logger.info("手动运行 Ani2Alist 模块...") + if settings.Ani2AlistList: + for server in settings.Ani2AlistList: + ani2alist = Ani2Alist(**server) + ani2alist.run_manual() # 手动触发任务 + else: + logger.warning("未检测到 Ani2Alist 模块配置") + return # 手动运行后退出程序 + + # 如果命令行参数中包含手动运行 Alist2Strm + if args.alist2strm: + logger.info("手动运行 Alist2Strm 模块...") + if settings.AlistServerList: + for server in settings.AlistServerList: + alist2strm = Alist2Strm(**server) + alist2strm.run_manual() # 手动触发任务 + else: + logger.warning("未检测到 Alist2Strm 模块配置") + return # 手动运行后退出程序 + + # 定时任务部分 scheduler = AsyncIOScheduler() if settings.AlistServerList: @@ -65,3 +106,7 @@ def print_logo() -> None: get_event_loop().run_forever() except (KeyboardInterrupt, SystemExit): logger.info("AutoFilm 程序退出!") + + +if __name__ == "__main__": + main() diff --git a/app/modules/alist2strm/alist2strm.py b/app/modules/alist2strm/alist2strm.py index 22a8eb7c..d856b832 100644 --- a/app/modules/alist2strm/alist2strm.py +++ b/app/modules/alist2strm/alist2strm.py @@ -1,3 +1,4 @@ +from asyncio import get_event_loop from asyncio import to_thread, Semaphore, TaskGroup from os import PathLike from pathlib import Path @@ -13,6 +14,15 @@ class Alist2Strm: + # 添加手动运行 Alist2Strm 的选项 + def run_manual(self) -> None: + """ + 手动运行 Alist2Strm 任务 + """ + logger.info(f"手动启动 Alist2Strm 任务") + loop = get_event_loop() + loop.run_until_complete(self.run()) + def __init__( self, url: str = "http://localhost:5244", diff --git a/app/modules/ani2alist/ani2alist.py b/app/modules/ani2alist/ani2alist.py index ee9b503b..3de35bb0 100644 --- a/app/modules/ani2alist/ani2alist.py +++ b/app/modules/ani2alist/ani2alist.py @@ -1,3 +1,4 @@ +from asyncio import get_event_loop from typing import Final from datetime import datetime @@ -17,6 +18,16 @@ class Ani2Alist: + + # 添加手动运行 Ani2Alist 的选项 + def run_manual(self) -> None: + """ + 手动运行 Ani2Alist 任务 + """ + logger.info(f"手动启动 Ani2Alist 任务") + loop = get_event_loop() + loop.run_until_complete(self.run()) + """ 将 ANI Open 项目的视频通过地址树的方式挂载在 Alist服务器上 """ From 65263e86f4a3cda665792dafc62878f9d85d956c Mon Sep 17 00:00:00 2001 From: Makise42 Date: Tue, 18 Feb 2025 22:07:40 +0800 Subject: [PATCH 2/3] =?UTF-8?q?=E4=BF=AE=E6=94=B9:=E5=BD=93=E5=90=8E?= =?UTF-8?q?=E7=BC=80=E5=9C=A8other=5Fext=E9=87=8C=E6=97=B6=E4=BC=9A?= =?UTF-8?q?=E8=A2=AB=E4=B8=8B=E8=BD=BD=E8=80=8C=E4=B8=8D=E6=98=AF=E8=BD=AC?= =?UTF-8?q?=E6=8D=A2=E4=B8=BAstrm?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/modules/alist2strm/alist2strm.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/app/modules/alist2strm/alist2strm.py b/app/modules/alist2strm/alist2strm.py index d856b832..2253d812 100644 --- a/app/modules/alist2strm/alist2strm.py +++ b/app/modules/alist2strm/alist2strm.py @@ -202,9 +202,10 @@ def __get_local_path(self, path: AlistPath) -> Path: relative_path = relative_path[1:] local_path = self.target_dir / relative_path - if path.suffix.lower() in VIDEO_EXTS: + ext = path.suffix.lower() + # 修改:当后缀在other_ext里时会被下载而不是转换为strm + if ext in VIDEO_EXTS and ext.lstrip('.') not in self.download_exts: local_path = local_path.with_suffix(".strm") - return local_path async def __cleanup_local_files(self) -> None: From 24c7584a8fc8b21dea78de461164ad4849d40508 Mon Sep 17 00:00:00 2001 From: Makise42 Date: Sun, 25 May 2025 17:33:22 +0800 Subject: [PATCH 3/3] Fix indentation error in alist2strm.py for Cython compilation --- app/modules/alist2strm/alist2strm.py | 531 +++++++++++++-------------- 1 file changed, 265 insertions(+), 266 deletions(-) diff --git a/app/modules/alist2strm/alist2strm.py b/app/modules/alist2strm/alist2strm.py index 3c49cf82..6f095c3a 100644 --- a/app/modules/alist2strm/alist2strm.py +++ b/app/modules/alist2strm/alist2strm.py @@ -1,266 +1,265 @@ -from asyncio import get_event_loop -from asyncio import to_thread, Semaphore, TaskGroup -from os import PathLike -from pathlib import Path -from re import compile as re_compile - -from aiofile import async_open - -from app.core import logger -from app.utils import RequestUtils -from app.extensions import VIDEO_EXTS, SUBTITLE_EXTS, IMAGE_EXTS, NFO_EXTS -from app.modules.alist import AlistClient, AlistPath - - -class Alist2Strm: - # 添加手动运行 Alist2Strm 的选项 - def run_manual(self) -> None: - """ - 手动运行 Alist2Strm 任务 - """ - logger.info(f"手动启动 Alist2Strm 任务") - loop = get_event_loop() - loop.run_until_complete(self.run()) - - def __init__( - self, - url: str = "http://localhost:5244", - username: str = "", - password: str = "", - token: str = "", - source_dir: str = "/", - target_dir: str | PathLike = "", - flatten_mode: bool = False, - subtitle: bool = False, - image: bool = False, - nfo: bool = False, - mode: str = "AlistURL", - overwrite: bool = False, - other_ext: str = "", - max_workers: int = 50, - max_downloaders: int = 5, - wait_time: float | int = 0, - sync_server: bool = False, - sync_ignore: str | None = None, - **_, - ) -> None: - """ - 实例化 Alist2Strm 对象 - - :param url: Alist 服务器地址,默认为 "http://localhost:5244" - :param username: Alist 用户名,默认为空 - :param password: Alist 密码,默认为空 - :param source_dir: 需要同步的 Alist 的目录,默认为 "/" - :param target_dir: strm 文件输出目录,默认为当前工作目录 - :param flatten_mode: 平铺模式,将所有 Strm 文件保存至同一级目录,默认为 False - :param subtitle: 是否下载字幕文件,默认为 False - :param image: 是否下载图片文件,默认为 False - :param nfo: 是否下载 .nfo 文件,默认为 False - :param mode: Strm模式(AlistURL/RawURL/AlistPath) - :param overwrite: 本地路径存在同名文件时是否重新生成/下载该文件,默认为 False - :param sync_server: 是否同步服务器,启用后若服务器中删除了文件,也会将本地文件删除,默认为 True - :param other_ext: 自定义下载后缀,使用西文半角逗号进行分割,默认为空 - :param max_workers: 最大并发数 - :param max_downloaders: 最大同时下载 - :param wait_time: 遍历请求间隔时间,单位为秒,默认为 0 - :param sync_ignore: 同步时忽略的文件正则表达式 - """ - - self.client = AlistClient(url, username, password, token) - self.mode = mode - - self.source_dir = source_dir - self.target_dir = Path(target_dir) - - self.flatten_mode = flatten_mode - if flatten_mode: - subtitle = image = nfo = False - - download_exts: set[str] = set() - if subtitle: - download_exts |= SUBTITLE_EXTS - if image: - download_exts |= IMAGE_EXTS - if nfo: - download_exts |= NFO_EXTS - if other_ext: - download_exts |= frozenset(other_ext.lower().split(",")) - - self.download_exts = download_exts - self.process_file_exts = VIDEO_EXTS | download_exts - - self.overwrite = overwrite - self.__max_workers = Semaphore(max_workers) - self.__max_downloaders = Semaphore(max_downloaders) - self.wait_time = wait_time - self.sync_server = sync_server - - if sync_ignore: - self.sync_ignore_pattern = re_compile(sync_ignore) - else: - self.sync_ignore_pattern = None - - async def run(self) -> None: - """ - 处理主体 - """ - - def filter(path: AlistPath) -> bool: - """ - 过滤器 - 根据 Alist2Strm 配置判断是否需要处理该文件 - 将云盘上上的文件对应的本地文件路径保存至 self.processed_local_paths - - :param path: AlistPath 对象 - """ - - if path.is_dir: - return False - - if path.suffix.lower() not in self.process_file_exts: - logger.debug(f"文件 {path.name} 不在处理列表中") - return False - - try: - local_path = self.__get_local_path(path) - except OSError as e: # 可能是文件名过长 - logger.warning(f"获取 {path.path} 本地路径失败:{e}") - return False - - self.processed_local_paths.add(local_path) - - if not self.overwrite and local_path.exists(): - if path.suffix in self.download_exts: - local_path_stat = local_path.stat() - if local_path_stat.st_mtime < path.modified_timestamp: - logger.debug( - f"文件 {local_path.name} 已过期,需要重新处理 {path.path}" - ) - return True - if local_path_stat.st_size < path.size: - logger.debug( - f"文件 {local_path.name} 大小不一致,可能是本地文件损坏,需要重新处理 {path.path}" - ) - return True - logger.debug(f"文件 {local_path.name} 已存在,跳过处理 {path.path}") - return False - - return True - - if self.mode not in ["AlistURL", "RawURL", "AlistPath"]: - logger.warning( - f"Alist2Strm 的模式 {self.mode} 不存在,已设置为默认模式 AlistURL" - ) - self.mode = "AlistURL" - - if self.mode == "RawURL": - is_detail = True - else: - is_detail = False - - self.processed_local_paths = set() # 云盘文件对应的本地文件路径 - - async with self.__max_workers, TaskGroup() as tg: - async for path in self.client.iter_path( - dir_path=self.source_dir, - wait_time=self.wait_time, - is_detail=is_detail, - filter=filter, - ): - tg.create_task(self.__file_processer(path)) - - if self.sync_server: - await self.__cleanup_local_files() - logger.info("清理过期的 .strm 文件完成") - logger.info("Alist2Strm 处理完成") - - async def __file_processer(self, path: AlistPath) -> None: - """ - 异步保存文件至本地 - - :param path: AlistPath 对象 - """ - local_path = self.__get_local_path(path) - - if self.mode == "AlistURL": - content = path.download_url - elif self.mode == "RawURL": - content = path.raw_url - elif self.mode == "AlistPath": - content = path.path - else: - raise ValueError(f"AlistStrm 未知的模式 {self.mode}") - - await to_thread(local_path.parent.mkdir, parents=True, exist_ok=True) - - logger.debug(f"开始处理 {local_path}") - if local_path.suffix == ".strm": - async with async_open(local_path, mode="w", encoding="utf-8") as file: - await file.write(content) - logger.info(f"{local_path.name} 创建成功") - else: - async with self.__max_downloaders: - await RequestUtils.download(path.download_url, local_path) - logger.info(f"{local_path.name} 下载成功") - - def __get_local_path(self, path: AlistPath) -> Path: - """ - 根据给定的 AlistPath 对象和当前的配置,计算出本地文件路径。 - - :param path: AlistPath 对象 - :return: 本地文件路径 - """ - if self.flatten_mode: - local_path = self.target_dir / path.name - else: - relative_path = path.path.replace(self.source_dir, "", 1) - if relative_path.startswith("/"): - relative_path = relative_path[1:] - local_path = self.target_dir / relative_path - - if path.suffix.lower() in VIDEO_EXTS: - ext = path.suffix.lower() - # 修改:当后缀在other_ext里时会被下载而不是转换为strm - if ext in VIDEO_EXTS and ext.lstrip('.') not in self.download_exts: - local_path = local_path.with_suffix(".strm") - - return local_path - - async def __cleanup_local_files(self) -> None: - """ - 删除服务器中已删除的本地的 .strm 文件及其关联文件 - 如果文件后缀在 sync_ignore 中,则不会被删除 - """ - logger.info("开始清理本地文件") - - if self.flatten_mode: - all_local_files = [f for f in self.target_dir.iterdir() if f.is_file()] - else: - all_local_files = [f for f in self.target_dir.rglob("*") if f.is_file()] - - files_to_delete = set(all_local_files) - self.processed_local_paths - - for file_path in files_to_delete: - # 检查文件是否匹配忽略正则表达式 - if self.sync_ignore_pattern and self.sync_ignore_pattern.search( - file_path.name - ): - logger.debug(f"文件 {file_path.name} 在忽略列表中,跳过删除") - continue - - try: - if file_path.exists(): - await to_thread(file_path.unlink) - logger.info(f"删除文件:{file_path}") - - # 检查并删除空目录 - parent_dir = file_path.parent - while parent_dir != self.target_dir: - if any(parent_dir.iterdir()): - break # 目录不为空,跳出循环 - else: - parent_dir.rmdir() - logger.info(f"删除空目录:{parent_dir}") - parent_dir = parent_dir.parent - except Exception as e: - logger.error(f"删除文件 {file_path} 失败:{e}") +from asyncio import get_event_loop +from asyncio import to_thread, Semaphore, TaskGroup +from os import PathLike +from pathlib import Path +from re import compile as re_compile + +from aiofile import async_open + +from app.core import logger +from app.utils import RequestUtils +from app.extensions import VIDEO_EXTS, SUBTITLE_EXTS, IMAGE_EXTS, NFO_EXTS +from app.modules.alist import AlistClient, AlistPath + + +class Alist2Strm: + + # 添加手动运行 Alist2Strm 的选项 + def run_manual(self) -> None: + """ + 手动运行 Alist2Strm 任务 + """ + logger.info(f"手动启动 Alist2Strm 任务") + loop = get_event_loop() + loop.run_until_complete(self.run()) + + def __init__( + self, + url: str = "http://localhost:5244", + username: str = "", + password: str = "", + token: str = "", + source_dir: str = "/", + target_dir: str | PathLike = "", + flatten_mode: bool = False, + subtitle: bool = False, + image: bool = False, + nfo: bool = False, + mode: str = "AlistURL", + overwrite: bool = False, + other_ext: str = "", + max_workers: int = 50, + max_downloaders: int = 5, + wait_time: float | int = 0, + sync_server: bool = False, + sync_ignore: str | None = None, + **_, + ) -> None: + """ + 实例化 Alist2Strm 对象 + + :param url: Alist 服务器地址,默认为 "http://localhost:5244" + :param username: Alist 用户名,默认为空 + :param password: Alist 密码,默认为空 + :param source_dir: 需要同步的 Alist 的目录,默认为 "/" + :param target_dir: strm 文件输出目录,默认为当前工作目录 + :param flatten_mode: 平铺模式,将所有 Strm 文件保存至同一级目录,默认为 False + :param subtitle: 是否下载字幕文件,默认为 False + :param image: 是否下载图片文件,默认为 False + :param nfo: 是否下载 .nfo 文件,默认为 False + :param mode: Strm模式(AlistURL/RawURL/AlistPath) + :param overwrite: 本地路径存在同名文件时是否重新生成/下载该文件,默认为 False + :param sync_server: 是否同步服务器,启用后若服务器中删除了文件,也会将本地文件删除,默认为 True + :param other_ext: 自定义下载后缀,使用西文半角逗号进行分割,默认为空 + :param max_workers: 最大并发数 + :param max_downloaders: 最大同时下载 + :param wait_time: 遍历请求间隔时间,单位为秒,默认为 0 + :param sync_ignore: 同步时忽略的文件正则表达式 + """ + + self.client = AlistClient(url, username, password, token) + self.mode = mode + + self.source_dir = source_dir + self.target_dir = Path(target_dir) + + self.flatten_mode = flatten_mode + if flatten_mode: + subtitle = image = nfo = False + + download_exts: set[str] = set() + if subtitle: + download_exts |= SUBTITLE_EXTS + if image: + download_exts |= IMAGE_EXTS + if nfo: + download_exts |= NFO_EXTS + if other_ext: + download_exts |= frozenset(other_ext.lower().split(",")) + + self.download_exts = download_exts + self.process_file_exts = VIDEO_EXTS | download_exts + + self.overwrite = overwrite + self.__max_workers = Semaphore(max_workers) + self.__max_downloaders = Semaphore(max_downloaders) + self.wait_time = wait_time + self.sync_server = sync_server + + if sync_ignore: + self.sync_ignore_pattern = re_compile(sync_ignore) + else: + self.sync_ignore_pattern = None + + async def run(self) -> None: + """ + 处理主体 + """ + + def filter(path: AlistPath) -> bool: + """ + 过滤器 + 根据 Alist2Strm 配置判断是否需要处理该文件 + 将云盘上上的文件对应的本地文件路径保存至 self.processed_local_paths + + :param path: AlistPath 对象 + """ + + if path.is_dir: + return False + + if path.suffix.lower() not in self.process_file_exts: + logger.debug(f"文件 {path.name} 不在处理列表中") + return False + + try: + local_path = self.__get_local_path(path) + except OSError as e: # 可能是文件名过长 + logger.warning(f"获取 {path.path} 本地路径失败:{e}") + return False + + self.processed_local_paths.add(local_path) + + if not self.overwrite and local_path.exists(): + if path.suffix in self.download_exts: + local_path_stat = local_path.stat() + if local_path_stat.st_mtime < path.modified_timestamp: + logger.debug( + f"文件 {local_path.name} 已过期,需要重新处理 {path.path}" + ) + return True + if local_path_stat.st_size < path.size: + logger.debug( + f"文件 {local_path.name} 大小不一致,可能是本地文件损坏,需要重新处理 {path.path}" + ) + return True + logger.debug(f"文件 {local_path.name} 已存在,跳过处理 {path.path}") + return False + + return True + + if self.mode not in ["AlistURL", "RawURL", "AlistPath"]: + logger.warning( + f"Alist2Strm 的模式 {self.mode} 不存在,已设置为默认模式 AlistURL" + ) + self.mode = "AlistURL" + + if self.mode == "RawURL": + is_detail = True + else: + is_detail = False + + self.processed_local_paths = set() # 云盘文件对应的本地文件路径 + + async with self.__max_workers, TaskGroup() as tg: + async for path in self.client.iter_path( + dir_path=self.source_dir, + wait_time=self.wait_time, + is_detail=is_detail, + filter=filter, + ): + tg.create_task(self.__file_processer(path)) + + if self.sync_server: + await self.__cleanup_local_files() + logger.info("清理过期的 .strm 文件完成") + logger.info("Alist2Strm 处理完成") + + async def __file_processer(self, path: AlistPath) -> None: + """ + 异步保存文件至本地 + + :param path: AlistPath 对象 + """ + local_path = self.__get_local_path(path) + + if self.mode == "AlistURL": + content = path.download_url + elif self.mode == "RawURL": + content = path.raw_url + elif self.mode == "AlistPath": + content = path.path + else: + raise ValueError(f"AlistStrm 未知的模式 {self.mode}") + + await to_thread(local_path.parent.mkdir, parents=True, exist_ok=True) + + logger.debug(f"开始处理 {local_path}") + if local_path.suffix == ".strm": + async with async_open(local_path, mode="w", encoding="utf-8") as file: + await file.write(content) + logger.info(f"{local_path.name} 创建成功") + else: + async with self.__max_downloaders: + await RequestUtils.download(path.download_url, local_path) + logger.info(f"{local_path.name} 下载成功") + + def __get_local_path(self, path: AlistPath) -> Path: + """ + 根据给定的 AlistPath 对象和当前的配置,计算出本地文件路径。 + + :param path: AlistPath 对象 + :return: 本地文件路径 + """ + if self.flatten_mode: + local_path = self.target_dir / path.name + else: + relative_path = path.path.replace(self.source_dir, "", 1) + if relative_path.startswith("/"): + relative_path = relative_path[1:] + local_path = self.target_dir / relative_path + + ext = path.suffix.lower() + # 修改:当后缀在other_ext里时会被下载而不是转换为strm + if ext in VIDEO_EXTS and ext.lstrip('.') not in self.download_exts: + local_path = local_path.with_suffix(".strm") + return local_path + + async def __cleanup_local_files(self) -> None: + """ + 删除服务器中已删除的本地的 .strm 文件及其关联文件 + 如果文件后缀在 sync_ignore 中,则不会被删除 + """ + logger.info("开始清理本地文件") + + if self.flatten_mode: + all_local_files = [f for f in self.target_dir.iterdir() if f.is_file()] + else: + all_local_files = [f for f in self.target_dir.rglob("*") if f.is_file()] + + files_to_delete = set(all_local_files) - self.processed_local_paths + + for file_path in files_to_delete: + # 检查文件是否匹配忽略正则表达式 + if self.sync_ignore_pattern and self.sync_ignore_pattern.search( + file_path.name + ): + logger.debug(f"文件 {file_path.name} 在忽略列表中,跳过删除") + continue + + try: + if file_path.exists(): + await to_thread(file_path.unlink) + logger.info(f"删除文件:{file_path}") + + # 检查并删除空目录 + parent_dir = file_path.parent + while parent_dir != self.target_dir: + if any(parent_dir.iterdir()): + break # 目录不为空,跳出循环 + else: + parent_dir.rmdir() + logger.info(f"删除空目录:{parent_dir}") + parent_dir = parent_dir.parent + except Exception as e: + logger.error(f"删除文件 {file_path} 失败:{e}") \ No newline at end of file