diff --git a/app/chain/mediaserver.py b/app/chain/mediaserver.py index 75a2df7e4..a6766a889 100644 --- a/app/chain/mediaserver.py +++ b/app/chain/mediaserver.py @@ -27,11 +27,17 @@ def librarys(self, server: str, username: str = None, hidden: bool = False) -> L """ return self.run_module("mediaserver_librarys", server=server, username=username, hidden=hidden) - def items(self, server: str, library_id: Union[str, int]) -> List[schemas.MediaServerItem]: + def items(self, server: str, library_id: Union[str, int], start_index: int = 0, limit: int = 100) -> List[schemas.MediaServerItem]: """ 获取媒体服务器所有项目 """ - return self.run_module("mediaserver_items", server=server, library_id=library_id) + data = [] + data_generator = self.run_module("mediaserver_items", server=server, library_id=library_id, start_index=start_index, limit=limit) + if data_generator: + for item in data_generator: + if item: + data.append(item) + return data def iteminfo(self, server: str, item_id: Union[str, int]) -> schemas.MediaServerItem: """ diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 49c7130cd..d0b345fad 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -182,13 +182,13 @@ def mediaserver_librarys(self, server: str, return server.get_librarys(username=username, hidden=hidden) return None - def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: + def mediaserver_items(self, server: str, library_id: str, start_index: int = 0, limit: int = 100) -> Optional[Generator]: """ 媒体库项目列表 """ server: Emby = self.get_server(server) if server: - return server.get_items(library_id) + return server.get_items(library_id, start_index, limit) return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: diff --git a/app/modules/emby/emby.py b/app/modules/emby/emby.py index 6dff5eb7d..0c8414fa4 100644 --- a/app/modules/emby/emby.py +++ b/app/modules/emby/emby.py @@ -1,6 +1,7 @@ import json import re import traceback +from datetime import datetime from pathlib import Path from typing import List, Optional, Union, Dict, Generator, Tuple @@ -352,7 +353,7 @@ def get_movies(self, url = f"{self._host}emby/Items" params = { "IncludeItemTypes": "Movie", - "Fields": "ProductionYear", + "Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId", "StartIndex": 0, "Recursive": "true", "SearchTerm": title, @@ -366,30 +367,15 @@ def get_movies(self, 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") - 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: + for item in res_items: + if not item: + continue + mediaserver_item = self.__format_item_info(item) + if mediaserver_item: + if (not tmdb_id or mediaserver_item.tmdbid == tmdb_id) and \ + mediaserver_item.title == title and \ + (not year or str(mediaserver_item.year) == str(year)): ret_movies.append(mediaserver_item) - continue - 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)) @@ -615,6 +601,48 @@ def __get_emby_library_id_by_item(self, item: schemas.RefreshMediaItem) -> Optio # 刷新根目录 return "/" + def __format_item_info(self, item) -> Optional[schemas.MediaServerItem]: + """ + 格式化item + """ + try: + user_data = item.get("UserData", {}) + if not user_data: + user_state = None + else: + resume = item.get("UserData", {}).get("PlaybackPositionTicks") and item.get("UserData", {}).get("PlaybackPositionTicks") > 0 + last_played_date = item.get("UserData", {}).get("LastPlayedDate") + if last_played_date is not None and "." in last_played_date: + last_played_date = last_played_date.split(".")[0] + user_state = schemas.MediaServerItemUserState( + played=item.get("UserData", {}).get("Played"), + resume=resume, + last_played_date=datetime.strptime(last_played_date, "%Y-%m-%dT%H:%M:%S").strftime( + "%Y-%m-%d %H:%M:%S") if last_played_date else None, + play_count=item.get("UserData", {}).get("PlayCount"), + percentage=item.get("UserData", {}).get("PlayedPercentage"), + ) + tmdbid = item.get("ProviderIds", {}).get("Tmdb") + return schemas.MediaServerItem( + id=item.get("Id"), + 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"), + user_state=user_state + + ) + except Exception as e: + logger.error(e) + return None + def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: """ 获取单个项目详情 @@ -630,28 +658,19 @@ def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: try: res = RequestUtils().get_res(url, params) if res and res.status_code == 200: - 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") - ) + iteminfo = self.__format_item_info(res.json()) + return iteminfo except Exception as e: - logger.error(f"连接Items/Id出错:" + str(e)) + logger.error(f"连接/Users/{self.user}/Items/{itemid}出错:" + str(e)) return None - def get_items(self, parent: str) -> Generator: + def get_items(self, parent: str, start_index: int = 0, limit: int = 100) -> Generator: """ 获取媒体服务器所有媒体库列表 + :param parent: 父媒体库ID + :param start_index: 开始索引,用于分页 + :param limit: 每次请求返回的项目数量 + :return: 生成器 schemas.MediaServerItem """ if not parent: yield None @@ -660,20 +679,25 @@ def get_items(self, parent: str) -> Generator: url = f"{self._host}emby/Users/{self.user}/Items" params = { "ParentId": parent, - "api_key": self._apikey + "api_key": self._apikey, + "Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId", + "StartIndex": start_index, + "Limit": limit } try: res = RequestUtils().get_res(url, params) - if res and res.status_code == 200: - results = res.json().get("Items") or [] - for result in results: - if not result: - continue - if result.get("Type") in ["Movie", "Series"]: - 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 + if not res or res.status_code != 200: + yield None + items = res.json().get("Items") or [] + for item in items: + if not item: + continue + if "Folder" in item.get("Type"): + for items in self.get_items(parent=item.get('Id')): + yield items + elif item.get("Type") in ["Movie", "Series"]: + yield self.__format_item_info(item) + except Exception as e: logger.error(f"连接Users/Items出错:" + str(e)) yield None diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index e9e414618..b3ccac7be 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -180,13 +180,13 @@ def mediaserver_librarys(self, server: str = None, return server.get_librarys(username=username, hidden=hidden) return None - def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: + def mediaserver_items(self, server: str, library_id: str, start_index: int = 0, limit: int = 100) -> Optional[Generator]: """ 媒体库项目列表 """ server: Jellyfin = self.get_server(server) if server: - return server.get_items(library_id) + return server.get_items(library_id, start_index, limit) return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: diff --git a/app/modules/jellyfin/jellyfin.py b/app/modules/jellyfin/jellyfin.py index e9242afbf..658485271 100644 --- a/app/modules/jellyfin/jellyfin.py +++ b/app/modules/jellyfin/jellyfin.py @@ -1,4 +1,5 @@ import json +from datetime import datetime from typing import List, Union, Optional, Dict, Generator, Tuple from requests import Response @@ -345,6 +346,8 @@ def get_movies(self, url = f"{self._host}Users/{self.user}/Items" params = { "IncludeItemTypes": "Movie", + "Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId", + "StartIndex": 0, "Recursive": "true", "searchTerm": title, "Limit": 10, @@ -357,29 +360,14 @@ def get_movies(self, if res_items: ret_movies = [] for item in res_items: - item_tmdbid = item.get("ProviderIds", {}).get("Tmdb") - mediaserver_item = schemas.MediaServerItem( - server="jellyfin", - 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: + if not item: + continue + mediaserver_item = self.__format_item_info(item) + if mediaserver_item: + if (not tmdb_id or mediaserver_item.tmdbid == tmdb_id) and \ + mediaserver_item.title == title and \ + (not year or str(mediaserver_item.year) == str(year)): ret_movies.append(mediaserver_item) - continue - 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)) @@ -674,6 +662,49 @@ def get_webhook_message(self, body: any) -> Optional[schemas.WebhookEventInfo]: return eventItem + + def __format_item_info(self, item) -> Optional[schemas.MediaServerItem]: + """ + 格式化item + """ + try: + user_data = item.get("UserData", {}) + if not user_data: + user_state = None + else: + resume = item.get("UserData", {}).get("PlaybackPositionTicks") and item.get("UserData", {}).get("PlaybackPositionTicks") > 0 + last_played_date = item.get("UserData", {}).get("LastPlayedDate") + if last_played_date is not None and "." in last_played_date: + last_played_date = last_played_date.split(".")[0] + user_state = schemas.MediaServerItemUserState( + played=item.get("UserData", {}).get("Played"), + resume=resume, + last_played_date=datetime.strptime(last_played_date, "%Y-%m-%dT%H:%M:%S").strftime( + "%Y-%m-%d %H:%M:%S") if last_played_date else None, + play_count=item.get("UserData", {}).get("PlayCount"), + percentage=item.get("UserData", {}).get("PlayedPercentage"), + ) + tmdbid = item.get("ProviderIds", {}).get("Tmdb") + return schemas.MediaServerItem( + server="jellyfin", + id=item.get("Id"), + 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"), + user_state=user_state + + ) + except Exception as e: + logger.error(e) + return None + def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: """ 获取单个项目详情 @@ -689,28 +720,18 @@ def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]: try: res = RequestUtils().get_res(url, params) if res and res.status_code == 200: - item = res.json() - tmdbid = item.get("ProviderIds", {}).get("Tmdb") - return schemas.MediaServerItem( - server="jellyfin", - 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") - ) + return self.__format_item_info(res.json()) except Exception as e: - logger.error(f"连接Users/Items出错:" + str(e)) + logger.error(f"连接Users/{self.user}/Items/{itemid}:" + str(e)) return None - def get_items(self, parent: str) -> Generator: + def get_items(self, parent: str, start_index: int = 0, limit: int = 100) -> Generator: """ 获取媒体服务器所有媒体库列表 + :param parent: 父媒体库ID + :param start_index: 开始索引,用于分页 + :param limit: 每次请求返回的项目数量 + :return: 生成器 schemas.MediaServerItem """ if not parent: yield None @@ -719,20 +740,24 @@ def get_items(self, parent: str) -> Generator: url = f"{self._host}Users/{self.user}/Items" params = { "parentId": parent, - "api_key": self._apikey + "api_key": self._apikey, + "Fields": "ProviderIds,OriginalTitle,ProductionYear,Path,UserDataPlayCount,UserDataLastPlayedDate,ParentId", + "StartIndex": start_index, + "Limit": limit, } try: res = RequestUtils().get_res(url, params) - if res and res.status_code == 200: - results = res.json().get("Items") or [] - for result in results: - if not result: - continue - if result.get("Type") in ["Movie", "Series"]: - yield self.get_iteminfo(result.get("Id")) - elif "Folder" in result.get("Type"): - for item in self.get_items(result.get("Id")): - yield item + if not res or res.status_code != 200: + yield None + items = res.json().get("Items") or [] + for item in items: + if not item: + continue + if "Folder" in item.get("Type"): + for items in self.get_items(item.get("Id")): + yield items + elif item.get("Type") in ["Movie", "Series"]: + yield self.__format_item_info(item) except Exception as e: logger.error(f"连接Users/Items出错:" + str(e)) yield None diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index 5573bfc14..013e9273f 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -168,13 +168,13 @@ def mediaserver_librarys(self, server: str = None, hidden: bool = False, return server.get_librarys(hidden) return None - def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: + def mediaserver_items(self, server: str, library_id: str, start_index: int = 0, limit: int = 100) -> Optional[Generator]: """ 媒体库项目列表 """ server: Plex = self.get_server(server) if server: - return server.get_items(library_id) + return server.get_items(library_id, start_index, limit) return None def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: diff --git a/app/modules/plex/plex.py b/app/modules/plex/plex.py index 15907b9b6..8724312c7 100644 --- a/app/modules/plex/plex.py +++ b/app/modules/plex/plex.py @@ -446,9 +446,13 @@ def parse_tmdb_id(value: str) -> (bool, int): return ids - def get_items(self, parent: str) -> Generator: + def get_items(self, parent: str, start_index: int = 0, limit: int = 100) -> Generator: """ 获取媒体服务器所有媒体库列表 + :param parent: 父媒体库ID + :param start_index: 开始索引,用于分页 + :param limit: 每次请求返回的项目数量 + :return: 生成器 schemas.MediaServerItem """ if not parent: yield None @@ -457,7 +461,7 @@ def get_items(self, parent: str) -> Generator: try: section = self._plex.library.sectionByID(int(parent)) if section: - for item in section.all(): + for item in section.all(container_start=start_index, limit=limit): try: if not item: continue @@ -465,7 +469,22 @@ def get_items(self, parent: str) -> Generator: path = None if item.locations: path = item.locations[0] + playback_position = item.viewOffset if hasattr(item, 'viewOffset') else 0 + duration = item.duration if hasattr(item, 'duration') else 0 + percentage = (playback_position / duration * 100) if duration > 0 else None + played = item.isPlayed if hasattr(item, 'isPlayed') else False + play_count = item.viewCount if hasattr(item, 'viewCount') else 0 + last_played_date = item.lastViewedAt if hasattr(item, 'lastViewedAt') else None + user_state = schemas.MediaServerItemUserState( + played=played, + resume=playback_position > 0, + last_played_date=last_played_date.isoformat() if last_played_date else None, + play_count=play_count, + percentage=percentage, + ) + yield schemas.MediaServerItem( + id=item.ratingKey, server="plex", library=item.librarySectionID, item_id=item.key, @@ -477,6 +496,7 @@ def get_items(self, parent: str) -> Generator: imdbid=ids['imdb_id'], tvdbid=ids['tvdb_id'], path=path, + user_state=user_state, ) except Exception as e: logger.error(f"处理媒体项目时出错:{str(e)}, 跳过此项目。") diff --git a/app/schemas/mediaserver.py b/app/schemas/mediaserver.py index 582ad2d7b..807f5c151 100644 --- a/app/schemas/mediaserver.py +++ b/app/schemas/mediaserver.py @@ -72,6 +72,19 @@ class MediaServerLibrary(BaseModel): link: Optional[str] = None + +class MediaServerItemUserState(BaseModel): + # 已播放 + played: Optional[bool] = None + # 继续播放 + resume: Optional[bool] = None + # 上次播放时间 10位时间戳 + last_played_date: Optional[str] = None + # 播放次数(不等于完播次数,理解为浏览次数) + play_count: Optional[int] = None + # 播放进度 + percentage: Optional[float] = None + class MediaServerItem(BaseModel): """ 媒体服务器媒体信息 @@ -106,6 +119,7 @@ class MediaServerItem(BaseModel): note: Optional[str] = None # 同步时间 lst_mod_date: Optional[str] = None + user_state: Optional[MediaServerItemUserState] = None class Config: orm_mode = True