From dfc58720876ac8a278abeeea480de6bd19af7e4a Mon Sep 17 00:00:00 2001 From: jxxghp Date: Tue, 2 Jul 2024 10:03:56 +0800 Subject: [PATCH] fix mediaservers --- app/chain/mediaserver.py | 32 +++--- app/core/config.py | 64 +++++------ app/helper/mediaserver.py | 13 ++- app/modules/emby/__init__.py | 189 +++++++++++++++++++------------ app/modules/jellyfin/__init__.py | 185 ++++++++++++++++++------------ app/modules/plex/__init__.py | 180 +++++++++++++++++------------ app/schemas/mediaserver.py | 12 ++ 7 files changed, 408 insertions(+), 267 deletions(-) diff --git a/app/chain/mediaserver.py b/app/chain/mediaserver.py index edabad81..e876249e 100644 --- a/app/chain/mediaserver.py +++ b/app/chain/mediaserver.py @@ -4,8 +4,8 @@ from typing import List, Union, Optional from app import schemas from app.chain import ChainBase -from app.core.config import settings from app.db.mediaserver_oper import MediaServerOper +from app.helper.mediaserver import MediaServerHelper from app.log import logger lock = threading.Lock() @@ -19,8 +19,9 @@ class MediaServerChain(ChainBase): def __init__(self): super().__init__() self.dboper = MediaServerOper() + self.mediaserverhelper = MediaServerHelper() - def librarys(self, server: str = None, username: str = None) -> List[schemas.MediaServerLibrary]: + def librarys(self, server: str, username: str = None) -> List[schemas.MediaServerLibrary]: """ 获取媒体服务器所有媒体库 """ @@ -44,13 +45,13 @@ class MediaServerChain(ChainBase): """ return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id) - def playing(self, count: int = 20, server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: + def playing(self, server: str, count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器正在播放信息 """ return self.run_module("mediaserver_playing", count=count, server=server, username=username) - def latest(self, count: int = 20, server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: + def latest(self, server: str, count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器最新入库条目 """ @@ -67,12 +68,9 @@ class MediaServerChain(ChainBase): 同步媒体库所有数据到本地数据库 """ # 设置的媒体服务器 - if not settings.MEDIASERVER: + mediaservers = self.mediaserverhelper.get_mediaservers() + if not mediaservers: return - # 同步黑名单 - sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split( - ",") if settings.MEDIASERVER_SYNC_BLACKLIST else [] - mediaservers = settings.MEDIASERVER.split(",") with lock: # 汇总统计 total_count = 0 @@ -82,14 +80,16 @@ class MediaServerChain(ChainBase): for mediaserver in mediaservers: if not mediaserver: continue - logger.info(f"开始同步媒体库 {mediaserver} 的数据 ...") - for library in self.librarys(mediaserver): + server_name = mediaserver.name + sync_blacklist = mediaserver.config.get("sync_blacklist") or [] + logger.info(f"开始同步媒体库 {server_name} 的数据 ...") + for library in self.librarys(server_name): # 同步黑名单 跳过 if library.name in sync_blacklist: continue - logger.info(f"正在同步 {mediaserver} 媒体库 {library.name} ...") + logger.info(f"正在同步 {server_name} 媒体库 {library.name} ...") library_count = 0 - for item in self.items(mediaserver, library.id): + for item in self.items(server_name, library.id): if not item: continue if not item.item_id: @@ -102,7 +102,7 @@ class MediaServerChain(ChainBase): item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影" if item_type == "电视剧": # 查询剧集信息 - espisodes_info = self.episodes(mediaserver, item.item_id) or [] + espisodes_info = self.episodes(server_name, item.item_id) or [] for episode in espisodes_info: seasoninfo[episode.season] = episode.episodes # 插入数据 @@ -110,7 +110,7 @@ class MediaServerChain(ChainBase): item_dict['seasoninfo'] = json.dumps(seasoninfo) item_dict['item_type'] = item_type self.dboper.add(**item_dict) - logger.info(f"{mediaserver} 媒体库 {library.name} 同步完成,共同步数量:{library_count}") + logger.info(f"{server_name} 媒体库 {library.name} 同步完成,共同步数量:{library_count}") # 总数累加 total_count += library_count - logger.info("【MediaServer】媒体库数据同步完成,同步数量:%s" % total_count) + logger.info(f"媒体库 {server_name} 数据同步完成,同步数量:%s" % total_count) diff --git a/app/core/config.py b/app/core/config.py index b0e012e6..d1cd966e 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -39,18 +39,16 @@ class Settings(BaseSettings): DEBUG: bool = False # 是否开发模式 DEV: bool = False - # 是否开启插件热加载 - PLUGIN_AUTO_RELOAD: bool = False # 配置文件目录 CONFIG_DIR: Optional[str] = None # 超级管理员 SUPERUSER: str = "admin" # API密钥,需要更换 API_TOKEN: str = "moviepilot" - # 登录页面电影海报,tmdb/bing - WALLPAPER: str = "tmdb" # 网络代理 IP:PORT PROXY_HOST: Optional[str] = None + # 登录页面电影海报,tmdb/bing + WALLPAPER: str = "tmdb" # 媒体搜索来源 themoviedb/douban/bangumi,多个用,分隔 SEARCH_SOURCE: str = "themoviedb,douban,bangumi" # 媒体识别来源 themoviedb/douban @@ -71,6 +69,16 @@ class Settings(BaseSettings): FANART_ENABLE: bool = True # Fanart API Key FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f" + # 元数据识别缓存过期时间(小时) + META_CACHE_EXPIRE: int = 0 + # 电视剧动漫的分类genre_ids + ANIME_GENREIDS = [16] + # 用户认证站点 + AUTH_SITE: str = "" + # 自动检查和更新站点资源包(站点索引、认证等) + AUTO_UPDATE_RESOURCE: bool = True + # 是否启用DOH解析域名 + DOH_ENABLE: bool = True # 支持的后缀格式 RMT_MEDIAEXT: list = ['.mp4', '.mkv', '.ts', '.iso', '.rmvb', '.avi', '.mov', '.mpeg', @@ -79,26 +87,26 @@ class Settings(BaseSettings): '.tp', '.f4v'] # 支持的字幕文件后缀格式 RMT_SUBEXT: list = ['.srt', '.ass', '.ssa', '.sup'] - # 下载器临时文件后缀 - DOWNLOAD_TMPEXT: list = ['.!qB', '.part'] # 支持的音轨文件后缀格式 RMT_AUDIO_TRACK_EXT: list = ['.mka'] - # 索引器 - INDEXER: str = "builtin" + # 下载器临时文件后缀 + DOWNLOAD_TMPEXT: list = ['.!qB', '.part'] # 订阅模式 SUBSCRIBE_MODE: str = "spider" # RSS订阅模式刷新时间间隔(分钟) SUBSCRIBE_RSS_INTERVAL: int = 30 + # 订阅数据共享 + SUBSCRIBE_STATISTIC_SHARE: bool = True # 订阅搜索开关 SUBSCRIBE_SEARCH: bool = False - # 用户认证站点 - AUTH_SITE: str = "" - # 交互搜索自动下载用户ID,使用,分割 - AUTO_DOWNLOAD_USER: Optional[str] = None + # 搜索多个名称 + SEARCH_MULTIPLE_NAME: bool = False # 种子标签 TORRENT_TAG: str = "MOVIEPILOT" # 下载站点字幕 DOWNLOAD_SUBTITLE: bool = True + # 交互搜索自动下载用户ID,使用,分割 + AUTO_DOWNLOAD_USER: Optional[str] = None # CookieCloud是否启动本地服务 COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False # CookieCloud服务器地址 @@ -111,12 +119,8 @@ class Settings(BaseSettings): COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24 # CookieCloud同步黑名单,多个域名,分割 COOKIECLOUD_BLACKLIST: Optional[str] = None - # OCR服务器地址 - OCR_HOST: str = "https://movie-pilot.org" # CookieCloud对应的浏览器UA USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57" - # 电视剧动漫的分类genre_ids - ANIME_GENREIDS = [16] # 电影重命名格式 MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \ "/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}" \ @@ -126,30 +130,22 @@ class Settings(BaseSettings): "/Season {{season}}" \ "/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}" \ "{{fileExt}}" - # 转移时覆盖模式 - OVERWRITE_MODE: str = "size" - # 大内存模式 - BIG_MEMORY_MODE: bool = False + # OCR服务器地址 + OCR_HOST: str = "https://movie-pilot.org" + # 服务器地址,对应 https://github.com/jxxghp/MoviePilot-Server 项目 + MP_SERVER_HOST: str = "https://movie-pilot.org" # 插件市场仓库地址,多个地址使用,分隔,地址以/结尾 PLUGIN_MARKET: str = "https://github.com/jxxghp/MoviePilot-Plugins,https://github.com/thsrite/MoviePilot-Plugins,https://github.com/honue/MoviePilot-Plugins,https://github.com/InfinityPacer/MoviePilot-Plugins" + # 插件安装数据共享 + PLUGIN_STATISTIC_SHARE: bool = True + # 是否开启插件热加载 + PLUGIN_AUTO_RELOAD: bool = False # Github token,提高请求api限流阈值 ghp_**** GITHUB_TOKEN: Optional[str] = None # Github代理服务器,格式:https://mirror.ghproxy.com/ GITHUB_PROXY: Optional[str] = '' - # 自动检查和更新站点资源包(站点索引、认证等) - AUTO_UPDATE_RESOURCE: bool = True - # 元数据识别缓存过期时间(小时) - META_CACHE_EXPIRE: int = 0 - # 是否启用DOH解析域名 - DOH_ENABLE: bool = True - # 搜索多个名称 - SEARCH_MULTIPLE_NAME: bool = False - # 订阅数据共享 - SUBSCRIBE_STATISTIC_SHARE: bool = True - # 插件安装数据共享 - PLUGIN_STATISTIC_SHARE: bool = True - # 服务器地址,对应 https://github.com/jxxghp/MoviePilot-Server 项目 - MP_SERVER_HOST: str = "https://movie-pilot.org" + # 大内存模式 + BIG_MEMORY_MODE: bool = False @validator("SUBSCRIBE_RSS_INTERVAL", "COOKIECLOUD_INTERVAL", diff --git a/app/helper/mediaserver.py b/app/helper/mediaserver.py index 88b475fb..0b881d96 100644 --- a/app/helper/mediaserver.py +++ b/app/helper/mediaserver.py @@ -1,4 +1,7 @@ +from typing import List + from app.db.systemconfig_oper import SystemConfigOper +from app.schemas import MediaServerConf from app.schemas.types import SystemConfigKey @@ -10,11 +13,11 @@ class MediaServerHelper: def __init__(self): self.systemconfig = SystemConfigOper() - def get_mediaservers(self) -> dict: + def get_mediaservers(self) -> List[MediaServerConf]: """ 获取媒体服务器 """ - mediaserver_conf: dict = self.systemconfig.get(SystemConfigKey.MediaServers) - if not mediaserver_conf: - return {} - return mediaserver_conf + mediaserver_confs: List[dict] = self.systemconfig.get(SystemConfigKey.MediaServers) + if not mediaserver_confs: + return [] + return [MediaServerConf(**conf) for conf in mediaserver_confs] diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 02aa100b..c3e4c81b 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -1,7 +1,8 @@ -from typing import Optional, Tuple, Union, Any, List, Generator +from typing import Optional, Tuple, Union, Any, List, Generator, Dict from app import schemas from app.core.context import MediaInfo +from app.helper.mediaserver import MediaServerHelper from app.log import logger from app.modules import _ModuleBase from app.modules.emby.emby import Emby @@ -9,10 +10,27 @@ from app.schemas.types import MediaType class EmbyModule(_ModuleBase): - emby: Emby = None + _servers: Dict[str, Emby] = {} def init_module(self) -> None: - self.emby = Emby() + """ + 初始化模块 + """ + # 读取媒体服务器配置 + self._servers = {} + mediaservers = MediaServerHelper().get_mediaservers() + if not mediaservers: + return + # 读取Emby配置 + for server in mediaservers: + if server.type == "emby": + self._servers[server.name] = Emby(**server.config) + + def get_server(self, name: str) -> Optional[Emby]: + """ + 获取Emby服务器 + """ + return self._servers.get(name) @staticmethod def get_name() -> str: @@ -25,22 +43,27 @@ class EmbyModule(_ModuleBase): """ 测试模块连接性 """ - if self.emby.is_inactive(): - self.emby.reconnect() - if not self.emby.get_user(): - return False, "无法连接Emby,请检查参数配置" + if not self._servers: + return False, "未配置Emby服务器" + for name, server in self._servers.items(): + if server.is_inactive(): + server.reconnect() + if not server.get_user(): + return False, f"无法连接Emby服务器:{name}" return True, "" def init_setting(self) -> Tuple[str, Union[str, bool]]: - return "MEDIASERVER", "emby" + pass def scheduler_job(self) -> None: """ 定时任务,每10分钟调用一次 """ # 定时重连 - if self.emby.is_inactive(): - self.emby.reconnect() + for name, server in self._servers.items(): + if server.is_inactive(): + logger.info(f"Emby服务器 {name} 连接断开,尝试重连 ...") + server.reconnect() def user_authenticate(self, name: str, password: str) -> Optional[str]: """ @@ -50,7 +73,11 @@ class EmbyModule(_ModuleBase): :return: token or None """ # Emby认证 - return self.emby.authenticate(name, password) + for server in self._servers.values(): + result = server.authenticate(name, password) + if result: + return result + return None def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ @@ -60,7 +87,11 @@ class EmbyModule(_ModuleBase): :param args: 请求参数 :return: 字典,解析为消息时需要包含:title、text、image """ - return self.emby.get_webhook_message(form, args) + for server in self._servers.values(): + result = server.get_webhook_message(form, args) + if result: + return result + return None def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: """ @@ -69,86 +100,97 @@ class EmbyModule(_ModuleBase): :param itemid: 媒体服务器ItemID :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} """ - if mediainfo.type == MediaType.MOVIE: - if itemid: - movie = self.emby.get_iteminfo(itemid) - if movie: - logger.info(f"媒体库中已存在:{movie}") + for name, server in self._servers.values(): + if mediainfo.type == MediaType.MOVIE: + if itemid: + movie = server.get_iteminfo(itemid) + if movie: + logger.info(f"媒体库 {name} 中找到了 {movie}") + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server=name, + itemid=movie.item_id + ) + movies = server.get_movies(title=mediainfo.title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id) + if not movies: + logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") + continue + else: + logger.info(f"媒体库 {name} 中找到了 {movies}") return schemas.ExistMediaInfo( type=MediaType.MOVIE, - server="emby", - itemid=movie.item_id + server=name, + itemid=movies[0].item_id ) - movies = self.emby.get_movies(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id) - if not movies: - logger.info(f"{mediainfo.title_year} 在媒体库中不存在") - return None else: - logger.info(f"媒体库中已存在:{movies}") - return schemas.ExistMediaInfo( - type=MediaType.MOVIE, - server="emby", - itemid=movies[0].item_id - ) - else: - itemid, tvs = self.emby.get_tv_episodes(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) - if not tvs: - logger.info(f"{mediainfo.title_year} 在媒体库中不存在") - return None - else: - logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") - return schemas.ExistMediaInfo( - type=MediaType.TV, - seasons=tvs, - server="emby", - itemid=itemid - ) + itemid, tvs = server.get_tv_episodes(title=mediainfo.title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id, + item_id=itemid) + if not tvs: + logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") + continue + else: + logger.info(f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}") + return schemas.ExistMediaInfo( + type=MediaType.TV, + seasons=tvs, + server=name, + itemid=itemid + ) + return None def media_statistic(self) -> List[schemas.Statistic]: """ 媒体数量统计 """ - media_statistic = self.emby.get_medias_count() - media_statistic.user_count = self.emby.get_user_count() - return [media_statistic] + media_statistics = [] + for server in self._servers.values(): + media_statistic = server.get_medias_count() + media_statistic.user_count = server.get_user_count() + if media_statistic: + media_statistics.append(media_statistic) + return media_statistics - def mediaserver_librarys(self, server: str = None, username: str = None) -> Optional[List[schemas.MediaServerLibrary]]: + def mediaserver_librarys(self, server: str, + username: str = None) -> Optional[List[schemas.MediaServerLibrary]]: """ 媒体库列表 """ - if server and server != "emby": - return None - return self.emby.get_librarys(username) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_librarys(username) + return None def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: """ 媒体库项目列表 """ - if server != "emby": - return None - return self.emby.get_items(library_id) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_items(library_id) + return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: """ 媒体库项目详情 """ - if server != "emby": - return None - return self.emby.get_iteminfo(item_id) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_iteminfo(item_id) + return None def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: """ 获取剧集信息 """ - if server != "emby": + server_obj = self.get_server(server) + if not server_obj: return None - _, seasoninfo = self.emby.get_tv_episodes(item_id=item_id) + _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id) if not seasoninfo: return [] return [schemas.MediaServerSeasonInfo( @@ -156,28 +198,31 @@ class EmbyModule(_ModuleBase): episodes=episodes ) for season, episodes in seasoninfo.items()] - def mediaserver_playing(self, count: int = 20, - server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: + def mediaserver_playing(self, server: str, + count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器正在播放信息 """ - if server and server != "emby": + server_obj = self.get_server(server) + if not server_obj: return [] - return self.emby.get_resume(num=count, username=username) + return server_obj.get_resume(num=count, username=username) def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: """ 获取媒体库播放地址 """ - if server != "emby": + server_obj = self.get_server(server) + if not server_obj: return None - return self.emby.get_play_url(item_id) + return server_obj.get_play_url(item_id) - def mediaserver_latest(self, count: int = 20, - server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: + def mediaserver_latest(self, server: str, + count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器最新入库条目 """ - if server and server != "emby": + server_obj = self.get_server(server) + if not server_obj: return [] - return self.emby.get_latest(num=count, username=username) + return server_obj.get_latest(num=count, username=username) diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index 11cd9c27..ba590f3e 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -1,7 +1,8 @@ -from typing import Optional, Tuple, Union, Any, List, Generator +from typing import Optional, Tuple, Union, Any, List, Generator, Dict from app import schemas from app.core.context import MediaInfo +from app.helper.mediaserver import MediaServerHelper from app.log import logger from app.modules import _ModuleBase from app.modules.jellyfin.jellyfin import Jellyfin @@ -9,25 +10,44 @@ from app.schemas.types import MediaType class JellyfinModule(_ModuleBase): - jellyfin: Jellyfin = None + _servers: Dict[str, Jellyfin] = {} def init_module(self) -> None: - self.jellyfin = Jellyfin() + """ + 初始化模块 + """ + # 读取媒体服务器配置 + self._servers = {} + mediaservers = MediaServerHelper().get_mediaservers() + if not mediaservers: + return + # 读取Jelly配置 + for server in mediaservers: + if server.type == "jellyfin": + self._servers[server.name] = Jellyfin(**server.config) + + def get_server(self, name: str) -> Optional[Jellyfin]: + """ + 获取Jellyfin服务器 + """ + return self._servers.get(name) @staticmethod def get_name() -> str: return "Jellyfin" def init_setting(self) -> Tuple[str, Union[str, bool]]: - return "MEDIASERVER", "jellyfin" + pass def scheduler_job(self) -> None: """ 定时任务,每10分钟调用一次 """ # 定时重连 - if self.jellyfin.is_inactive(): - self.jellyfin.reconnect() + for name, server in self._servers.items(): + if server.is_inactive(): + logger.info(f"Jellyfin {name} 服务器连接断开,尝试重连 ...") + server.reconnect() def stop(self): pass @@ -36,10 +56,13 @@ class JellyfinModule(_ModuleBase): """ 测试模块连接性 """ - if self.jellyfin.is_inactive(): - self.jellyfin.reconnect() - if not self.jellyfin.get_user(): - return False, "无法连接Jellyfin,请检查参数配置" + if not self._servers: + return False, "未配置Jellyfin服务器" + for name, server in self._servers.items(): + if server.is_inactive(): + server.reconnect() + if not server.get_user(): + return False, f"无法连接Jellyfin服务器:{name}" return True, "" def user_authenticate(self, name: str, password: str) -> Optional[str]: @@ -50,7 +73,11 @@ class JellyfinModule(_ModuleBase): :return: Token or None """ # Jellyfin认证 - return self.jellyfin.authenticate(name, password) + for server in self._servers.values(): + result = server.authenticate(name, password) + if result: + return result + return None def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ @@ -60,7 +87,11 @@ class JellyfinModule(_ModuleBase): :param args: 请求参数 :return: 字典,解析为消息时需要包含:title、text、image """ - return self.jellyfin.get_webhook_message(body) + for server in self._servers.values(): + result = server.get_webhook_message(body) + if result: + return result + return None def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: """ @@ -69,84 +100,95 @@ class JellyfinModule(_ModuleBase): :param itemid: 媒体服务器ItemID :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} """ - if mediainfo.type == MediaType.MOVIE: - if itemid: - movie = self.jellyfin.get_iteminfo(itemid) - if movie: - logger.info(f"媒体库中已存在:{movie}") + for name, server in self._servers.items(): + if mediainfo.type == MediaType.MOVIE: + if itemid: + movie = server.get_iteminfo(itemid) + if movie: + logger.info(f"媒体库 {name} 中找到了 {movie}") + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server=name, + itemid=movie.item_id + ) + movies = server.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id) + if not movies: + logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") + continue + else: + logger.info(f"媒体库 {name} 中找到了 {movies}") return schemas.ExistMediaInfo( type=MediaType.MOVIE, - server="jellyfin", - itemid=movie.item_id + server=name, + itemid=movies[0].item_id ) - movies = self.jellyfin.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id) - if not movies: - logger.info(f"{mediainfo.title_year} 在媒体库中不存在") - return None else: - logger.info(f"媒体库中已存在:{movies}") - return schemas.ExistMediaInfo( - type=MediaType.MOVIE, - server="jellyfin", - itemid=movies[0].item_id - ) - else: - itemid, tvs = self.jellyfin.get_tv_episodes(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) - if not tvs: - logger.info(f"{mediainfo.title_year} 在媒体库中不存在") - return None - else: - logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") - return schemas.ExistMediaInfo( - type=MediaType.TV, - seasons=tvs, - server="jellyfin", - itemid=itemid - ) + itemid, tvs = server.get_tv_episodes(title=mediainfo.title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id, + item_id=itemid) + if not tvs: + logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") + continue + else: + logger.info(f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}") + return schemas.ExistMediaInfo( + type=MediaType.TV, + seasons=tvs, + server=name, + itemid=itemid + ) + return None def media_statistic(self) -> List[schemas.Statistic]: """ 媒体数量统计 """ - media_statistic = self.jellyfin.get_medias_count() - media_statistic.user_count = self.jellyfin.get_user_count() - return [media_statistic] + media_statistics = [] + for server in self._servers.values(): + media_statistic = server.get_medias_count() + media_statistic.user_count = server.get_user_count() + if media_statistic: + media_statistics.append(media_statistic) + return media_statistics - def mediaserver_librarys(self, server: str = None, username: str = None) -> Optional[List[schemas.MediaServerLibrary]]: + def mediaserver_librarys(self, server: str = None, + username: str = None) -> Optional[List[schemas.MediaServerLibrary]]: """ 媒体库列表 """ - if server and server != "jellyfin": - return None - return self.jellyfin.get_librarys(username) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_librarys(username) + return None def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: """ 媒体库项目列表 """ - if server != "jellyfin": - return None - return self.jellyfin.get_items(library_id) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_items(library_id) + return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: """ 媒体库项目详情 """ - if server != "jellyfin": - return None - return self.jellyfin.get_iteminfo(item_id) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_iteminfo(item_id) + return None def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: """ 获取剧集信息 """ - if server != "jellyfin": + server_obj = self.get_server(server) + if not server_obj: return None - _, seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id) + _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id) if not seasoninfo: return [] return [schemas.MediaServerSeasonInfo( @@ -154,28 +196,31 @@ class JellyfinModule(_ModuleBase): episodes=episodes ) for season, episodes in seasoninfo.items()] - def mediaserver_playing(self, count: int = 20, - server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: + def mediaserver_playing(self, server: str, + count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器正在播放信息 """ - if server and server != "jellyfin": + server_obj = self.get_server(server) + if not server_obj: return [] - return self.jellyfin.get_resume(num=count, username=username) + return server_obj.get_resume(num=count, username=username) def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: """ 获取媒体库播放地址 """ - if server != "jellyfin": + server_obj = self.get_server(server) + if not server_obj: return None - return self.jellyfin.get_play_url(item_id) + return server_obj.get_play_url(item_id) - def mediaserver_latest(self, count: int = 20, - server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: + def mediaserver_latest(self, server: str, + count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器最新入库条目 """ - if server and server != "jellyfin": + server_obj = self.get_server(server) + if not server_obj: return [] - return self.jellyfin.get_latest(num=count, username=username) + return server_obj.get_latest(num=count, username=username) diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index e4b28a2d..8a4a1890 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -1,7 +1,8 @@ -from typing import Optional, Tuple, Union, Any, List, Generator +from typing import Optional, Tuple, Union, Any, List, Generator, Dict from app import schemas from app.core.context import MediaInfo +from app.helper.mediaserver import MediaServerHelper from app.log import logger from app.modules import _ModuleBase from app.modules.plex.plex import Plex @@ -9,15 +10,32 @@ from app.schemas.types import MediaType class PlexModule(_ModuleBase): - plex: Plex = None + _servers: Dict[str, Plex] = {} def init_module(self) -> None: - self.plex = Plex() + """ + 初始化模块 + """ + # 读取媒体服务器配置 + self._servers = {} + mediaservers = MediaServerHelper().get_mediaservers() + if not mediaservers: + return + # 读取Emby配置 + for server in mediaservers: + if server.type == "plex": + self._servers[server.name] = Plex(**server.config) @staticmethod def get_name() -> str: return "Plex" + def get_server(self, name: str) -> Optional[Plex]: + """ + 获取Plex服务器 + """ + return self._servers.get(name) + def stop(self): pass @@ -25,22 +43,27 @@ class PlexModule(_ModuleBase): """ 测试模块连接性 """ - if self.plex.is_inactive(): - self.plex.reconnect() - if not self.plex.get_librarys(): - return False, "无法连接Plex,请检查参数配置" + if not self._servers: + return False, "未配置Plex服务器" + for name, server in self._servers.items(): + if server.is_inactive(): + server.reconnect() + if not server.get_librarys(): + return False, f"无法连接Plex服务器:{name}" return True, "" def init_setting(self) -> Tuple[str, Union[str, bool]]: - return "MEDIASERVER", "plex" + pass def scheduler_job(self) -> None: """ 定时任务,每10分钟调用一次 """ # 定时重连 - if self.plex.is_inactive(): - self.plex.reconnect() + for name, server in self._servers.items(): + if server.is_inactive(): + logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...") + server.reconnect() def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ @@ -50,7 +73,11 @@ class PlexModule(_ModuleBase): :param args: 请求参数 :return: 字典,解析为消息时需要包含:title、text、image """ - return self.plex.get_webhook_message(form) + for server in self._servers.values(): + result = server.get_webhook_message(body) + if result: + return result + return None def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: """ @@ -59,88 +86,98 @@ class PlexModule(_ModuleBase): :param itemid: 媒体服务器ItemID :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} """ - if mediainfo.type == MediaType.MOVIE: - if itemid: - movie = self.plex.get_iteminfo(itemid) - if movie: - logger.info(f"媒体库中已存在:{movie}") + for name, server in self._servers.items(): + if mediainfo.type == MediaType.MOVIE: + if itemid: + movie = server.get_iteminfo(itemid) + if movie: + logger.info(f"媒体库 {name} 中找到了 {movie}") + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server=name, + itemid=movie.item_id + ) + movies = server.get_movies(title=mediainfo.title, + original_title=mediainfo.original_title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id) + if not movies: + logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") + continue + else: + logger.info(f"媒体库 {name} 中找到了 {movies}") return schemas.ExistMediaInfo( type=MediaType.MOVIE, - server="plex", - itemid=movie.item_id + server=name, + itemid=movies[0].item_id ) - movies = self.plex.get_movies(title=mediainfo.title, - original_title=mediainfo.original_title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id) - if not movies: - logger.info(f"{mediainfo.title_year} 在媒体库中不存在") - return None else: - logger.info(f"媒体库中已存在:{movies}") - return schemas.ExistMediaInfo( - type=MediaType.MOVIE, - server="plex", - itemid=movies[0].item_id - ) - else: - item_id, tvs = self.plex.get_tv_episodes(title=mediainfo.title, - original_title=mediainfo.original_title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) - if not tvs: - logger.info(f"{mediainfo.title_year} 在媒体库中不存在") - return None - else: - logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") - return schemas.ExistMediaInfo( - type=MediaType.TV, - seasons=tvs, - server="plex", - itemid=item_id - ) + item_id, tvs = server.get_tv_episodes(title=mediainfo.title, + original_title=mediainfo.original_title, + year=mediainfo.year, + tmdb_id=mediainfo.tmdb_id, + item_id=itemid) + if not tvs: + logger.info(f"{mediainfo.title_year} 没有在媒体库 {name} 中") + continue + else: + logger.info(f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}") + return schemas.ExistMediaInfo( + type=MediaType.TV, + seasons=tvs, + server=name, + itemid=item_id + ) + return None def media_statistic(self) -> List[schemas.Statistic]: """ 媒体数量统计 """ - media_statistic = self.plex.get_medias_count() - media_statistic.user_count = 1 - return [media_statistic] + media_statistics = [] + for server in self._servers.values(): + media_statistic = server.get_medias_count() + media_statistic.user_count = 1 + if media_statistic: + media_statistics.append(media_statistic) + return media_statistics def mediaserver_librarys(self, server: str = None, **kwargs) -> Optional[List[schemas.MediaServerLibrary]]: """ 媒体库列表 """ - if server and server != "plex": - return None - return self.plex.get_librarys() + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_librarys() + return None def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: """ 媒体库项目列表 """ - if server != "plex": - return None - return self.plex.get_items(library_id) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_items(library_id) + return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: """ 媒体库项目详情 """ - if server != "plex": - return None - return self.plex.get_iteminfo(item_id) + server_obj = self.get_server(server) + if server_obj: + return server_obj.get_iteminfo(item_id) + return None def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: """ 获取剧集信息 """ - if server != "plex": + server_obj = self.get_server(server) + if not server_obj: return None - _, seasoninfo = self.plex.get_tv_episodes(item_id=item_id) + _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id) if not seasoninfo: return [] return [schemas.MediaServerSeasonInfo( @@ -148,26 +185,29 @@ class PlexModule(_ModuleBase): episodes=episodes ) for season, episodes in seasoninfo.items()] - def mediaserver_playing(self, count: int = 20, server: str = None, **kwargs) -> List[schemas.MediaServerPlayItem]: + def mediaserver_playing(self, server: str, count: int = 20, **kwargs) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器正在播放信息 """ - if server and server != "plex": + server_obj = self.get_server(server) + if not server_obj: return [] - return self.plex.get_resume(count) + return server_obj.get_resume(num=count) - def mediaserver_latest(self, count: int = 20, server: str = None, **kwargs) -> List[schemas.MediaServerPlayItem]: + def mediaserver_latest(self, server: str, count: int = 20, **kwargs) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器最新入库条目 """ - if server and server != "plex": + server_obj = self.get_server(server) + if not server_obj: return [] - return self.plex.get_latest(count) + return server_obj.get_latest(num=count) def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: """ 获取媒体库播放地址 """ - if server != "plex": + server_obj = self.get_server(server) + if not server_obj: return None - return self.plex.get_play_url(item_id) + return server_obj.get_play_url(item_id) diff --git a/app/schemas/mediaserver.py b/app/schemas/mediaserver.py index 582ad2d7..eadad97f 100644 --- a/app/schemas/mediaserver.py +++ b/app/schemas/mediaserver.py @@ -156,3 +156,15 @@ class MediaServerPlayItem(BaseModel): image: Optional[str] = None link: Optional[str] = None percent: Optional[float] = None + + +class MediaServerConf(BaseModel): + """ + 媒体服务器配置 + """ + # 名称 + name: Optional[str] = None + # 类型 emby/jellyfin/plex + type: Optional[str] = None + # 配置 + config: Optional[dict] = {}