From 0593275a623ed2842775b42fbf02146846497e37 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Sun, 29 Sep 2024 23:46:41 +0800 Subject: [PATCH 1/9] feat(module): add ServiceBaseHelper for service and instance --- app/helper/downloader.py | 15 +++++- app/helper/mediaserver.py | 15 +++++- app/helper/notification.py | 18 +++++-- app/helper/servicebase.py | 97 +++++++++++++++++++++++++++++++++++++ app/helper/serviceconfig.py | 2 +- app/modules/__init__.py | 8 +++ app/schemas/system.py | 20 +++++++- 7 files changed, 166 insertions(+), 9 deletions(-) create mode 100644 app/helper/servicebase.py diff --git a/app/helper/downloader.py b/app/helper/downloader.py index 86d3d557..6ffcab13 100644 --- a/app/helper/downloader.py +++ b/app/helper/downloader.py @@ -1,5 +1,16 @@ -class DownloaderHelper: +from app.helper.servicebase import ServiceBaseHelper +from app.schemas import DownloaderConf +from app.schemas.types import SystemConfigKey + + +class DownloaderHelper(ServiceBaseHelper[DownloaderConf]): """ 下载器帮助类 """ - pass + + def __init__(self): + super().__init__( + config_key=SystemConfigKey.Downloaders, + conf_type=DownloaderConf, + modules=["QbittorrentModule", "TransmissionModule"] + ) diff --git a/app/helper/mediaserver.py b/app/helper/mediaserver.py index a21a1155..1c17eaa0 100644 --- a/app/helper/mediaserver.py +++ b/app/helper/mediaserver.py @@ -1,5 +1,16 @@ -class MediaServerHelper: +from app.helper.servicebase import ServiceBaseHelper +from app.schemas import MediaServerConf +from app.schemas.types import SystemConfigKey + + +class MediaServerHelper(ServiceBaseHelper[MediaServerConf]): """ 媒体服务器帮助类 """ - pass + + def __init__(self): + super().__init__( + config_key=SystemConfigKey.MediaServers, + conf_type=MediaServerConf, + modules=["PlexModule", "EmbyModule", "JellyfinModule"] + ) diff --git a/app/helper/notification.py b/app/helper/notification.py index 8d32d452..e5fcd280 100644 --- a/app/helper/notification.py +++ b/app/helper/notification.py @@ -1,5 +1,17 @@ -class NotificationHelper: +from app.helper.servicebase import ServiceBaseHelper +from app.schemas import NotificationConf +from app.schemas.types import SystemConfigKey + + +class NotificationHelper(ServiceBaseHelper[NotificationConf]): """ - 消息通知渠道帮助类 + 消息通知帮助类 """ - pass + + def __init__(self): + super().__init__( + config_key=SystemConfigKey.Notifications, + conf_type=NotificationConf, + modules=["WechatModule", "WebPushModule", "VoceChatModule", "TelegramModule", "SynologyChatModule", + "SlackModule"] + ) diff --git a/app/helper/servicebase.py b/app/helper/servicebase.py new file mode 100644 index 00000000..bfd831f7 --- /dev/null +++ b/app/helper/servicebase.py @@ -0,0 +1,97 @@ +from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator + +from app.core.module import ModuleManager +from app.helper.serviceconfig import ServiceConfigHelper +from app.schemas import ServiceInfo +from app.schemas.types import SystemConfigKey + +TConf = TypeVar("TConf") + + +class ServiceBaseHelper(Generic[TConf]): + """ + 通用服务帮助类,抽象获取配置和服务实例的通用逻辑 + """ + + def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf], modules: List[str]): + self.modulemanager = ModuleManager() + self.config_key = config_key + self.conf_type = conf_type + self.modules = modules + + def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]: + """ + 获取配置列表 + + :param include_disabled: 是否包含禁用的配置,默认 False(仅返回启用的配置) + :return: 配置字典 + """ + configs: List[TConf] = ServiceConfigHelper.get_configs(self.config_key, self.conf_type) + return { + config.name: config + for config in configs + if (config.name and config.type and config.enabled) or include_disabled + } if configs else {} + + def get_config(self, name: str) -> Optional[TConf]: + """ + 获取指定名称配置 + """ + if not name: + return None + configs = self.get_configs() + return configs.get(name) + + def iterate_module_instances(self) -> Iterator[ServiceInfo]: + """ + 迭代所有模块的实例及其对应的配置,返回 ServiceInfo 实例 + """ + configs = self.get_configs() + for module_name in self.modules: + module = self.modulemanager.get_running_module(module_name) + if not module: + continue + module_instances = module.get_instances() + if not isinstance(module_instances, dict): + continue + for name, instance in module_instances.items(): + if not instance: + continue + config = configs.get(name) + service_info = ServiceInfo( + name=name, + instance=instance, + module=module, + type=config.type if config else None, + config=config + ) + yield service_info + + def get_services(self, type_filter: Optional[str] = None) -> Dict[str, ServiceInfo]: + """ + 获取服务信息列表,并根据类型过滤 + + :param type_filter: 需要过滤的服务类型 + :return: 过滤后的服务信息字典 + """ + return { + service_info.name: service_info + for service_info in self.iterate_module_instances() + if service_info.config and (type_filter is None or service_info.type == type_filter) + } + + def get_service(self, name: str, type_filter: Optional[str] = None) -> Optional[ServiceInfo]: + """ + 获取指定名称的服务信息,并根据类型过滤 + + :param name: 服务名称 + :param type_filter: 需要过滤的服务类型 + :return: 对应的服务信息,若不存在或类型不匹配则返回 None + """ + if not name: + return None + for service_info in self.iterate_module_instances(): + if service_info.name == name: + if service_info.config and (type_filter is None or service_info.type == type_filter): + return service_info + return None diff --git a/app/helper/serviceconfig.py b/app/helper/serviceconfig.py index c65cdc12..6822d22e 100644 --- a/app/helper/serviceconfig.py +++ b/app/helper/serviceconfig.py @@ -56,7 +56,7 @@ class ServiceConfigHelper: @staticmethod def get_notification_switch(mtype: NotificationType) -> Optional[str]: """ - 获取消息通知场景开关 + 获取指定类型的消息通知场景的开关 """ switchs = ServiceConfigHelper.get_notification_switches() for switch in switchs: diff --git a/app/modules/__init__.py b/app/modules/__init__.py index 3a54d817..d8263e10 100644 --- a/app/modules/__init__.py +++ b/app/modules/__init__.py @@ -90,6 +90,14 @@ class ServiceBase(Generic[TService, TConf], metaclass=ABCMeta): # 如果传入的是工厂函数,直接调用工厂函数 self._instances[conf.name] = service_type(conf) + def get_instances(self) -> Dict[str, TService]: + """ + 获取服务实例列表 + + :return: 返回服务实例列表 + """ + return self._instances + def get_instance(self, name: str) -> Optional[TService]: """ 获取服务实例 diff --git a/app/schemas/system.py b/app/schemas/system.py index f39be609..3680957f 100644 --- a/app/schemas/system.py +++ b/app/schemas/system.py @@ -1,8 +1,26 @@ -from typing import Optional +from dataclasses import dataclass +from typing import Optional, Any from pydantic import BaseModel +@dataclass +class ServiceInfo: + """ + 封装服务相关信息的数据类 + """ + # 名称 + name: Optional[str] = None + # 实例 + instance: Optional[Any] = None + # 模块 + module: Optional[Any] = None + # 类型 + type: Optional[str] = None + # 配置 + config: Optional[Any] = None + + class MediaServerConf(BaseModel): """ 媒体服务器配置 From fdab59a84eeab5142a8714b6bf1633a8742cfeab Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:31:03 +0800 Subject: [PATCH 2/9] fix #2784 --- app/db/site_oper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/db/site_oper.py b/app/db/site_oper.py index 7956e65d..768a907b 100644 --- a/app/db/site_oper.py +++ b/app/db/site_oper.py @@ -153,7 +153,7 @@ class SiteOper(DbOper): 更新站点图标 """ icon_base64 = f"data:image/ico;base64,{icon_base64}" if icon_base64 else "" - siteicon = self.get_by_domain(domain) + siteicon = self.get_icon_by_domain(domain) if not siteicon: SiteIcon(name=name, domain=domain, url=icon_url, base64=icon_base64).create(self._db) elif icon_base64: From 709f8ef3edf210d1faed66453c61fb52c351a2c2 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 30 Sep 2024 00:38:01 +0800 Subject: [PATCH 3/9] chore: Update .gitignore to exclude all log files and archives --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 9d135e49..7ec674ab 100644 --- a/.gitignore +++ b/.gitignore @@ -14,6 +14,7 @@ app/plugins/** config/cookies/** config/user.db config/sites/** +config/logs *.pyc *.log .vscode From 61ecc175f39e2735655d33e01797843fa239a9cc Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 30 Sep 2024 02:13:45 +0800 Subject: [PATCH 4/9] chore: Update .gitignore --- .gitignore | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 7ec674ab..705c952f 100644 --- a/.gitignore +++ b/.gitignore @@ -14,7 +14,9 @@ app/plugins/** config/cookies/** config/user.db config/sites/** -config/logs +config/logs/ +config/temp/ +config/cache/ *.pyc *.log .vscode From 838e17bf6ebcc5ebe36f7d1e7647cb85ec240bfc Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 30 Sep 2024 02:59:09 +0800 Subject: [PATCH 5/9] fix(sync): have module return results directly instead of using yield --- app/modules/emby/__init__.py | 2 +- app/modules/jellyfin/__init__.py | 2 +- app/modules/plex/__init__.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 11d22b77..0760b782 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -187,7 +187,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]): """ server: Emby = self.get_instance(server) if server: - yield from server.get_items(library_id, start_index, limit) + return server.get_items(library_id, start_index, limit) return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index 35718fe1..814f6cae 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -185,7 +185,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]): """ server: Jellyfin = self.get_instance(server) if server: - yield from server.get_items(library_id, start_index, limit) + return server.get_items(library_id, start_index, limit) return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index dfe339e0..0643a544 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -173,7 +173,7 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]): """ server: Plex = self.get_instance(server) if server: - yield from server.get_items(library_id, start_index, limit) + return server.get_items(library_id, start_index, limit) return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: From 06ea9e2d096aa7c01f2aab12926effe181a3ead0 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 30 Sep 2024 10:26:32 +0800 Subject: [PATCH 6/9] fix siteuserdata --- app/chain/site.py | 1 + app/db/models/siteuserdata.py | 2 ++ app/db/site_oper.py | 9 +++++++- database/versions/0fb94bf69b38_2_0_2.py | 30 +++++++++++++++++++++++++ 4 files changed, 41 insertions(+), 1 deletion(-) create mode 100644 database/versions/0fb94bf69b38_2_0_2.py diff --git a/app/chain/site.py b/app/chain/site.py index 748b4330..48fd6fa2 100644 --- a/app/chain/site.py +++ b/app/chain/site.py @@ -64,6 +64,7 @@ class SiteChain(ChainBase): userdata: SiteUserData = self.run_module("refresh_userdata", site=site) if userdata: self.siteoper.update_userdata(domain=StringUtils.get_url_domain(site.get("domain")), + name=site.get("name"), payload=userdata.dict()) return userdata diff --git a/app/db/models/siteuserdata.py b/app/db/models/siteuserdata.py index ce23a970..0db26662 100644 --- a/app/db/models/siteuserdata.py +++ b/app/db/models/siteuserdata.py @@ -13,6 +13,8 @@ class SiteUserData(Base): id = Column(Integer, Sequence('id'), primary_key=True, index=True) # 站点域名 domain = Column(String, index=True) + # 站点名称 + name = Column(String) # 用户名 username = Column(String) # 用户ID diff --git a/app/db/site_oper.py b/app/db/site_oper.py index 768a907b..7a2f9c02 100644 --- a/app/db/site_oper.py +++ b/app/db/site_oper.py @@ -105,7 +105,7 @@ class SiteOper(DbOper): }) return True, "更新站点RSS地址成功" - def update_userdata(self, domain: str, payload: dict) -> Tuple[bool, str]: + def update_userdata(self, domain: str, name: str, payload: dict) -> Tuple[bool, str]: """ 更新站点用户数据 """ @@ -114,6 +114,7 @@ class SiteOper(DbOper): current_time = datetime.now().strftime('%H:%M:%S') payload.update({ "domain": domain, + "name": name, "updated_day": current_day, "updated_time": current_time }) @@ -130,6 +131,12 @@ class SiteOper(DbOper): SiteUserData(**payload).create(self._db) return True, "更新站点用户数据成功" + def get_userdata(self) -> List[SiteUserData]: + """ + 获取站点用户数据 + """ + return SiteUserData.list(self._db) + def get_userdata_by_domain(self, domain: str, workdate: str = None) -> List[SiteUserData]: """ 获取站点用户数据 diff --git a/database/versions/0fb94bf69b38_2_0_2.py b/database/versions/0fb94bf69b38_2_0_2.py new file mode 100644 index 00000000..14fa4c9e --- /dev/null +++ b/database/versions/0fb94bf69b38_2_0_2.py @@ -0,0 +1,30 @@ +"""2.0.2 + +Revision ID: 0fb94bf69b38 +Revises: 262735d025da +Create Date: 2024-09-30 10:03:58.546036 + +""" +import contextlib + +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = '0fb94bf69b38' +down_revision = '262735d025da' +branch_labels = None +depends_on = None + + +def upgrade() -> None: + # ### commands auto generated by Alembic - please adjust! ### + with contextlib.suppress(Exception): + with op.batch_alter_table("siteuserdata") as batch_op: + batch_op.add_column(sa.Column('name', sa.VARCHAR)) + # ### end Alembic commands ### + + +def downgrade() -> None: + pass From e731767dfac01a14124fa9d8d986f681696ca755 Mon Sep 17 00:00:00 2001 From: InfinityPacer <160988576+InfinityPacer@users.noreply.github.com> Date: Mon, 30 Sep 2024 10:33:20 +0800 Subject: [PATCH 7/9] feat(plugin): add PluginTriggered event type --- app/schemas/types.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/app/schemas/types.py b/app/schemas/types.py index 5dc30960..47096ebe 100644 --- a/app/schemas/types.py +++ b/app/schemas/types.py @@ -18,8 +18,10 @@ class TorrentStatus(Enum): class EventType(Enum): # 插件需要重载 PluginReload = "plugin.reload" - # 插件动作 + # 触发插件动作 PluginAction = "plugin.action" + # 插件触发事件 + PluginTriggered = "plugin.triggered" # 执行命令 CommandExcute = "command.excute" # 站点已删除 From 80a1ded60240742b4c9d216044dfc5c70232f59e Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 30 Sep 2024 12:06:07 +0800 Subject: [PATCH 8/9] fix scraping file upload --- app/chain/media.py | 60 +++++++++++++++++++++++++++++++++------------- 1 file changed, 44 insertions(+), 16 deletions(-) diff --git a/app/chain/media.py b/app/chain/media.py index 2ddd6ae7..4af7723b 100644 --- a/app/chain/media.py +++ b/app/chain/media.py @@ -30,6 +30,10 @@ class MediaChain(ChainBase, metaclass=Singleton): # 临时识别结果 {title, name, year, season, episode} recognize_temp: Optional[dict] = None + def __init__(self): + super().__init__() + self.storagechain = StorageChain() + def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo, season: int = None, episode: int = None) -> Optional[str]: """ @@ -348,37 +352,35 @@ class MediaChain(ChainBase, metaclass=Singleton): def scrape_metadata(self, fileitem: schemas.FileItem, meta: MetaBase = None, mediainfo: MediaInfo = None, - init_folder: bool = True, parent: schemas.FileItem = None): + init_folder: bool = True, parent: schemas.FileItem = None, + overwrite: bool = False): """ 手动刮削媒体信息 + :param fileitem: 刮削目录或文件 + :param meta: 元数据 + :param mediainfo: 媒体信息 + :param init_folder: 是否刮削根目录 + :param parent: 上级目录 + :param overwrite: 是否覆盖已有文件 """ def __list_files(_fileitem: schemas.FileItem): """ 列出下级文件 """ - return StorageChain().list_files(fileitem=_fileitem) + return self.storagechain.list_files(fileitem=_fileitem) - def __save_file(_fileitem: schemas.FileItem, _path: Path, _content: Union[bytes, str], - overwrite: bool = False): + def __save_file(_fileitem: schemas.FileItem, _path: Path, _content: Union[bytes, str]): """ 保存或上传文件 :param _fileitem: 关联的媒体文件项 :param _path: 元数据文件路径 :param _content: 文件内容 - :param overwrite: 是否覆盖 """ - if not overwrite and _path.exists(): - return tmp_file = settings.TEMP_PATH / _path.name tmp_file.write_bytes(_content) - upload_item = copy.deepcopy(_fileitem) - upload_item.path = str(_path) - upload_item.name = _path.name - upload_item.basename = _path.stem - upload_item.extension = _path.suffix logger.info(f"保存文件:【{_fileitem.storage}】{_path}") - StorageChain().upload_file(fileitem=upload_item, path=tmp_file) + self.storagechain.upload_file(fileitem=_fileitem, path=tmp_file) if tmp_file.exists(): tmp_file.unlink() @@ -419,7 +421,11 @@ class MediaChain(ChainBase, metaclass=Singleton): logger.warn(f"{filepath.name} nfo文件生成失败!") return # 保存或上传nfo文件到上级目录 - __save_file(_fileitem=parent, _path=filepath.with_suffix(".nfo"), _content=movie_nfo) + nfo_path = filepath.with_suffix(".nfo") + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path): + logger.info(f"已存在nfo文件:{nfo_path}") + return + __save_file(_fileitem=parent, _path=nfo_path, _content=movie_nfo) else: # 电影目录 files = __list_files(_fileitem=fileitem) @@ -438,6 +444,9 @@ class MediaChain(ChainBase, metaclass=Singleton): and attr_value.startswith("http"): image_name = attr_name.replace("_path", "") + Path(attr_value).suffix image_path = filepath / image_name + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path): + logger.info(f"已存在图片文件:{image_path}") + continue # 下载图片 content = __download_image(_url=attr_value) # 写入图片到当前目录 @@ -461,17 +470,24 @@ class MediaChain(ChainBase, metaclass=Singleton): logger.warn(f"{filepath.name} nfo生成失败!") return # 保存或上传nfo文件到上级目录 - __save_file(_fileitem=parent, _path=filepath.with_suffix(".nfo"), _content=episode_nfo) + nfo_path = filepath.with_suffix(".nfo") + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path): + logger.info(f"已存在nfo文件:{nfo_path}") + return + __save_file(_fileitem=parent, _path=nfo_path, _content=episode_nfo) # 获取集的图片 image_dict = self.metadata_img(mediainfo=file_mediainfo, season=file_meta.begin_season, episode=file_meta.begin_episode) if image_dict: for episode, image_url in image_dict.items(): image_path = filepath.with_suffix(Path(image_url).suffix) + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path): + logger.info(f"已存在图片文件:{image_path}") + continue # 下载图片 content = __download_image(image_url) # 保存图片文件到当前目录 - __save_file(_fileitem=fileitem, _path=image_path, _content=content) + __save_file(_fileitem=parent, _path=image_path, _content=content) else: # 当前为目录,处理目录内的文件 @@ -493,12 +509,18 @@ class MediaChain(ChainBase, metaclass=Singleton): return # 写入nfo到根目录 nfo_path = filepath / "season.nfo" + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path): + logger.info(f"已存在nfo文件:{nfo_path}") + return __save_file(_fileitem=fileitem, _path=nfo_path, _content=season_nfo) # TMDB季poster图片 image_dict = self.metadata_img(mediainfo=mediainfo, season=season_meta.begin_season) if image_dict: for image_name, image_url in image_dict.items(): image_path = filepath.with_name(image_name) + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path): + logger.info(f"已存在图片文件:{image_path}") + continue # 下载图片 content = __download_image(image_url) # 保存图片文件到当前目录 @@ -511,12 +533,18 @@ class MediaChain(ChainBase, metaclass=Singleton): return # 写入tvshow nfo到根目录 nfo_path = filepath / "tvshow.nfo" + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path): + logger.info(f"已存在nfo文件:{nfo_path}") + return __save_file(_fileitem=fileitem, _path=nfo_path, _content=tv_nfo) # 生成目录图片 image_dict = self.metadata_img(mediainfo=mediainfo) if image_dict: for image_name, image_url in image_dict.items(): image_path = filepath / image_name + if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path): + logger.info(f"已存在图片文件:{image_path}") + continue # 下载图片 content = __download_image(image_url) # 保存图片文件到当前目录 From 666f9a536d181ad44758faf8b518dd4fda6e21d4 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Mon, 30 Sep 2024 13:33:06 +0800 Subject: [PATCH 9/9] fix subscribe api --- app/api/endpoints/subscribe.py | 2 +- app/chain/subscribe.py | 74 ++++++++++++++--------------- app/modules/filemanager/__init__.py | 3 -- app/schemas/subscribe.py | 56 +++++++++++----------- 4 files changed, 65 insertions(+), 70 deletions(-) diff --git a/app/api/endpoints/subscribe.py b/app/api/endpoints/subscribe.py index 64a299d0..262793bf 100644 --- a/app/api/endpoints/subscribe.py +++ b/app/api/endpoints/subscribe.py @@ -398,7 +398,7 @@ def user_subscribes( return Subscribe.list_by_username(db, username) -@router.get("/files/{subscribe_id}", summary="订阅相关文件信息", response_model=List[schemas.SubscrbieInfo]) +@router.get("/files/{subscribe_id}", summary="订阅相关文件信息", response_model=schemas.SubscrbieInfo) def subscribe_files( subscribe_id: int, db: Session = Depends(get_db), diff --git a/app/chain/subscribe.py b/app/chain/subscribe.py index f2578bd6..561f45b5 100644 --- a/app/chain/subscribe.py +++ b/app/chain/subscribe.py @@ -25,7 +25,8 @@ from app.helper.message import MessageHelper from app.helper.subscribe import SubscribeHelper from app.helper.torrent import TorrentHelper from app.log import logger -from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo +from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \ + SubscribeLibraryFileInfo from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType @@ -1167,47 +1168,34 @@ class SubscribeChain(ChainBase): return # 返回订阅数据 - subscribe_info = SubscrbieInfo( - id=subscribe.id, - name=subscribe.name, - year=subscribe.year, - type=subscribe.type, - tmdbid=subscribe.tmdbid, - doubanid=subscribe.doubanid, - season=subscribe.season, - poster=subscribe.poster, - backdrop=subscribe.backdrop, - vote=subscribe.vote, - description=subscribe.description, - episodes_info={} - ) + subscribe_info = SubscrbieInfo() # 所有集的数据 - episodes_info = {} + episodes: Dict[int, SubscribeEpisodeInfo] = {} if subscribe.tmdbid and subscribe.type == MediaType.TV.value: # 查询TMDB中的集信息 tmdb_episodes = self.tmdbchain.tmdb_episodes( - tmdbid=subscribe.tmdb_id, + tmdbid=subscribe.tmdbid, season=subscribe.season ) if tmdb_episodes: for episode in tmdb_episodes: - episode_info = SubscribeEpisodeInfo() - episodes_info.title = episode.name - episodes_info.description = episode.overview - episodes_info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}" - episodes_info[episode.episode_number] = episode_info + info = SubscribeEpisodeInfo() + info.title = episode.name + info.description = episode.overview + info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}" + episodes[episode.episode_number] = info elif subscribe.type == MediaType.TV.value: # 根据开始结束集计算集信息 for i in range(subscribe.start_episode or 1, subscribe.total_episode + 1): - episode_info = SubscribeEpisodeInfo() - episode_info.title = f'第 {i} 集' - episodes_info[i] = episode_info + info = SubscribeEpisodeInfo() + info.title = f'第 {i} 集' + episodes[i] = info else: # 电影 - episode_info = SubscribeEpisodeInfo() - episode_info.title = subscribe.name - episodes_info[0] = episode_info + info = SubscribeEpisodeInfo() + info.title = subscribe.name + episodes[0] = info # 所有下载记录 download_his = self.downloadhis.get_by_mediaid(tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid) @@ -1221,14 +1209,20 @@ class SubscribeChain(ChainBase): for file in files: # 识别文件名 file_meta = MetaInfo(file.filepath) + # 下载文件信息 + file_info = SubscribeDownloadFileInfo( + torrent_title=his.torrent_name, + site_name=his.torrent_site, + downloader=file.downloader, + hash=his.download_hash, + file_path=file.fullpath, + ) if subscribe.type == MediaType.TV.value: episode_number = file_meta.begin_episode - if episode_number and episodes_info.get(episode_number): - episodes_info[episode_number].download_file = file.fullpath - episodes_info[episode_number].torrent = torrent_url + if episode_number and episodes.get(episode_number): + episodes[episode_number].download.append(file_info) else: - episodes_info[0].download_file = file.fullpath - episodes_info[0].torrent = torrent_url + episodes[0].download.append(file_info) # 生成元数据 meta = MetaInfo(subscribe.name) @@ -1255,13 +1249,19 @@ class SubscribeChain(ChainBase): for fileitem in library_fileitems: # 识别文件名 file_meta = MetaInfo(fileitem.path) + # 媒体库文件信息 + file_info = SubscribeLibraryFileInfo( + storage=fileitem.storage, + file_path=fileitem.path, + ) if subscribe.type == MediaType.TV.value: episode_number = file_meta.begin_episode - if episode_number and episodes_info.get(episode_number): - episodes_info[episode_number].library_file = fileitem.path + if episode_number and episodes.get(episode_number): + episodes[episode_number].library.append(file_info) else: - episodes_info[0].library_file = fileitem.path + episodes[0].library.append(file_info) # 更新订阅信息 - subscribe_info.episodes_info = episodes_info + subscribe_info.subscribe = Subscribe(**subscribe.to_dict()) + subscribe_info.episodes = episodes return subscribe_info diff --git a/app/modules/filemanager/__init__.py b/app/modules/filemanager/__init__.py index 1271ddf6..3a271a06 100644 --- a/app/modules/filemanager/__init__.py +++ b/app/modules/filemanager/__init__.py @@ -1093,9 +1093,6 @@ class FileManagerModule(_ModuleBase): media_path = dir_path / rel_path.parts[0] else: continue - # 检查媒体文件夹是否存在 - if not media_path.exists(): - continue # 检索媒体文件 fileitem = storage_oper.get_item(media_path) if not fileitem: diff --git a/app/schemas/subscribe.py b/app/schemas/subscribe.py index e437228b..3cb8feda 100644 --- a/app/schemas/subscribe.py +++ b/app/schemas/subscribe.py @@ -81,43 +81,41 @@ class Subscribe(BaseModel): orm_mode = True +class SubscribeDownloadFileInfo(BaseModel): + # 种子名称 + torrent_title: Optional[str] = None + # 站点名称 + site_name: Optional[str] = None + # 下载器 + downloader: Optional[str] = None + # hash + hash: Optional[str] = None + # 文件路径 + file_path: Optional[str] = None + + +class SubscribeLibraryFileInfo(BaseModel): + # 存储 + storage: Optional[str] = "local" + # 文件路径 + file_path: Optional[str] = None + + class SubscribeEpisodeInfo(BaseModel): - # 种子地址 - torrent: Optional[str] = None - # 下载文件路程 - download_file: Optional[str] = None - # 媒体库文件路径 - library_file: Optional[str] = None # 标题 title: Optional[str] = None # 描述 description: Optional[str] = None # 背景图 backdrop: Optional[str] = None + # 下载文件信息 + download: Optional[List[SubscribeDownloadFileInfo]] = [] + # 媒体库文件信息 + library: Optional[List[SubscribeLibraryFileInfo]] = [] class SubscrbieInfo(BaseModel): - # 订阅ID - id: Optional[int] = None - # 订阅名称 - name: Optional[str] = None - # 订阅年份 - year: Optional[str] = None - # 订阅类型 电影/电视剧 - type: Optional[str] = None - # 媒体ID - tmdbid: Optional[int] = None - doubanid: Optional[str] = None - bangumiid: Optional[int] = None - # 季号 - season: Optional[int] = None - # 海报 - poster: Optional[str] = None - # 背景图 - backdrop: Optional[str] = None - # 评分 - vote: Optional[int] = 0 - # 描述 - description: Optional[str] = None + # 订阅信息 + subscribe: Optional[Subscribe] = None # 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}} - episodes_info: Optional[Dict[int, SubscribeEpisodeInfo]] = {} + episodes: Optional[Dict[int, SubscribeEpisodeInfo]] = {}