diff --git a/app/chain/mediaserver.py b/app/chain/mediaserver.py index 37503a1a..a252f162 100644 --- a/app/chain/mediaserver.py +++ b/app/chain/mediaserver.py @@ -28,12 +28,18 @@ class MediaServerChain(ChainBase): """ return self.run_module("mediaserver_librarys", server=server) - def items(self, server: str, library_id: Union[str, int]) -> Generator: + def items(self, server: str, library_id: Union[str, int]) -> Generator[schemas.MediaServerItem]: """ 获取媒体服务器所有项目 """ return self.run_module("mediaserver_items", server=server, library_id=library_id) + def iteminfo(self, server: str, item_id: Union[str, int]) -> schemas.MediaServerItem: + """ + 获取媒体服务器项目信息 + """ + return self.run_module("mediaserver_iteminfo", server=server, item_id=item_id) + def episodes(self, server: str, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]: """ 获取媒体服务器剧集信息 diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 1d160ef1..932d6d68 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -6,7 +6,6 @@ from app.core.context import MediaInfo from app.log import logger from app.modules import _ModuleBase from app.modules.emby.emby import Emby -from app.schemas import ExistMediaInfo, RefreshMediaItem, WebhookEventInfo from app.schemas.types import MediaType @@ -40,7 +39,7 @@ class EmbyModule(_ModuleBase): # Emby认证 return self.emby.authenticate(name, password) - def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]: + def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ 解析Webhook报文体 :param body: 请求体 @@ -50,7 +49,7 @@ class EmbyModule(_ModuleBase): """ return self.emby.get_webhook_message(form, args) - def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: + def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: """ 判断媒体文件是否存在 :param mediainfo: 识别的媒体信息 @@ -62,25 +61,40 @@ class EmbyModule(_ModuleBase): movie = self.emby.get_iteminfo(itemid) if movie: logger.info(f"媒体库中已存在:{movie}") - return ExistMediaInfo(type=MediaType.MOVIE) - movies = self.emby.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id) + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server="emby", + itemid=movie.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 ExistMediaInfo(type=MediaType.MOVIE) + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server="emby", + itemid=movies[0].item_id + ) else: - tvs = self.emby.get_tv_episodes(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) + 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 ExistMediaInfo(type=MediaType.TV, seasons=tvs) + return schemas.ExistMediaInfo( + type=MediaType.TV, + seasons=tvs, + server="emby", + itemid=itemid + ) def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None: """ @@ -90,7 +104,7 @@ class EmbyModule(_ModuleBase): :return: 成功或失败 """ items = [ - RefreshMediaItem( + schemas.RefreshMediaItem( title=mediainfo.title, year=mediainfo.year, type=mediainfo.type, @@ -105,52 +119,32 @@ class EmbyModule(_ModuleBase): 媒体数量统计 """ media_statistic = self.emby.get_medias_count() - user_count = self.emby.get_user_count() - return [schemas.Statistic( - movie_count=media_statistic.get("MovieCount") or 0, - tv_count=media_statistic.get("SeriesCount") or 0, - episode_count=media_statistic.get("EpisodeCount") or 0, - user_count=user_count or 0 - )] + media_statistic.user_count = self.emby.get_user_count() + return [media_statistic] - def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]: + def mediaserver_librarys(self, server: str) -> List[schemas.MediaServerLibrary]: """ 媒体库列表 """ if server != "emby": return None - librarys = self.emby.get_librarys() - if not librarys: - return [] - return [schemas.MediaServerLibrary( - server="emby", - id=library.get("id"), - name=library.get("name"), - type=library.get("type"), - path=library.get("path") - ) for library in librarys] + return self.emby.get_librarys() - def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: + def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator[schemas.MediaServerItem]]: """ 媒体库项目列表 """ if server != "emby": return None - items = self.emby.get_items(library_id) - for item in items: - yield schemas.MediaServerItem( - server="emby", - library=item.get("library"), - item_id=item.get("id"), - item_type=item.get("type"), - title=item.get("title"), - original_title=item.get("original_title"), - year=item.get("year"), - tmdbid=int(item.get("tmdbid")) if item.get("tmdbid") else None, - imdbid=item.get("imdbid"), - tvdbid=item.get("tvdbid"), - path=item.get("path"), - ) + return self.emby.get_items(library_id) + + def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[Generator[schemas.MediaServerItem]]: + """ + 媒体库项目详情 + """ + if server != "emby": + return None + return self.emby.get_iteminfo(item_id) def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: @@ -159,7 +153,7 @@ class EmbyModule(_ModuleBase): """ if server != "emby": return None - seasoninfo = self.emby.get_tv_episodes(item_id=item_id) + _, seasoninfo = self.emby.get_tv_episodes(item_id=item_id) if not seasoninfo: return [] return [schemas.MediaServerSeasonInfo( diff --git a/app/modules/emby/emby.py b/app/modules/emby/emby.py index f6b1cd7f..a120d74b 100644 --- a/app/modules/emby/emby.py +++ b/app/modules/emby/emby.py @@ -1,17 +1,16 @@ import json import re from pathlib import Path -from typing import List, Optional, Union, Dict, Generator +from typing import List, Optional, Union, Dict, Generator, Tuple from requests import Response +from app import schemas from app.core.config import settings from app.log import logger -from app.schemas import RefreshMediaItem, WebhookEventInfo from app.schemas.types import MediaType from app.utils.http import RequestUtils from app.utils.singleton import Singleton -from app.utils.string import StringUtils class Emby(metaclass=Singleton): @@ -78,7 +77,7 @@ class Emby(metaclass=Singleton): logger.error(f"连接User/Views 出错:" + str(e)) return [] - def get_librarys(self): + def get_librarys(self) -> List[schemas.MediaServerLibrary]: """ 获取媒体服务器所有媒体库列表 """ @@ -93,12 +92,15 @@ class Emby(metaclass=Singleton): library_type = MediaType.TV.value case _: continue - libraries.append({ - "id": library.get("Id"), - "name": library.get("Name"), - "path": library.get("Path"), - "type": library_type - }) + libraries.append( + schemas.MediaServerLibrary( + server="emby", + id=library.get("Id"), + name=library.get("Name"), + path=library.get("Path"), + type=library_type + ) + ) return libraries def get_user(self, user_name: str = None) -> Optional[Union[str, int]]: @@ -200,59 +202,29 @@ class Emby(metaclass=Singleton): logger.error(f"连接Users/Query出错:" + str(e)) return 0 - def get_activity_log(self, num: int = 30) -> List[dict]: - """ - 获取Emby活动记录 - """ - if not self._host or not self._apikey: - return [] - req_url = "%semby/System/ActivityLog/Entries?api_key=%s&" % (self._host, self._apikey) - ret_array = [] - try: - res = RequestUtils().get_res(req_url) - if res: - ret_json = res.json() - items = ret_json.get('Items') - for item in items: - if item.get("Type") == "AuthenticationSucceeded": - event_type = "LG" - event_date = StringUtils.get_time(item.get("Date")) - event_str = "%s, %s" % (item.get("Name"), item.get("ShortOverview")) - activity = {"type": event_type, "event": event_str, "date": event_date} - ret_array.append(activity) - if item.get("Type") in ["VideoPlayback", "VideoPlaybackStopped"]: - event_type = "PL" - event_date = StringUtils.get_time(item.get("Date")) - event_str = item.get("Name") - activity = {"type": event_type, "event": event_str, "date": event_date} - ret_array.append(activity) - else: - logger.error(f"System/ActivityLog/Entries 未获取到返回数据") - return [] - except Exception as e: - - logger.error(f"连接System/ActivityLog/Entries出错:" + str(e)) - return [] - return ret_array[:num] - - def get_medias_count(self) -> dict: + def get_medias_count(self) -> schemas.Statistic: """ 获得电影、电视剧、动漫媒体数量 :return: MovieCount SeriesCount SongCount """ if not self._host or not self._apikey: - return {} + return schemas.Statistic() req_url = "%semby/Items/Counts?api_key=%s" % (self._host, self._apikey) try: res = RequestUtils().get_res(req_url) if res: - return res.json() + result = res.json() + return schemas.Statistic( + movie_count=result.get("MovieCount") or 0, + tv_count=result.get("SeriesCount") or 0, + episode_count=result.get("EpisodeCount") or 0 + ) else: logger.error(f"Items/Counts 未获取到返回数据") - return {} + return schemas.Statistic() except Exception as e: logger.error(f"连接Items/Counts出错:" + str(e)) - return {} + return schemas.Statistic() def __get_emby_series_id_by_name(self, name: str, year: str) -> Optional[str]: """ @@ -282,7 +254,7 @@ class Emby(metaclass=Singleton): def get_movies(self, title: str, year: str = None, - tmdb_id: int = None) -> Optional[List[dict]]: + tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]: """ 根据标题和年份,检查电影是否在Emby中存在,存在则返回列表 :param title: 标题 @@ -303,17 +275,28 @@ class Emby(metaclass=Singleton): ret_movies = [] for res_item in res_items: item_tmdbid = res_item.get("ProviderIds", {}).get("Tmdb") + mediaserver_item = schemas.MediaServerItem( + server="emby", + library=res_item.get("ParentId"), + item_id=res_item.get("Id"), + item_type=res_item.get("Type"), + title=res_item.get("Name"), + original_title=res_item.get("OriginalTitle"), + year=res_item.get("ProductionYear"), + tmdbid=int(item_tmdbid) if item_tmdbid else None, + imdbid=res_item.get("ProviderIds", {}).get("Imdb"), + tvdbid=res_item.get("ProviderIds", {}).get("Tvdb"), + path=res_item.get("Path") + ) if tmdb_id and item_tmdbid: if str(item_tmdbid) != str(tmdb_id): continue else: - ret_movies.append( - {'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))}) + ret_movies.append(mediaserver_item) continue - if res_item.get('Name') == title and ( - not year or str(res_item.get('ProductionYear')) == str(year)): - ret_movies.append( - {'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))}) + if (mediaserver_item.title == title + and (not year or str(mediaserver_item.year) == str(year))): + ret_movies.append(mediaserver_item) return ret_movies except Exception as e: logger.error(f"连接Items出错:" + str(e)) @@ -325,7 +308,8 @@ class Emby(metaclass=Singleton): title: str = None, year: str = None, tmdb_id: int = None, - season: int = None) -> Optional[Dict[int, list]]: + season: int = None + ) -> Tuple[Optional[str], Optional[Dict[int, List[Dict[int, list]]]]]: """ 根据标题和年份和季,返回Emby中的剧集列表 :param item_id: Emby中的ID @@ -336,21 +320,20 @@ class Emby(metaclass=Singleton): :return: 每一季的已有集数 """ if not self._host or not self._apikey: - return None + return None, None # 电视剧 if not item_id: item_id = self.__get_emby_series_id_by_name(title, year) if item_id is None: - return None + return None, None if not item_id: - return {} + return None, {} # 验证tmdbid是否相同 item_info = self.get_iteminfo(item_id) if item_info: - item_tmdbid = (item_info.get("ProviderIds") or {}).get("Tmdb") - if tmdb_id and item_tmdbid: - if str(tmdb_id) != str(item_tmdbid): - return {} + if tmdb_id and item_info.tmdbid: + if str(tmdb_id) != str(item_info.tmdbid): + return None, {} # /Shows/Id/Episodes 查集的信息 if not season: season = "" @@ -359,7 +342,8 @@ class Emby(metaclass=Singleton): self._host, item_id, season, self._apikey) res_json = RequestUtils().get_res(req_url) if res_json: - res_items = res_json.json().get("Items") + tv_item = res_json.json() + res_items = tv_item.get("Items") season_episodes = {} for res_item in res_items: season_index = res_item.get("ParentIndexNumber") @@ -374,11 +358,11 @@ class Emby(metaclass=Singleton): season_episodes[season_index] = [] season_episodes[season_index].append(episode_index) # 返回 - return season_episodes + return tv_item.get("Id"), season_episodes except Exception as e: logger.error(f"连接Shows/Id/Episodes出错:" + str(e)) - return None - return {} + return None, None + return None, {} def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]: """ @@ -441,7 +425,7 @@ class Emby(metaclass=Singleton): return False return False - def refresh_library_by_items(self, items: List[RefreshMediaItem]) -> bool: + def refresh_library_by_items(self, items: List[schemas.RefreshMediaItem]) -> bool: """ 按类型、名称、年份来刷新媒体库 :param items: 已识别的需要刷新媒体库的媒体信息列表 @@ -463,7 +447,7 @@ class Emby(metaclass=Singleton): return self.__refresh_emby_library_by_id(library_id) logger.info(f"Emby媒体库刷新完成") - def __get_emby_library_id_by_item(self, item: RefreshMediaItem) -> Optional[str]: + def __get_emby_library_id_by_item(self, item: schemas.RefreshMediaItem) -> Optional[str]: """ 根据媒体信息查询在哪个媒体库,返回要刷新的位置的ID :param item: {title, year, type, category, target_path} @@ -491,39 +475,53 @@ class Emby(metaclass=Singleton): return folder.get("Id") except Exception as err: print(str(err)) - # 如果找不到,只要路径中有分类目录名就命中 - for subfolder in folder.get("SubFolders"): - if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category, - subfolder.get("Path")): - return folder.get("Id") + # 如果找不到,只要路径中有分类目录名就命中 + for subfolder in folder.get("SubFolders"): + if subfolder.get("Path") and re.search(r"[/\\]%s" % item.category, + subfolder.get("Path")): + return folder.get("Id") # 刷新根目录 return "/" - def get_iteminfo(self, itemid: str) -> dict: + def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: """ 获取单个项目详情 """ if not itemid: - return {} + return None if not self._host or not self._apikey: - return {} + return None req_url = "%semby/Users/%s/Items/%s?api_key=%s" % (self._host, self.user, itemid, self._apikey) try: res = RequestUtils().get_res(req_url) if res and res.status_code == 200: - return res.json() + item = res.json() + tmdbid = item.get("ProviderIds", {}).get("Tmdb") + return schemas.MediaServerItem( + server="emby", + library=item.get("ParentId"), + item_id=item.get("Id"), + item_type=item.get("Type"), + title=item.get("Name"), + original_title=item.get("OriginalTitle"), + year=item.get("ProductionYear"), + tmdbid=int(tmdbid) if tmdbid else None, + imdbid=item.get("ProviderIds", {}).get("Imdb"), + tvdbid=item.get("ProviderIds", {}).get("Tvdb"), + path=item.get("Path") + ) except Exception as e: logger.error(f"连接Items/Id出错:" + str(e)) - return {} + return None - def get_items(self, parent: str) -> Generator: + def get_items(self, parent: str) -> Generator[Optional[schemas.MediaServerItem]]: """ 获取媒体服务器所有媒体库列表 """ if not parent: - yield {} + yield None if not self._host or not self._apikey: - yield {} + yield None req_url = "%semby/Users/%s/Items?ParentId=%s&api_key=%s" % (self._host, self.user, parent, self._apikey) try: res = RequestUtils().get_res(req_url) @@ -533,26 +531,15 @@ class Emby(metaclass=Singleton): if not result: continue if result.get("Type") in ["Movie", "Series"]: - item_info = self.get_iteminfo(result.get("Id")) - yield {"id": result.get("Id"), - "library": item_info.get("ParentId"), - "type": item_info.get("Type"), - "title": item_info.get("Name"), - "original_title": item_info.get("OriginalTitle"), - "year": item_info.get("ProductionYear"), - "tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"), - "imdbid": item_info.get("ProviderIds", {}).get("Imdb"), - "tvdbid": item_info.get("ProviderIds", {}).get("Tvdb"), - "path": item_info.get("Path"), - "json": str(item_info)} + yield self.get_iteminfo(result.get("Id")) elif "Folder" in result.get("Type"): for item in self.get_items(parent=result.get('Id')): yield item except Exception as e: logger.error(f"连接Users/Items出错:" + str(e)) - yield {} + yield None - def get_webhook_message(self, form: any, args: dict) -> Optional[WebhookEventInfo]: + def get_webhook_message(self, form: any, args: dict) -> Optional[schemas.WebhookEventInfo]: """ 解析Emby Webhook报文 电影: @@ -805,7 +792,7 @@ class Emby(metaclass=Singleton): if not eventType: return None logger.info(f"接收到emby webhook:{message}") - eventItem = WebhookEventInfo(event=eventType, channel="emby") + eventItem = schemas.WebhookEventInfo(event=eventType, channel="emby") if message.get('Item'): if message.get('Item', {}).get('Type') == 'Episode': eventItem.item_type = "TV" diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index ff5716b5..41c10241 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -6,7 +6,6 @@ from app.core.context import MediaInfo from app.log import logger from app.modules import _ModuleBase from app.modules.jellyfin.jellyfin import Jellyfin -from app.schemas import ExistMediaInfo, WebhookEventInfo from app.schemas.types import MediaType @@ -40,7 +39,7 @@ class JellyfinModule(_ModuleBase): # Jellyfin认证 return self.jellyfin.authenticate(name, password) - def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]: + def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ 解析Webhook报文体 :param body: 请求体 @@ -50,7 +49,7 @@ class JellyfinModule(_ModuleBase): """ return self.jellyfin.get_webhook_message(body) - def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: + def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: """ 判断媒体文件是否存在 :param mediainfo: 识别的媒体信息 @@ -62,25 +61,38 @@ class JellyfinModule(_ModuleBase): movie = self.jellyfin.get_iteminfo(itemid) if movie: logger.info(f"媒体库中已存在:{movie}") - return ExistMediaInfo(type=MediaType.MOVIE) + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server="jellyfin", + itemid=movie.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 ExistMediaInfo(type=MediaType.MOVIE) + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server="jellyfin", + itemid=movies[0].item_id + ) else: - tvs = self.jellyfin.get_tv_episodes(title=mediainfo.title, - year=mediainfo.year, - tmdb_id=mediainfo.tmdb_id, - item_id=itemid) + 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 ExistMediaInfo(type=MediaType.TV, seasons=tvs) + return schemas.ExistMediaInfo( + type=MediaType.TV, + seasons=tvs, + server="jellyfin", + itemid=itemid + ) def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None: """ @@ -96,13 +108,8 @@ class JellyfinModule(_ModuleBase): 媒体数量统计 """ media_statistic = self.jellyfin.get_medias_count() - user_count = self.jellyfin.get_user_count() - return [schemas.Statistic( - movie_count=media_statistic.get("MovieCount") or 0, - tv_count=media_statistic.get("SeriesCount") or 0, - episode_count=media_statistic.get("EpisodeCount") or 0, - user_count=user_count or 0 - )] + media_statistic.user_count = self.jellyfin.get_user_count() + return media_statistic def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]: """ @@ -121,27 +128,21 @@ class JellyfinModule(_ModuleBase): path=library.get("path") ) for library in librarys] - def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: + def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator[schemas.MediaServerItem]]: """ 媒体库项目列表 """ if server != "jellyfin": return None - items = self.jellyfin.get_items(library_id) - for item in items: - yield schemas.MediaServerItem( - server="jellyfin", - library=item.get("library"), - item_id=item.get("id"), - item_type=item.get("type"), - title=item.get("title"), - original_title=item.get("original_title"), - year=item.get("year"), - tmdbid=item.get("tmdbid"), - imdbid=item.get("imdbid"), - tvdbid=item.get("tvdbid"), - path=item.get("path"), - ) + return self.jellyfin.get_items(library_id) + + def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: + """ + 媒体库项目详情 + """ + if server != "jellyfin": + return None + return self.jellyfin.get_iteminfo(item_id) def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: @@ -150,7 +151,7 @@ class JellyfinModule(_ModuleBase): """ if server != "jellyfin": return None - seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id) + _, seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id) if not seasoninfo: return [] return [schemas.MediaServerSeasonInfo( diff --git a/app/modules/jellyfin/jellyfin.py b/app/modules/jellyfin/jellyfin.py index 21898817..73cbe420 100644 --- a/app/modules/jellyfin/jellyfin.py +++ b/app/modules/jellyfin/jellyfin.py @@ -1,15 +1,14 @@ import json -import re -from typing import List, Union, Optional, Dict, Generator +from typing import List, Union, Optional, Dict, Generator, Tuple from requests import Response +from app import schemas from app.core.config import settings from app.log import logger -from app.schemas import MediaType, WebhookEventInfo +from app.schemas import MediaType from app.utils.http import RequestUtils from app.utils.singleton import Singleton -from app.utils.string import StringUtils class Jellyfin(metaclass=Singleton): @@ -73,12 +72,14 @@ class Jellyfin(metaclass=Singleton): library_type = MediaType.TV.value case _: continue - libraries.append({ - "id": library.get("Id"), - "name": library.get("Name"), - "path": library.get("Path"), - "type": library_type - }) + libraries.append( + schemas.MediaServerLibrary( + server="emby", + id=library.get("Id"), + name=library.get("Name"), + path=library.get("Path"), + type=library_type + )) return libraries def get_user_count(self) -> int: @@ -179,59 +180,29 @@ class Jellyfin(metaclass=Singleton): logger.error(f"连接System/Info出错:" + str(e)) return None - def get_activity_log(self, num: int = 30) -> List[dict]: - """ - 获取Jellyfin活动记录 - """ - if not self._host or not self._apikey: - return [] - req_url = "%sSystem/ActivityLog/Entries?api_key=%s&Limit=%s" % (self._host, self._apikey, num) - ret_array = [] - try: - res = RequestUtils().get_res(req_url) - if res: - ret_json = res.json() - items = ret_json.get('Items') - for item in items: - if item.get("Type") == "SessionStarted": - event_type = "LG" - event_date = re.sub(r'\dZ', 'Z', item.get("Date")) - event_str = "%s, %s" % (item.get("Name"), item.get("ShortOverview")) - activity = {"type": event_type, "event": event_str, - "date": StringUtils.get_time(event_date)} - ret_array.append(activity) - if item.get("Type") in ["VideoPlayback", "VideoPlaybackStopped"]: - event_type = "PL" - event_date = re.sub(r'\dZ', 'Z', item.get("Date")) - activity = {"type": event_type, "event": item.get("Name"), - "date": StringUtils.get_time(event_date)} - ret_array.append(activity) - else: - logger.error(f"System/ActivityLog/Entries 未获取到返回数据") - return [] - except Exception as e: - logger.error(f"连接System/ActivityLog/Entries出错:" + str(e)) - return [] - return ret_array - - def get_medias_count(self) -> Optional[dict]: + def get_medias_count(self) -> schemas.Statistic: """ 获得电影、电视剧、动漫媒体数量 :return: MovieCount SeriesCount SongCount """ if not self._host or not self._apikey: - return None + return schemas.Statistic() req_url = "%sItems/Counts?api_key=%s" % (self._host, self._apikey) try: res = RequestUtils().get_res(req_url) if res: - return res.json() + result = res.json() + schemas.Statistic( + movie_count=result.get("MovieCount") or 0, + tv_count=result.get("SeriesCount") or 0, + episode_count=result.get("EpisodeCount") or 0 + ) else: logger.error(f"Items/Counts 未获取到返回数据") - return {} + return schemas.Statistic() except Exception as e: logger.error(f"连接Items/Counts出错:" + str(e)) - return {} + return schemas.Statistic() def __get_jellyfin_series_id_by_name(self, name: str, year: str) -> Optional[str]: """ @@ -258,7 +229,7 @@ class Jellyfin(metaclass=Singleton): def get_movies(self, title: str, year: str = None, - tmdb_id: int = None) -> Optional[List[dict]]: + tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]: """ 根据标题和年份,检查电影是否在Jellyfin中存在,存在则返回列表 :param title: 标题 @@ -276,19 +247,30 @@ class Jellyfin(metaclass=Singleton): res_items = res.json().get("Items") if res_items: ret_movies = [] - for res_item in res_items: - item_tmdbid = res_item.get("ProviderIds", {}).get("Tmdb") + for item in res_items: + item_tmdbid = item.get("ProviderIds", {}).get("Tmdb") + mediaserver_item = schemas.MediaServerItem( + server="emby", + library=item.get("ParentId"), + item_id=item.get("Id"), + item_type=item.get("Type"), + title=item.get("Name"), + original_title=item.get("OriginalTitle"), + year=item.get("ProductionYear"), + tmdbid=int(item_tmdbid) if item_tmdbid else None, + imdbid=item.get("ProviderIds", {}).get("Imdb"), + tvdbid=item.get("ProviderIds", {}).get("Tvdb"), + path=item.get("Path") + ) if tmdb_id and item_tmdbid: if str(item_tmdbid) != str(tmdb_id): continue else: - ret_movies.append( - {'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))}) + ret_movies.append(mediaserver_item) continue - if res_item.get('Name') == title and ( - not year or str(res_item.get('ProductionYear')) == str(year)): - ret_movies.append( - {'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))}) + if mediaserver_item.title == title and ( + not year or str(mediaserver_item.year) == str(year)): + ret_movies.append(mediaserver_item) return ret_movies except Exception as e: logger.error(f"连接Items出错:" + str(e)) @@ -300,7 +282,7 @@ class Jellyfin(metaclass=Singleton): title: str = None, year: str = None, tmdb_id: int = None, - season: int = None) -> Optional[Dict[int, list]]: + season: int = None) -> Tuple[Optional[str], Optional[Dict[int, list]]]: """ 根据标题和年份和季,返回Jellyfin中的剧集列表 :param item_id: Jellyfin中的Id @@ -311,19 +293,21 @@ class Jellyfin(metaclass=Singleton): :return: 集号的列表 """ if not self._host or not self._apikey or not self.user: - return None + return None, None # 查TVID if not item_id: item_id = self.__get_jellyfin_series_id_by_name(title, year) if item_id is None: - return None + return None, None if not item_id: - return {} + return None, {} # 验证tmdbid是否相同 - item_tmdbid = (self.get_iteminfo(item_id).get("ProviderIds") or {}).get("Tmdb") - if tmdb_id and item_tmdbid: - if str(tmdb_id) != str(item_tmdbid): - return {} + item_info = self.get_iteminfo(item_id) or {} + if item_info: + item_tmdbid = (item_info.get("ProviderIds") or {}).get("Tmdb") + if tmdb_id and item_tmdbid: + if str(tmdb_id) != str(item_tmdbid): + return None, {} if not season: season = "" try: @@ -331,7 +315,8 @@ class Jellyfin(metaclass=Singleton): self._host, item_id, season, self.user, self._apikey) res_json = RequestUtils().get_res(req_url) if res_json: - res_items = res_json.json().get("Items") + tv_info = res_json.json() + res_items = tv_info.get("Items") # 返回的季集信息 season_episodes = {} for res_item in res_items: @@ -346,11 +331,11 @@ class Jellyfin(metaclass=Singleton): if not season_episodes.get(season_index): season_episodes[season_index] = [] season_episodes[season_index].append(episode_index) - return season_episodes + return tv_info.get('Id'), season_episodes except Exception as e: logger.error(f"连接Shows/Id/Episodes出错:" + str(e)) - return None - return {} + return None, None + return None, {} def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]: """ @@ -394,7 +379,7 @@ class Jellyfin(metaclass=Singleton): logger.error(f"连接Library/Refresh出错:" + str(e)) return False - def get_webhook_message(self, body: any) -> Optional[WebhookEventInfo]: + def get_webhook_message(self, body: any) -> Optional[schemas.WebhookEventInfo]: """ 解析Jellyfin报文 { @@ -470,7 +455,7 @@ class Jellyfin(metaclass=Singleton): eventType = message.get('NotificationType') if not eventType: return None - eventItem = WebhookEventInfo( + eventItem = schemas.WebhookEventInfo( event=eventType, channel="jellyfin" ) @@ -506,32 +491,46 @@ class Jellyfin(metaclass=Singleton): return eventItem - def get_iteminfo(self, itemid: str) -> dict: + def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: """ 获取单个项目详情 """ if not itemid: - return {} + return None if not self._host or not self._apikey: - return {} + return None req_url = "%sUsers/%s/Items/%s?api_key=%s" % ( self._host, self.user, itemid, self._apikey) try: res = RequestUtils().get_res(req_url) if res and res.status_code == 200: - return res.json() + item = res.json() + tmdbid = item.get("ProviderIds", {}).get("Tmdb") + return schemas.MediaServerItem( + server="emby", + library=item.get("ParentId"), + item_id=item.get("Id"), + item_type=item.get("Type"), + title=item.get("Name"), + original_title=item.get("OriginalTitle"), + year=item.get("ProductionYear"), + tmdbid=int(tmdbid) if tmdbid else None, + imdbid=item.get("ProviderIds", {}).get("Imdb"), + tvdbid=item.get("ProviderIds", {}).get("Tvdb"), + path=item.get("Path") + ) except Exception as e: logger.error(f"连接Users/Items出错:" + str(e)) - return {} + return None - def get_items(self, parent: str) -> Generator: + def get_items(self, parent: str) -> Generator[schemas.MediaServerItem]: """ 获取媒体服务器所有媒体库列表 """ if not parent: - yield {} + yield None if not self._host or not self._apikey: - yield {} + yield None req_url = "%sUsers/%s/Items?parentId=%s&api_key=%s" % (self._host, self.user, parent, self._apikey) try: res = RequestUtils().get_res(req_url) @@ -541,24 +540,13 @@ class Jellyfin(metaclass=Singleton): if not result: continue if result.get("Type") in ["Movie", "Series"]: - item_info = self.get_iteminfo(result.get("Id")) - yield {"id": result.get("Id"), - "library": item_info.get("ParentId"), - "type": item_info.get("Type"), - "title": item_info.get("Name"), - "original_title": item_info.get("OriginalTitle"), - "year": item_info.get("ProductionYear"), - "tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"), - "imdbid": item_info.get("ProviderIds", {}).get("Imdb"), - "tvdbid": item_info.get("ProviderIds", {}).get("Tvdb"), - "path": item_info.get("Path"), - "json": str(item_info)} + yield self.get_iteminfo(result.get("Id")) elif "Folder" in result.get("Type"): for item in self.get_items(result.get("Id")): yield item except Exception as e: logger.error(f"连接Users/Items出错:" + str(e)) - yield {} + yield None def get_data(self, url: str) -> Optional[Response]: """ diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index 76fe555f..1dabd525 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -6,12 +6,10 @@ from app.core.context import MediaInfo from app.log import logger from app.modules import _ModuleBase from app.modules.plex.plex import Plex -from app.schemas import ExistMediaInfo, RefreshMediaItem, WebhookEventInfo from app.schemas.types import MediaType class PlexModule(_ModuleBase): - plex: Plex = None def init_module(self) -> None: @@ -31,7 +29,7 @@ class PlexModule(_ModuleBase): if not self.plex.is_inactive(): self.plex.reconnect() - def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]: + def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: """ 解析Webhook报文体 :param body: 请求体 @@ -41,7 +39,7 @@ class PlexModule(_ModuleBase): """ return self.plex.get_webhook_message(form) - def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]: + def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: """ 判断媒体文件是否存在 :param mediainfo: 识别的媒体信息 @@ -53,29 +51,42 @@ class PlexModule(_ModuleBase): movie = self.plex.get_iteminfo(itemid) if movie: logger.info(f"媒体库中已存在:{movie}") - return ExistMediaInfo(type=MediaType.MOVIE) + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server="plex", + itemid=movie.item_id + ) movies = self.plex.get_movies(title=mediainfo.title, - original_title=mediainfo.original_title, - year=mediainfo.year, + 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 ExistMediaInfo(type=MediaType.MOVIE) + return schemas.ExistMediaInfo( + type=MediaType.MOVIE, + server="plex", + itemid=movies[0].item_id + ) else: - 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) + 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 ExistMediaInfo(type=MediaType.TV, seasons=tvs) + return schemas.ExistMediaInfo( + type=MediaType.TV, + seasons=tvs, + server="plex", + itemid=item_id + ) def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None: """ @@ -85,7 +96,7 @@ class PlexModule(_ModuleBase): :return: 成功或失败 """ items = [ - RefreshMediaItem( + schemas.RefreshMediaItem( title=mediainfo.title, year=mediainfo.year, type=mediainfo.type, @@ -100,12 +111,8 @@ class PlexModule(_ModuleBase): 媒体数量统计 """ media_statistic = self.plex.get_medias_count() - return [schemas.Statistic( - movie_count=media_statistic.get("MovieCount") or 0, - tv_count=media_statistic.get("SeriesCount") or 0, - episode_count=media_statistic.get("EpisodeCount") or 0, - user_count=1 - )] + media_statistic.user_count = 1 + return [media_statistic] def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]: """ @@ -113,38 +120,23 @@ class PlexModule(_ModuleBase): """ if server != "plex": return None - librarys = self.plex.get_librarys() - if not librarys: - return [] - return [schemas.MediaServerLibrary( - server="plex", - id=library.get("id"), - name=library.get("name"), - type=library.get("type"), - path=library.get("path") - ) for library in librarys] + return self.plex.get_librarys() - def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: + def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator[schemas.MediaServerItem]]: """ 媒体库项目列表 """ if server != "plex": return None - items = self.plex.get_items(library_id) - for item in items: - yield schemas.MediaServerItem( - server="plex", - library=item.get("library"), - item_id=item.get("id"), - item_type=item.get("type"), - title=item.get("title"), - original_title=item.get("original_title"), - year=item.get("year"), - tmdbid=item.get("tmdbid"), - imdbid=item.get("imdbid"), - tvdbid=item.get("tvdbid"), - path=item.get("path"), - ) + return self.plex.get_items(library_id) + + def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: + """ + 媒体库项目详情 + """ + if server != "plex": + return None + return self.plex.get_iteminfo(item_id) def mediaserver_tv_episodes(self, server: str, item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: @@ -153,7 +145,7 @@ class PlexModule(_ModuleBase): """ if server != "plex": return None - seasoninfo = self.plex.get_tv_episodes(item_id=item_id) + _, seasoninfo = self.plex.get_tv_episodes(item_id=item_id) if not seasoninfo: return [] return [schemas.MediaServerSeasonInfo( diff --git a/app/modules/plex/plex.py b/app/modules/plex/plex.py index 4b1217fd..9775dd3e 100644 --- a/app/modules/plex/plex.py +++ b/app/modules/plex/plex.py @@ -6,9 +6,10 @@ from urllib.parse import quote_plus from plexapi import media from plexapi.server import PlexServer +from app import schemas from app.core.config import settings from app.log import logger -from app.schemas import RefreshMediaItem, MediaType, WebhookEventInfo +from app.schemas import MediaType from app.utils.singleton import Singleton @@ -49,7 +50,7 @@ class Plex(metaclass=Singleton): self._plex = None logger.error(f"Plex服务器连接失败:{str(e)}") - def get_librarys(self): + def get_librarys(self) -> List[schemas.MediaServerLibrary]: """ 获取媒体服务器所有媒体库列表 """ @@ -69,81 +70,42 @@ class Plex(metaclass=Singleton): library_type = MediaType.TV.value case _: continue - libraries.append({ - "id": library.key, - "name": library.title, - "path": library.locations, - "type": library_type - }) + libraries.append( + schemas.MediaServerLibrary( + id=library.key, + name=library.title, + path=library.locations, + type=library_type + ) + ) return libraries - def get_activity_log(self, num: int = 30) -> Optional[List[dict]]: - """ - 获取Plex活动记录 - """ - if not self._plex: - return [] - ret_array = [] - try: - # type的含义: 1 电影 4 剧集单集 详见 plexapi/utils.py中SEARCHTYPES的定义 - # 根据最后播放时间倒序获取数据 - historys = self._plex.library.search(sort='lastViewedAt:desc', limit=num, type='1,4') - for his in historys: - # 过滤掉最后播放时间为空的 - if his.lastViewedAt: - if his.type == "episode": - event_title = "%s %s%s %s" % ( - his.grandparentTitle, - "S" + str(his.parentIndex), - "E" + str(his.index), - his.title - ) - event_str = "开始播放剧集 %s" % event_title - else: - event_title = "%s %s" % ( - his.title, "(" + str(his.year) + ")") - event_str = "开始播放电影 %s" % event_title - - event_type = "PL" - event_date = his.lastViewedAt.strftime('%Y-%m-%d %H:%M:%S') - activity = {"type": event_type, "event": event_str, "date": event_date} - ret_array.append(activity) - except Exception as e: - logger.error(f"连接System/ActivityLog/Entries出错:" + str(e)) - return [] - if ret_array: - ret_array = sorted(ret_array, key=lambda x: x['date'], reverse=True) - return ret_array - - def get_medias_count(self) -> dict: + def get_medias_count(self) -> schemas.Statistic: """ 获得电影、电视剧、动漫媒体数量 :return: MovieCount SeriesCount SongCount """ if not self._plex: - return {} + return schemas.Statistic() sections = self._plex.library.sections() - MovieCount = SeriesCount = SongCount = EpisodeCount = 0 + MovieCount = SeriesCount = EpisodeCount = 0 for sec in sections: if sec.type == "movie": MovieCount += sec.totalSize if sec.type == "show": SeriesCount += sec.totalSize EpisodeCount += sec.totalViewSize(libtype='episode') - if sec.type == "artist": - SongCount += sec.totalSize - return { - "MovieCount": MovieCount, - "SeriesCount": SeriesCount, - "SongCount": SongCount, - "EpisodeCount": EpisodeCount - } + return schemas.Statistic( + movie_count=MovieCount, + tv_count=SeriesCount, + episode_count=EpisodeCount + ) - def get_movies(self, - title: str, + def get_movies(self, + title: str, original_title: str = None, year: str = None, - tmdb_id: int = None) -> Optional[List[dict]]: + tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]: """ 根据标题和年份,检查电影是否在Plex中存在,存在则返回列表 :param title: 标题 @@ -163,13 +125,30 @@ class Plex(metaclass=Singleton): else: movies = self._plex.library.search(title=title, libtype="movie") if original_title and str(original_title) != str(title): - movies.extend(self._plex.library.search(title=original_title, year=year, libtype="movie")) - for movie in set(movies): - movie_tmdbid = self.__get_ids(movie.guids).get("tmdb_id") - if tmdb_id and movie_tmdbid: - if str(movie_tmdbid) != str(tmdb_id): + movies.extend(self._plex.library.search(title=original_title, libtype="movie")) + for item in set(movies): + ids = self.__get_ids(item.guids) + if tmdb_id and ids['tmdb_id']: + if str(ids['tmdb_id']) != str(tmdb_id): continue - ret_movies.append({'title': movie.title, 'year': movie.year}) + path = None + if item.locations: + path = item.locations[0] + ret_movies.append( + schemas.MediaServerItem( + server="plex", + library=item.librarySectionID, + item_id=item.key, + item_type=item.type, + title=item.title, + original_title=item.originalTitle, + year=item.year, + tmdbid=ids['tmdb_id'], + imdbid=ids['imdb_id'], + tvdbid=ids['tvdb_id'], + path=path, + ) + ) return ret_movies def get_tv_episodes(self, @@ -178,7 +157,7 @@ class Plex(metaclass=Singleton): original_title: str = None, year: str = None, tmdb_id: int = None, - season: int = None) -> Optional[Dict[int, list]]: + season: int = None) -> Tuple[Optional[str], Optional[Dict[int, list]]]: """ 根据标题、年份、季查询电视剧所有集信息 :param item_id: 媒体ID @@ -190,7 +169,7 @@ class Plex(metaclass=Singleton): :return: 所有集的列表 """ if not self._plex: - return {} + return None, {} if item_id: videos = self._plex.fetchItem(item_id) else: @@ -199,13 +178,13 @@ class Plex(metaclass=Singleton): if not videos and original_title and str(original_title) != str(title): videos = self._plex.library.search(title=original_title, year=year, libtype="show") if not videos: - return {} + return None, {} if isinstance(videos, list): videos = videos[0] video_tmdbid = self.__get_ids(videos.guids).get('tmdb_id') if tmdb_id and video_tmdbid: if str(video_tmdbid) != str(tmdb_id): - return {} + return None, {} episodes = videos.episodes() season_episodes = {} for episode in episodes: @@ -214,7 +193,7 @@ class Plex(metaclass=Singleton): if episode.seasonNumber not in season_episodes: season_episodes[episode.seasonNumber] = [] season_episodes[episode.seasonNumber].append(episode.index) - return season_episodes + return videos.key, season_episodes def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]: """ @@ -245,7 +224,7 @@ class Plex(metaclass=Singleton): return False return self._plex.library.update() - def refresh_library_by_items(self, items: List[RefreshMediaItem]) -> bool: + def refresh_library_by_items(self, items: List[schemas.RefreshMediaItem]) -> bool: """ 按路径刷新媒体库 item: target_path """ @@ -294,19 +273,34 @@ class Plex(metaclass=Singleton): logger.error(f"查找媒体库出错:{err}") return "", "" - def get_iteminfo(self, itemid: str) -> dict: + def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: """ 获取单个项目详情 """ if not self._plex: - return {} + return None try: item = self._plex.fetchItem(itemid) ids = self.__get_ids(item.guids) - return {'ProviderIds': {'Tmdb': ids['tmdb_id'], 'Imdb': ids['imdb_id']}} + path = None + if item.locations: + path = item.locations[0] + return schemas.MediaServerItem( + server="plex", + library=item.librarySectionID, + item_id=item.key, + item_type=item.type, + title=item.title, + original_title=item.originalTitle, + year=item.year, + tmdbid=ids['tmdb_id'], + imdbid=ids['imdb_id'], + tvdbid=ids['tvdb_id'], + path=path, + ) except Exception as err: logger.error(f"获取项目详情出错:{err}") - return {} + return None @staticmethod def __get_ids(guids: List[Any]) -> dict: @@ -332,14 +326,14 @@ class Plex(metaclass=Singleton): break return ids - def get_items(self, parent: str) -> Generator: + def get_items(self, parent: str) -> Generator[Optional[schemas.MediaServerItem]]: """ 获取媒体服务器所有媒体库列表 """ if not parent: - yield {} + yield None if not self._plex: - yield {} + yield None try: section = self._plex.library.sectionByID(int(parent)) if section: @@ -350,21 +344,24 @@ class Plex(metaclass=Singleton): path = None if item.locations: path = item.locations[0] - yield {"id": item.key, - "library": item.librarySectionID, - "type": item.type, - "title": item.title, - "original_title": item.originalTitle, - "year": item.year, - "tmdbid": ids['tmdb_id'], - "imdbid": ids['imdb_id'], - "tvdbid": ids['tvdb_id'], - "path": path} + yield schemas.MediaServerItem( + server="plex", + library=item.librarySectionID, + item_id=item.key, + item_type=item.type, + title=item.title, + original_title=item.originalTitle, + year=item.year, + tmdbid=ids['tmdb_id'], + imdbid=ids['imdb_id'], + tvdbid=ids['tvdb_id'], + path=path, + ) except Exception as err: logger.error(f"获取媒体库列表出错:{err}") - yield {} + yield None - def get_webhook_message(self, form: any) -> Optional[WebhookEventInfo]: + def get_webhook_message(self, form: any) -> Optional[schemas.WebhookEventInfo]: """ 解析Plex报文 eventItem 字段的含义 @@ -482,7 +479,7 @@ class Plex(metaclass=Singleton): if not eventType: return None logger.info(f"接收到plex webhook:{message}") - eventItem = WebhookEventInfo(event=eventType, channel="plex") + eventItem = schemas.WebhookEventInfo(event=eventType, channel="plex") if message.get('Metadata'): if message.get('Metadata', {}).get('type') == 'episode': eventItem.item_type = "TV" diff --git a/app/plugins/bestfilmversion/__init__.py b/app/plugins/bestfilmversion/__init__.py index 69063912..41a88f46 100644 --- a/app/plugins/bestfilmversion/__init__.py +++ b/app/plugins/bestfilmversion/__init__.py @@ -420,8 +420,7 @@ class BestFilmVersion(_PluginBase): item_info_resp = Emby().get_iteminfo(itemid=data.get('Id')) else: item_info_resp = self.plex_get_iteminfo(itemid=data.get('Id')) - - logger.info(f'BestFilmVersion插件 item打印 {item_info_resp}') + logger.debug(f'BestFilmVersion插件 item打印 {item_info_resp}') if not item_info_resp: continue @@ -430,41 +429,35 @@ class BestFilmVersion(_PluginBase): continue # 获取tmdb_id - media_info_ids = item_info_resp.get('ExternalUrls') - if not media_info_ids: + tmdb_id = item_info_resp.tmdbid + if not tmdb_id: continue - for media_info_id in media_info_ids: - if 'TheMovieDb' != media_info_id.get('Name'): - continue - tmdb_find_id = str(media_info_id.get('Url')).split('/') - tmdb_find_id.reverse() - tmdb_id = tmdb_find_id[0] - # 识别媒体信息 - mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE) - if not mediainfo: - logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbID:{tmdb_id}') - continue - # 添加订阅 - self.subscribechain.add(mtype=MediaType.MOVIE, - title=mediainfo.title, - year=mediainfo.year, - tmdbid=mediainfo.tmdb_id, - best_version=True, - username="收藏洗版", - exist_ok=True) - # 加入缓存 - caches.append(data.get('Name')) - # 存储历史记录 - if mediainfo.tmdb_id not in [h.get("tmdbid") for h in history]: - history.append({ - "title": mediainfo.title, - "type": mediainfo.type.value, - "year": mediainfo.year, - "poster": mediainfo.get_poster_image(), - "overview": mediainfo.overview, - "tmdbid": mediainfo.tmdb_id, - "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") - }) + # 识别媒体信息 + mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE) + if not mediainfo: + logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbid:{tmdb_id}') + continue + # 添加订阅 + self.subscribechain.add(mtype=MediaType.MOVIE, + title=mediainfo.title, + year=mediainfo.year, + tmdbid=mediainfo.tmdb_id, + best_version=True, + username="收藏洗版", + exist_ok=True) + # 加入缓存 + caches.append(data.get('Name')) + # 存储历史记录 + if mediainfo.tmdb_id not in [h.get("tmdbid") for h in history]: + history.append({ + "title": mediainfo.title, + "type": mediainfo.type.value, + "year": mediainfo.year, + "poster": mediainfo.get_poster_image(), + "overview": mediainfo.overview, + "tmdbid": mediainfo.tmdb_id, + "time": datetime.now().strftime("%Y-%m-%d %H:%M:%S") + }) # 保存历史记录 self.save_data('history', history) # 保存缓存 @@ -634,52 +627,34 @@ class BestFilmVersion(_PluginBase): if not _is_lock: return try: - - mediainfo: Optional[MediaInfo] = None if not data.tmdb_id: info = None - if data.channel == 'jellyfin' and data.save_reason == 'UpdateUserRating' and data.item_favorite: + if (data.channel == 'jellyfin' + and data.save_reason == 'UpdateUserRating' + and data.item_favorite): info = Jellyfin().get_iteminfo(itemid=data.item_id) elif data.channel == 'emby' and data.event == 'item.rate': info = Emby().get_iteminfo(itemid=data.item_id) elif data.channel == 'plex' and data.event == 'item.rate': info = Plex().get_iteminfo(itemid=data.item_id) - logger.info(f'BestFilmVersion/webhook_message_action item打印:{info}') - + logger.debug(f'BestFilmVersion/webhook_message_action item打印:{info}') if not info: return - if info['Type'] not in ['Movie', 'MOV', 'movie']: + if info.item_type not in ['Movie', 'MOV', 'movie']: return - # 获取tmdb_id - media_info_ids = info.get('ExternalUrls') - if not media_info_ids: - return - for media_info_id in media_info_ids: - - if 'TheMovieDb' != media_info_id.get('Name'): - continue - - tmdb_find_id = str(media_info_id.get('Url')).split('/') - tmdb_find_id.reverse() - tmdb_id = tmdb_find_id[0] - - mediainfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE) - if not mediainfo: - logger.warn(f'未识别到媒体信息,标题:{data.item_name},tmdbID:{tmdb_id}') - return + tmdb_id = info.tmdbid else: - if data.channel == 'jellyfin' and (data.save_reason != 'UpdateUserRating' or not data.item_favorite): + tmdb_id = data.tmdb_id + if (data.channel == 'jellyfin' + and (data.save_reason != 'UpdateUserRating' or not data.item_favorite)): return if data.item_type not in ['Movie', 'MOV', 'movie']: return - - mediainfo = self.chain.recognize_media(tmdbid=data.tmdb_id, mtype=MediaType.MOVIE) - if not mediainfo: - logger.warn(f'未识别到媒体信息,标题:{data.item_name},tmdbID:{data.tmdb_id}') - return - + # 识别媒体信息 + mediainfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE) if not mediainfo: + logger.warn(f'未识别到媒体信息,标题:{data.item_name},tmdbID:{tmdb_id}') return # 读取缓存 caches = self._cache_path.read_text().split("\n") if self._cache_path.exists() else [] diff --git a/app/plugins/personmeta/__init__.py b/app/plugins/personmeta/__init__.py index 9320b838..409e85a3 100644 --- a/app/plugins/personmeta/__init__.py +++ b/app/plugins/personmeta/__init__.py @@ -1,19 +1,18 @@ -import os -from pathlib import Path +import threading +import time from typing import Any, List, Dict, Tuple -from requests import RequestException +from apscheduler.schedulers.background import BackgroundScheduler +from apscheduler.triggers.cron import CronTrigger +from app.chain.mediaserver import MediaServerChain from app.chain.tmdb import TmdbChain from app.core.config import settings from app.core.event import eventmanager, Event -from app.helper.nfo import NfoReader from app.log import logger from app.plugins import _PluginBase -from app.schemas import TransferInfo, MediaInfo +from app.schemas import MediaInfo, MediaServerItem from app.schemas.types import EventType, MediaType -from app.utils.common import retry -from app.utils.http import RequestUtils class PersonMeta(_PluginBase): @@ -38,16 +37,64 @@ class PersonMeta(_PluginBase): # 可使用的用户级别 auth_level = 1 + # 退出事件 + _event = threading.Event() + # 私有属性 + _scheduler = None tmdbchain = None + mschain = None _enabled = False - _metadir = "" + _onlyonce = False + _cron = None + _delay = 0 def init_plugin(self, config: dict = None): self.tmdbchain = TmdbChain(self.db) + self.mschain = MediaServerChain(self.db) if config: self._enabled = config.get("enabled") - self._metadir = config.get("metadir") + self._onlyonce = config.get("onlyonce") + self._cron = config.get("cron") + self._delay = config.get("delay") or 0 + + # 停止现有任务 + self.stop_service() + + # 启动服务 + if self._enabled or self._onlyonce: + self._scheduler = BackgroundScheduler(timezone=settings.TZ) + if self._cron: + logger.info(f"演职人员刮削服务启动,周期:{self._cron}") + try: + self._scheduler.add_job(func=self.scrap_library, + trigger=CronTrigger.from_crontab(self._cron), + name="演职人员刮削") + except Exception as e: + logger.error(f"演职人员刮削服务启动失败,错误信息:{str(e)}") + self.systemmessage.put(f"演职人员刮削服务启动失败,错误信息:{str(e)}") + + if self._onlyonce: + # 关闭一次性开关 + self._onlyonce = False + # 保存配置 + self.__update_config() + + if self._scheduler.get_jobs(): + # 启动服务 + self._scheduler.print_jobs() + self._scheduler.start() + + def __update_config(self): + """ + 更新配置 + """ + self.update_config({ + "enabled": self._enabled, + "onlyonce": self._onlyonce, + "cron": self._cron, + "delay": self._delay + }) def get_state(self) -> bool: return self._enabled @@ -85,6 +132,22 @@ class PersonMeta(_PluginBase): } } ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VSwitch', + 'props': { + 'model': 'onlyonce', + 'label': '立即运行一次', + } + } + ] } ] }, @@ -95,14 +158,32 @@ class PersonMeta(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, + 'md': 6 }, 'content': [ { 'component': 'VTextField', 'props': { - 'model': 'metadir', - 'label': '人物元数据目录', - 'placeholder': '/metadata/people' + 'model': 'cron', + 'label': '媒体库扫描周期', + 'placeholder': '5位cron表达式' + } + } + ] + }, + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 6 + }, + 'content': [ + { + 'component': 'VTextField', + 'props': { + 'model': 'delay', + 'label': '入库延迟时间(秒)', + 'placeholder': '30' } } ] @@ -113,7 +194,9 @@ class PersonMeta(_PluginBase): } ], { "enabled": False, - "metadir": "" + "onlyonce": False, + "cron": "", + "delay": 30 } def get_page(self) -> List[dict]: @@ -126,103 +209,98 @@ class PersonMeta(_PluginBase): """ if not self._enabled: return - # 下载人物头像 - if not self._metadir: - logger.warning("人物元数据目录未配置,无法下载人物头像") - return # 事件数据 mediainfo: MediaInfo = event.event_data.get("mediainfo") - transferinfo: TransferInfo = event.event_data.get("transferinfo") - if not mediainfo or not transferinfo: + if not mediainfo: return - # 文件路径 - if not transferinfo.file_list_new: + # 延迟 + if self._delay: + time.sleep(int(self._delay)) + # 查询媒体服务器中的条目 + existsinfo = self.chain.media_exists(mediainfo=mediainfo) + if not existsinfo or not existsinfo.itemid: + logger.warn(f"演职人员刮削 {mediainfo.title_year} 在媒体库中不存在") return - filepath = Path(transferinfo.file_list_new[0]) - # 电影 - if mediainfo.type == MediaType.MOVIE: - # nfo文件 - nfofile = filepath.with_name("movie.nfo") - if not nfofile.exists(): - nfofile = filepath.with_name(f"{filepath.stem}.nfo") - if not nfofile.exists(): - logger.warning(f"演职人员刮削 电影nfo文件不存在:{nfofile}") - return - else: - # nfo文件 - nfofile = filepath.parent.with_name("tvshow.nfo") - if not nfofile.exists(): - logger.warning(f"演职人员刮削 剧集nfo文件不存在:{nfofile}") - return - logger.info(f"演职人员刮削 开始刮削:{filepath}") - # 主要媒体服务器 - mediaserver = str(settings.MEDIASERVER).split(",")[0] - # 读取nfo文件 - nfo = NfoReader(nfofile) - # 读取演员信息 - actors = nfo.get_elements("actor") or [] - for actor in actors: - # 演员ID - actor_id = actor.find("tmdbid").text - if not actor_id: - continue - # 演员名称 - actor_name = actor.find("name").text - if not actor_name: - continue - # 查询演员详情 - actor_info = self.tmdbchain.person_detail(int(actor_id)) - if not actor_info: - continue - # 演员头像 - actor_image = actor_info.get("profile_path") - if not actor_image: - continue - # 计算保存目录 - if mediaserver == 'jellyfin': - pers_path = Path(self._metadir) / f"{actor_name[0]}" / f"{actor_name}" - else: - pers_path = Path(self._metadir) / f"{actor_name}-tmdb-{actor_id}" - # 创建目录 - if not pers_path.exists(): - os.makedirs(pers_path, exist_ok=True) - # 文件路径 - image_path = pers_path / f"folder{Path(actor_image).suffix}" - if image_path.exists(): - continue - # 下载图片 - self.download_image( - image_url=f"https://image.tmdb.org/t/p/original{actor_image}", - path=image_path - ) - # 刷新媒体库 - self.chain.refresh_mediaserver( - mediainfo=mediainfo, - file_path=filepath - ) - logger.info(f"演职人员刮削 刮削完成:{filepath}") + # 初始化媒体服务器 + if existsinfo.server == "plex": + logger.warn(f"演职人员刮削 不支持{existsinfo.server}媒体服务器") + return + # 查询条目详情 + iteminfo = self.mschain.iteminfo(server=existsinfo.server, item_id=existsinfo.itemid) + if not iteminfo: + logger.warn(f"演职人员刮削 {mediainfo.title_year} 条目详情获取失败") + return + # 刮削演职人员信息 + self.__update_item(server=existsinfo.server, item=iteminfo, mediainfo=mediainfo) - @staticmethod - @retry(RequestException, logger=logger) - def download_image(image_url: str, path: Path): + def scrap_library(self): """ - 下载图片,保存到指定路径 + 扫描整个媒体库,刮削演员信息 """ - try: - logger.info(f"正在下载演职人员图片:{image_url} ...") - r = RequestUtils().get_res(url=image_url, raise_exception=True) - if r: - path.write_bytes(r.content) - logger.info(f"图片已保存:{path}") - else: - logger.info(f"图片下载失败,请检查网络连通性:{image_url}") - except RequestException as err: - raise err - except Exception as err: - logger.error(f"图片下载失败:{err}") + # 所有媒体服务器 + if not settings.MEDIASERVER: + return + for server in settings.MEDIASERVER.split(","): + if server == "plex": + logger.warn(f"演职人员刮削 不支持{server}媒体服务器") + continue + # 扫描所有媒体库 + logger.info(f"开始刮削服务器 {server} 的演员信息 ...") + for library in self.mschain.librarys(server): + logger.info(f"开始刮削媒体库 {library.name} 的演员信息 ...") + for item in self.mschain.items(server, library.id): + if not item: + continue + if not item.item_id: + continue + if self._event.is_set(): + logger.info(f"演职人员刮削服务停止") + return + # 处理条目 + logger.info(f"开始刮削 {item.title} 的演员信息 ...") + self.__update_item(server=server, item=item) + logger.info(f"{item.title} 的演员信息刮削完成") + logger.info(f"媒体库 {library.name} 的演员信息刮削完成") + logger.info(f"服务器 {server} 的演员信息刮削完成") + + def __update_item(self, server: str, item: MediaServerItem, mediainfo: MediaInfo = None): + """ + 更新媒体服务器中的条目 + """ + # 识别媒体信息 + if not mediainfo: + if not item.tmdbid: + logger.warn(f"{item.title} 未找到tmdbid,无法识别媒体信息") + return + mtype = MediaType.TV if item.item_type in ['Series', 'show'] else MediaType.MOVIE + mediainfo = self.chain.recognize_media(mtype=mtype, tmdbid=item.tmdbid) + if not mediainfo: + logger.warn(f"{item.title} 未识别到媒体信息") + return + # 搜索豆瓣词条 + + # 搜索豆瓣人物信息 + + # 匹配非中文人名 + + # 更新中文人名 + + # 下载图片 + + # 更新演员图片 + pass def stop_service(self): """ - 退出插件 + 停止服务 """ - pass + try: + if self._scheduler: + self._scheduler.remove_all_jobs() + if self._scheduler.running: + self._event.set() + self._scheduler.shutdown() + self._event.clear() + self._scheduler = None + except Exception as e: + print(str(e)) diff --git a/app/schemas/mediaserver.py b/app/schemas/mediaserver.py index 0f8ff8a4..9da086d5 100644 --- a/app/schemas/mediaserver.py +++ b/app/schemas/mediaserver.py @@ -14,6 +14,10 @@ class ExistMediaInfo(BaseModel): type: Optional[MediaType] # 季 seasons: Optional[Dict[int, list]] = {} + # 媒体服务器 + server: Optional[str] = None + # 媒体ID + itemid: Optional[Union[str, int]] = None class NotExistMediaInfo(BaseModel):