From 47b895e517355326c5b45934314d13a8fe3bc117 Mon Sep 17 00:00:00 2001 From: wumode Date: Wed, 23 Jul 2025 12:56:46 +0800 Subject: [PATCH] =?UTF-8?q?update(ImdbSource)=20=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E4=BA=8C=E7=BA=A7=E5=88=86=E7=B1=BB=E5=92=8C=E8=87=AA=E5=AE=9A?= =?UTF-8?q?=E4=B9=89=E6=8E=A8=E8=8D=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 2 +- plugins.v2/imdbsource/__init__.py | 321 +++++++++++++++------------- plugins.v2/imdbsource/imdbhelper.py | 76 +++++-- 3 files changed, 231 insertions(+), 168 deletions(-) diff --git a/package.v2.json b/package.v2.json index d188482..8cf4ae1 100644 --- a/package.v2.json +++ b/package.v2.json @@ -440,7 +440,7 @@ "author": "wumode", "level": 1, "history": { - "v1.5.1": "改进媒体id转换", + "v1.5.1": "改进媒体id转换; 支持二级分类和自定义推荐", "v1.5.0": "支持媒体识别", "v1.4.4": "更新数据源", "v1.4.3": "为仪表盘组件添加缓存", diff --git a/plugins.v2/imdbsource/__init__.py b/plugins.v2/imdbsource/__init__.py index 923e8fd..7acc23c 100644 --- a/plugins.v2/imdbsource/__init__.py +++ b/plugins.v2/imdbsource/__init__.py @@ -45,13 +45,13 @@ class ImdbSource(_PluginBase): _proxy: bool = False _staff_picks: bool = False _recognize_media: bool = False + _interests: List[str] = [] _component_size: str = 'medium' _recognition_mode = 'auxiliary' # 私有属性 _imdb_helper = None - _cache = {"discover": [], "trending": [], "trending_in_anime": [], "trending_in_sitcom": [], - "trending_in_documentary": [], "imdb_top_250": [], "staff_picks": {}} + _cache = {"discover": [], "trending": [], "imdb_top_250": [], "staff_picks": {}} _img_proxy_prefix = '' _scheduler: Optional[BackgroundScheduler] = None _original_method = None @@ -59,6 +59,7 @@ class ImdbSource(_PluginBase): def init_plugin(self, config: dict = None): plugin_instance = self + def patched_recognize_media(chain_self, meta: MetaBase = None, mtype: Optional[MediaType] = None, tmdbid: Optional[int] = None, @@ -75,6 +76,7 @@ class ImdbSource(_PluginBase): logger.info(f"通过插件 {plugin_instance.plugin_name} 执行:recognize_media ...") return plugin_instance.recognize_media(meta, mtype) return result + # 给 patch 函数加唯一标记 patched_recognize_media._patched_by = id(self) # 保存原始方法 @@ -87,9 +89,16 @@ class ImdbSource(_PluginBase): self._proxy = config.get("proxy") self._staff_picks = config.get("staff_picks") self._recognize_media = config.get("recognize_media") + if 'interests' not in config: + self._interests = ['Anime', 'Documentary', 'Sitcom'] + else: + self._interests = config.get("interests") + if isinstance(self._interests, str): + self._interests = [self._interests] self._component_size = config.get("component_size") or "medium" self._recognition_mode = config.get("recognition_mode") or "auxiliary" self._imdb_helper = ImdbHelper(proxies=settings.PROXY if self._proxy else None) + self.__update_config() if "media-amazon.com" not in settings.SECURITY_IMAGE_DOMAINS: settings.SECURITY_IMAGE_DOMAINS.append("media-amazon.com") if "media-imdb.com" not in settings.SECURITY_IMAGE_DOMAINS: @@ -112,7 +121,6 @@ class ImdbSource(_PluginBase): else: self.stop_service() - def get_state(self) -> bool: return self._enabled @@ -210,7 +218,7 @@ class ImdbSource(_PluginBase): mtype, year, plot = year_and_type(entry) mp_url = f"/media?mediaid=imdb:{entry.get('ttconst')}&title={entry.get('name')}&year={year}&type={mtype.value}" primary_img_url = next((f"{image.get('url')}" for image in images - if image.get("id") == entry.get('rmconst')), '') + if image.get("id") == entry.get('rmconst')), '') primary_img_url = f'{self._img_proxy_prefix}{primary_img_url}' item1 = { 'component': 'VCarouselItem', @@ -480,6 +488,76 @@ class ImdbSource(_PluginBase): """ 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构 """ + option_groups = [] + for i, v in self._imdb_helper.interests.items(): + options = [] + for name, in_id in v.items(): + option = { + 'component': 'VCol', + 'props': {'cols': 12, 'md': 3}, + 'content': [ + { + 'component': 'VCheckbox', + 'props': {'label': name, 'value': name, 'model': 'interests'}, + } + ] + } + options.append(option) + group = { + 'component': 'VExpansionPanel', + 'content': [ + { + 'component': 'VExpansionPanelTitle', + 'text': i + }, + { + 'component': 'VExpansionPanelText', + 'content': [ + { + 'component': 'VRow', + 'content': options + } + ] + } + ] + } + option_groups.append(group) + interests_ui = { + 'component': 'VExpansionPanels', + 'props': { + 'multiple': False, + 'popout': True + }, + 'content': [ + { + 'component': 'VExpansionPanel', + 'content': [ + { + 'component': 'VExpansionPanelTitle', + 'text': '推荐' + }, + { + 'component': 'VExpansionPanelText', + 'content': [ + { + 'component': 'VRow', + 'content': [ + { + 'component': 'VExpansionPanels', + 'props': { + 'multiple': True, + 'popout': True + }, + 'content': option_groups + } + ] + } + ] + } + ] + } + ] + } return [ { "component": "VForm", @@ -495,7 +573,7 @@ class ImdbSource(_PluginBase): "component": "VSwitch", "props": { "model": "enabled", - "label": "启用插件", + "label": "启用插件" }, } ], @@ -511,7 +589,7 @@ class ImdbSource(_PluginBase): 'component': 'VSwitch', 'props': { 'model': 'proxy', - 'label': '使用代理服务器', + 'label': '使用代理服务器' } } ] @@ -527,7 +605,7 @@ class ImdbSource(_PluginBase): 'component': 'VSwitch', 'props': { 'model': 'staff_picks', - 'label': 'IMDb 编辑精选组件', + 'label': 'IMDb 编辑精选组件' } } ] @@ -543,7 +621,7 @@ class ImdbSource(_PluginBase): 'component': 'VSwitch', 'props': { 'model': 'recognize_media', - 'label': '媒体识别', + 'label': '媒体识别' } } ] @@ -567,7 +645,7 @@ class ImdbSource(_PluginBase): 'label': '组件规格', 'items': [ {"title": "小型", "value": "small"}, - {"title": "中型", "value": "medium"}, + {"title": "中型", "value": "medium"} ] } } @@ -594,14 +672,30 @@ class ImdbSource(_PluginBase): ] } ] + }, + { + "component": "VRow", + "content": [ + { + 'component': 'VCol', + 'props': { + 'cols': 12, + 'md': 12 + }, + 'content': [ + interests_ui + ] + } + ] } - ], + ] } ], { "enabled": False, "proxy": False, "staff_picks": False, "recognize_media": False, + "interests": ['Anime', 'Documentary', 'Sitcom'], "component_size": "medium", "recognition_mode": "auxiliary" } @@ -631,6 +725,19 @@ class ImdbSource(_PluginBase): modules['recognize_media'] = self.recognize_media return modules + def __update_config(self): + self.update_config( + { + "enabled": self._enabled, + "proxy": self._proxy, + "staff_picks": self._staff_picks, + "recognize_media": self._recognize_media, + "interests": self._interests, + "component_size": self._component_size, + "recognition_mode": self._recognition_mode, + } + ) + def __cache_staff_picks(self): entries = self._imdb_helper.staff_picks() imdb_items = None @@ -732,29 +839,30 @@ class ImdbSource(_PluginBase): return True return False - def trending_in_documentary(self, apikey: str, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: - if apikey != settings.API_TOKEN: - return [] + def trending(self, interest: str, page: int = 1) -> List[schemas.MediaInfo]: if not self._imdb_helper: return [] - title_types = ("tvSeries", "tvMiniSeries", "tvShort", 'movie') + if interest not in self._imdb_helper.interest_id: + return [] + count = 30 + title_types = ("tvSeries", "tvMiniSeries", "tvShort", 'tvMovie', 'movie') first_page = False if page == 1: first_page = True - self._cache["trending_in_documentary"] = [] # 清空缓存 + self._cache[interest] = [] # 清空缓存 results = [] - if len(self._cache["trending_in_documentary"]) >= count: - results = self._cache["trending_in_documentary"][:count] - self._cache["trending_in_documentary"] = self._cache["trending_in_documentary"][count:] + if len(self._cache[interest]) >= count: + results = self._cache[interest][:count] + self._cache[interest] = self._cache[interest][count:] else: - results.extend(self._cache["trending_in_documentary"]) + results.extend(self._cache[interest]) remaining = count - len(results) - self._cache["trending_in_documentary"] = [] # 清空缓存 + self._cache[interest] = [] # 清空缓存 data = self._imdb_helper.advanced_title_search(first_page=first_page, title_types=title_types, sort_by="POPULARITY", sort_order="ASC", - interests=("Documentary",) + interests=(interest,) ) if not data: new_results = [] @@ -762,7 +870,7 @@ class ImdbSource(_PluginBase): new_results = data.get("edges") if new_results: results.extend(new_results[:remaining]) - self._cache["trending_in_documentary"] = new_results[remaining:] + self._cache[interest] = new_results[remaining:] res = [] for item in results: title_type_id = item.get('node').get("title").get("titleType", {}).get("id") @@ -773,9 +881,7 @@ class ImdbSource(_PluginBase): res.append(self.__series_to_media(item.get('node').get("title"))) return res - def imdb_top_250(self, apikey: str, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: - if apikey != settings.API_TOKEN: - return [] + def imdb_top_250(self, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: if not self._imdb_helper: return [] title_types = ("movie",) @@ -812,89 +918,7 @@ class ImdbSource(_PluginBase): res.append(self.__movie_to_media(item.get('node').get("title"))) return res - def trending_in_sitcom(self, apikey: str, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: - if apikey != settings.API_TOKEN: - return [] - if not self._imdb_helper: - return [] - title_types = ("tvSeries", "tvMiniSeries", "tvShort") - first_page = False - if page == 1: - first_page = True - self._cache["trending_in_sitcom"] = [] # 清空缓存 - results = [] - if len(self._cache["trending_in_sitcom"]) >= count: - results = self._cache["trending_in_sitcom"][:count] - self._cache["trending_in_sitcom"] = self._cache["trending_in_sitcom"][count:] - else: - results.extend(self._cache["trending_in_sitcom"]) - remaining = count - len(results) - self._cache["trending_in_sitcom"] = [] # 清空缓存 - data = self._imdb_helper.advanced_title_search(first_page=first_page, - title_types=title_types, - sort_by="POPULARITY", - sort_order="ASC", - interests=("Sitcom",) - ) - if not data: - new_results = [] - else: - new_results = data.get("edges") - if new_results: - results.extend(new_results[:remaining]) - self._cache["trending_in_sitcom"] = new_results[remaining:] - res = [] - for item in results: - title_type_id = item.get('node').get("title").get("titleType", {}).get("id") - mtype = ImdbHelper.type_to_mtype(title_type_id) - if mtype == MediaType.TV: - res.append(self.__series_to_media(item.get('node').get("title"))) - return res - - def trending_in_anime(self, apikey: str, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: - if apikey != settings.API_TOKEN: - return [] - if not self._imdb_helper: - return [] - title_types = ("tvSeries", "tvMiniSeries", "tvShort", 'movie') - first_page = False - if page == 1: - first_page = True - self._cache["trending_in_anime"] = [] # 清空缓存 - results = [] - if len(self._cache["trending_in_anime"]) >= count: - results = self._cache["trending_in_anime"][:count] - self._cache["trending_in_anime"] = self._cache["trending_in_anime"][count:] - else: - results.extend(self._cache["trending_in_anime"]) - remaining = count - len(results) - self._cache["trending_in_anime"] = [] # 清空缓存 - data = self._imdb_helper.advanced_title_search(first_page=first_page, - title_types=title_types, - sort_by="POPULARITY", - sort_order="ASC", - interests=("Anime",) - ) - if not data: - new_results = [] - else: - new_results = data.get("edges") - if new_results: - results.extend(new_results[:remaining]) - self._cache["trending_in_anime"] = new_results[remaining:] - res = [] - for item in results: - title_type_id = item.get('node').get("title").get("titleType", {}).get("id") - mtype = ImdbHelper.type_to_mtype(title_type_id) - if mtype == MediaType.MOVIE: - res.append(self.__movie_to_media(item.get('node').get("title"))) - elif mtype == MediaType.TV: - res.append(self.__series_to_media(item.get('node').get("title"))) - return res - - def imdb_trending(self, apikey: str, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: - if apikey != settings.API_TOKEN: - return [] + def imdb_trending(self, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: if not self._imdb_helper: return [] title_types = ("tvSeries", "tvMiniSeries", "tvShort", 'movie') @@ -932,7 +956,7 @@ class ImdbSource(_PluginBase): res.append(self.__series_to_media(item.get('node').get("title"))) return res - def imdb_discover(self, apikey: str, mtype: str = "series", + def imdb_discover(self, mtype: str = "series", country: str = None, lang: str = None, genre: str = None, @@ -945,8 +969,6 @@ class ImdbSource(_PluginBase): ranked_list: str = None, page: int = 1, count: int = 30) -> List[schemas.MediaInfo]: - if apikey != settings.API_TOKEN: - return [] if not self._imdb_helper: return [] title_type = ("tvSeries", "tvMiniSeries", "tvShort") @@ -1055,50 +1077,41 @@ class ImdbSource(_PluginBase): "summary": "API说明" }] """ - return [ + apis = [ { - "path": "/imdb_discover", + "path": "/imdb-discover", "endpoint": self.imdb_discover, "methods": ["GET"], + "auth": 'bear', "summary": "IMDb探索数据源", "description": "获取 IMDb探索 数据", }, { - "path": "/imdb_trending", + "path": "/imdb-trending", "endpoint": self.imdb_trending, "methods": ["GET"], + "auth": 'bear', "summary": "IMDb Trending", "description": "获取 IMDb Trending 数据", }, { - "path": "/trending_in_anime", - "endpoint": self.trending_in_anime, - "methods": ["GET"], - "summary": "IMDb Trending in Anime", - "description": "获取 IMDb Trending in Anime 数据", - }, - { - "path": "/trending_in_sitcom", - "endpoint": self.trending_in_sitcom, - "methods": ["GET"], - "summary": "IMDb Trending in Sitcom", - "description": "获取 IMDb Trending in Sitcom 数据", - }, - { - "path": "/imdb_top_250", + "path": "/imdb-top-250", "endpoint": self.imdb_top_250, "methods": ["GET"], + "auth": 'bear', "summary": "IMDb Top 250 Movies", "description": "获取 IMDb Top 250 Movies 数据", }, { - "path": "/trending_in_documentary", - "endpoint": self.trending_in_documentary, + "path": "/trending", + "endpoint": self.trending, "methods": ["GET"], - "summary": "IMDb Trending in Documentary", - "description": "获取 IMDb Trending in Documentary 数据", + "auth": 'bear', + "summary": f"Trending on IMDb", + "description": f"获取 Trending on IMDb 数据", } ] + return apis @staticmethod def imdb_filter_ui() -> List[dict]: @@ -1638,7 +1651,7 @@ class ImdbSource(_PluginBase): imdb_source = schemas.DiscoverMediaSource( name="IMDb", mediaid_prefix="imdb", - api_path=f"plugin/ImdbSource/imdb_discover?apikey={settings.API_TOKEN}", + api_path=f"plugin/ImdbSource/imdb-discover", filter_params={ "mtype": "series", "company": None, @@ -1690,31 +1703,27 @@ class ImdbSource(_PluginBase): return imdb_trending: schemas.RecommendMediaSource = schemas.RecommendMediaSource( name="IMDb Trending", - api_path=f"plugin/ImdbSource/imdb_trending?apikey={settings.API_TOKEN}", + api_path=f"plugin/ImdbSource/imdb-trending", type='Rankings' ) - trending_in_anime: schemas.RecommendMediaSource = schemas.RecommendMediaSource( - name="Trending Anime on IMDb", - api_path=f"plugin/ImdbSource/trending_in_anime?apikey={settings.API_TOKEN}", - type='Anime' - ) - trending_in_sitcom: schemas.RecommendMediaSource = schemas.RecommendMediaSource( - name="Trending Sitcom on IMDb", - api_path=f"plugin/ImdbSource/trending_in_sitcom?apikey={settings.API_TOKEN}", - type='TV Shows' - ) - imdb_top_250: schemas.RecommendMediaSource = schemas.RecommendMediaSource( name="IMDb Top 250 Movies", - api_path=f"plugin/ImdbSource/imdb_top_250?apikey={settings.API_TOKEN}", + api_path=f"plugin/ImdbSource/imdb-top-250", type='Movies' ) - imdb_documentary: schemas.RecommendMediaSource = schemas.RecommendMediaSource( - name="Trending Documentary on IMDb", - api_path=f"plugin/ImdbSource/trending_in_documentary?apikey={settings.API_TOKEN}", - type='Rankings' - ) - trending_source = [imdb_trending, trending_in_anime, trending_in_sitcom, imdb_top_250, imdb_documentary] + trending_source = [imdb_trending, imdb_top_250] + for interest in self._interests: + source_type = 'Rankings' + if interest in self._imdb_helper.interests['Anime']: + source_type = 'Anime' + elif interest in ['Sitcom']: + source_type = 'TV Shows' + source = schemas.RecommendMediaSource( + name=f"Trending {interest} on IMDb", + api_path=f"plugin/ImdbSource/trending?interest={interest}", + type=source_type + ) + trending_source.append(source) if not event_data.extra_sources: event_data.extra_sources = trending_source else: @@ -1769,9 +1778,11 @@ class ImdbSource(_PluginBase): if info: break if info: - info = self._imdb_helper.update_info(info.get('id'), info=info) or {} + info = self._imdb_helper.update_info(info.get('id'), info=info) or info mediainfo = ImdbSource._convert_mediainfo(info) mediainfo.tmdb_id = ImdbSource.imdb_to_tmdb(info.get('id'), mediainfo) + cat = ImdbHelper.get_category(info.get('media_type'), info) + mediainfo.set_category(cat) logger.info(f"{meta.name} IMDb 识别结果:{mediainfo.type.value} " f"{mediainfo.title_year} " f"{mediainfo.imdb_id}") @@ -1865,6 +1876,7 @@ class ImdbSource(_PluginBase): def match_year(res): date = res.get('first_air_date') or res.get('release_date') or '' return date[:4] == media_info.year + result_id, filtered = filter_and_return(filtered, match_year) if result_id: return result_id @@ -1875,6 +1887,7 @@ class ImdbSource(_PluginBase): def match_name(res): name = res.get('name') or '' return ImdbHelper.compare_names(name, media_info.names) + result_id, filtered = filter_and_return(filtered, match_name) if result_id: return result_id diff --git a/plugins.v2/imdbsource/imdbhelper.py b/plugins.v2/imdbsource/imdbhelper.py index 1ecff47..172173b 100644 --- a/plugins.v2/imdbsource/imdbhelper.py +++ b/plugins.v2/imdbsource/imdbhelper.py @@ -44,20 +44,11 @@ class ImdbHelper: all_title_types = ["tvSeries", "tvMiniSeries", "movie", "tvMovie", "musicVideo", "tvShort", "short", "tvEpisode", "tvSpecial"] - interest_id = { - "Anime": "in0000027", - "Superhero": "in0000008", - "Sitcom": "in0000044", - "Coming-of-Age": "in0000073", - "Slasher Horror": "in0000115", - "Raunchy Comedy": "in0000041", - "Documentary": "in0000060" - } + interests = {'Action': {'Action': 'in0000001', 'Action Epic': 'in0000002', 'B-Action': 'in0000003', 'Car Action': 'in0000004', 'Disaster': 'in0000005', 'Gun Fu': 'in0000197', 'Kung Fu': 'in0000198', 'Martial Arts': 'in0000006', 'One-Person Army Action': 'in0000007', 'Samurai': 'in0000199', 'Superhero': 'in0000008', 'Sword & Sandal': 'in0000009', 'War': 'in0000010', 'War Epic': 'in0000011', 'Wuxia': 'in0000200'}, 'Adventure': {'Adventure': 'in0000012', 'Adventure Epic': 'in0000015', 'Desert Adventure': 'in0000013', 'Dinosaur Adventure': 'in0000014', 'Globetrotting Adventure': 'in0000016', 'Jungle Adventure': 'in0000017', 'Mountain Adventure': 'in0000018', 'Quest': 'in0000019', 'Road Trip': 'in0000020', 'Sea Adventure': 'in0000021', 'Swashbuckler': 'in0000022', 'Teen Adventure': 'in0000023', 'Urban Adventure': 'in0000024'}, 'Animation': {'Adult Animation': 'in0000025', 'Animation': 'in0000026', 'Computer Animation': 'in0000028', 'Hand-Drawn Animation': 'in0000029', 'Stop Motion Animation': 'in0000030'}, 'Anime': {'Anime': 'in0000027', 'Isekai': 'in0000201', 'Iyashikei': 'in0000202', 'Josei': 'in0000203', 'Mecha': 'in0000204', 'Seinen': 'in0000205', 'Shōjo': 'in0000207', 'Shōnen': 'in0000206', 'Slice of Life': 'in0000208'}, 'Comedy': {'Body Swap Comedy': 'in0000031', 'Buddy Comedy': 'in0000032', 'Buddy Cop': 'in0000033', 'Comedy': 'in0000034', 'Dark Comedy': 'in0000035', 'Farce': 'in0000036', 'High-Concept Comedy': 'in0000037', 'Mockumentary': 'in0000038', 'Parody': 'in0000039', 'Quirky Comedy': 'in0000040', 'Raunchy Comedy': 'in0000041', 'Satire': 'in0000042', 'Screwball Comedy': 'in0000043', 'Sitcom': 'in0000044', 'Sketch Comedy': 'in0000045', 'Slapstick': 'in0000046', 'Stand-Up': 'in0000047', 'Stoner Comedy': 'in0000048', 'Teen Comedy': 'in0000049'}, 'Crime': {'Caper': 'in0000050', 'Cop Drama': 'in0000051', 'Crime': 'in0000052', 'Drug Crime': 'in0000053', 'Film Noir': 'in0000054', 'Gangster': 'in0000055', 'Heist': 'in0000056', 'Police Procedural': 'in0000057', 'True Crime': 'in0000058'}, 'Documentary': {'Crime Documentary': 'in0000059', 'Documentary': 'in0000060', 'Docuseries': 'in0000061', 'Faith & Spirituality Documentary': 'in0000062', 'Food Documentary': 'in0000063', 'History Documentary': 'in0000064', 'Military Documentary': 'in0000065', 'Music Documentary': 'in0000066', 'Nature Documentary': 'in0000067', 'Political Documentary': 'in0000068', 'Science & Technology Documentary': 'in0000069', 'Sports Documentary': 'in0000070', 'Travel Documentary': 'in0000071'}, 'Drama': {'Biography': 'in0000072', 'Coming-of-Age': 'in0000073', 'Costume Drama': 'in0000074', 'Docudrama': 'in0000075', 'Drama': 'in0000076', 'Epic': 'in0000077', 'Financial Drama': 'in0000078', 'Historical Epic': 'in0000079', 'History': 'in0000080', 'Korean Drama': 'in0000209', 'Legal Drama': 'in0000081', 'Medical Drama': 'in0000082', 'Period Drama': 'in0000083', 'Political Drama': 'in0000084', 'Prison Drama': 'in0000085', 'Psychological Drama': 'in0000086', 'Showbiz Drama': 'in0000087', 'Soap Opera': 'in0000088', 'Teen Drama': 'in0000089', 'Telenovela': 'in0000210', 'Tragedy': 'in0000090', 'Workplace Drama': 'in0000091'}, 'Family': {'Animal Adventure': 'in0000092', 'Family': 'in0000093'}, 'Fantasy': {'Dark Fantasy': 'in0000095', 'Fairy Tale': 'in0000097', 'Fantasy': 'in0000098', 'Fantasy Epic': 'in0000096', 'Supernatural Fantasy': 'in0000099', 'Sword & Sorcery': 'in0000100', 'Teen Fantasy': 'in0000101'}, 'Game Show': {'Beauty Competition': 'in0000102', 'Cooking Competition': 'in0000103', 'Game Show': 'in0000105', 'Quiz Show': 'in0000104', 'Survival Competition': 'in0000106', 'Talent Competition': 'in0000107'}, 'Horror': {'B-Horror': 'in0000108', 'Body Horror': 'in0000109', 'Folk Horror': 'in0000110', 'Found Footage Horror': 'in0000111', 'Horror': 'in0000112', 'Monster Horror': 'in0000113', 'Psychological Horror': 'in0000114', 'Slasher Horror': 'in0000115', 'Splatter Horror': 'in0000116', 'Supernatural Horror': 'in0000117', 'Teen Horror': 'in0000118', 'Vampire Horror': 'in0000119', 'Werewolf Horror': 'in0000120', 'Witch Horror': 'in0000121', 'Zombie Horror': 'in0000122'}, 'Lifestyle': {'Beauty Makeover': 'in0000123', 'Cooking & Food': 'in0000124', 'Home Improvement': 'in0000125', 'Lifestyle': 'in0000126', 'News': 'in0000211', 'Talk Show': 'in0000127', 'Travel': 'in0000128'}, 'Music': {'Concert': 'in0000129', 'Music': 'in0000130'}, 'Musical': {'Classic Musical': 'in0000131', 'Jukebox Musical': 'in0000132', 'Musical': 'in0000133', 'Pop Musical': 'in0000134', 'Rock Musical': 'in0000135'}, 'Mystery': {'Bumbling Detective': 'in0000136', 'Cozy Mystery': 'in0000137', 'Hard-boiled Detective': 'in0000138', 'Mystery': 'in0000139', 'Suspense Mystery': 'in0000140', 'Whodunnit': 'in0000141'}, 'Reality TV': {'Business Reality TV': 'in0000142', 'Crime Reality TV': 'in0000143', 'Dating Reality TV': 'in0000144', 'Docusoap Reality TV': 'in0000145', 'Hidden Camera': 'in0000146', 'Paranormal Reality TV': 'in0000147', 'Reality TV': 'in0000148'}, 'Romance': {'Dark Romance': 'in0000149', 'Feel-Good Romance': 'in0000151', 'Romance': 'in0000152', 'Romantic Comedy': 'in0000153', 'Romantic Epic': 'in0000150', 'Steamy Romance': 'in0000154', 'Teen Romance': 'in0000155', 'Tragic Romance': 'in0000156'}, 'Sci-Fi': {'Alien Invasion': 'in0000157', 'Artificial Intelligence': 'in0000158', 'Cyberpunk': 'in0000159', 'Dystopian Sci-Fi': 'in0000160', 'Kaiju': 'in0000161', 'Sci-Fi': 'in0000162', 'Sci-Fi Epic': 'in0000163', 'Space Sci-Fi': 'in0000164', 'Steampunk': 'in0000165', 'Time Travel': 'in0000166'}, 'Seasonal': {'Holiday': 'in0000192', 'Holiday Animation': 'in0000193', 'Holiday Comedy': 'in0000194', 'Holiday Family': 'in0000195', 'Holiday Romance': 'in0000196'}, 'Short': {'Short': 'in0000212'}, 'Sport': {'Baseball': 'in0000167', 'Basketball': 'in0000168', 'Boxing': 'in0000169', 'Extreme Sport': 'in0000170', 'Football': 'in0000171', 'Motorsport': 'in0000172', 'Soccer': 'in0000173', 'Sport': 'in0000174', 'Water Sport': 'in0000175'}, 'Thriller': {'Conspiracy Thriller': 'in0000176', 'Cyber Thriller': 'in0000177', 'Erotic Thriller': 'in0000178', 'Giallo': 'in0000179', 'Legal Thriller': 'in0000180', 'Political Thriller': 'in0000181', 'Psychological Thriller': 'in0000182', 'Serial Killer': 'in0000183', 'Spy': 'in0000184', 'Survival': 'in0000185', 'Thriller': 'in0000186'}, 'Western': {'Classical Western': 'in0000187', 'Contemporary Western': 'in0000188', 'Spaghetti Western': 'in0000190', 'Western': 'in0000191', 'Western Epic': 'in0000189'}} _official_endpoint = "https://caching.graphql.imdb.com/" _imdb_headers = { "Accept": "text/html,application/json,text/plain,*/*", - "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome" - "/84.0.4147.105 Safari/537.36", + "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/138.0.0.0 Safari/537.36", "Referer": "https://www.imdb.com/", } _free_api = "https://api.imdbapi.dev" @@ -75,6 +66,10 @@ class ImdbHelper: self.hash_status = {"AdvancedTitleSearch": False, "TitleAkasPaginated": False} self._search_states = OrderedDict() self._max_states = 30 + self.interest_id = {} + for category, value in self.interests.items(): + for name, in_id in value.items(): + self.interest_id[name] = in_id @retry(Exception, logger=logger) @cached(maxsize=32, ttl=1800) @@ -246,7 +241,59 @@ class ImdbHelper: @staticmethod def release_date_string(release_date: Dict) -> Optional[str]: - return f"{release_date.get('year', 0):04d}-{release_date.get('month', 0):02d}-{release_date.get('day', 0):02d}" + year = release_date.get('year') or 0 + month = release_date.get('month') or 0 + day = release_date.get('day') or 0 + return f"{year:04d}-{month:02d}-{day:02d}" + + @staticmethod + def get_category(mtype: MediaType, imdb_info: dict) -> str: + tv_category = { + '国漫': {'genres': 'Animation', 'originCountries': 'CN,TW,HK'}, + '日番': {'genres': 'Animation', 'originCountries': 'JP'}, + '纪录片': {'genres': 'Documentary'}, + '综艺': {'genres': 'Reality-TV,Game-Show'}, + '国产剧': {'originCountries': 'CN,TW,HK'}, + '欧美剧': {'originCountries': 'US,FR,GB,DE,ES,IT,NL,PT,RU,UK'}, + '日韩剧': {'originCountries': 'JP,KP,KR,TH,IN,SG'}, + '未分类': None + } + movie_category = { + '动画电影': {'genres': 'Animation'}, + '华语电影': {'spokenLanguages': 'zho,cmn,yue,nan'}, + '外语电影': None} + categories = {MediaType.TV: tv_category, MediaType.MOVIE: movie_category} + category = categories.get(mtype) + if not imdb_info or not category: + return "" + for key, item in category.items(): + if not item: + return key + match_flag = True + for attr, value in item.items(): + if not value: + continue + if attr == 'originCountries': + origin_countries = imdb_info.get('originCountries') + info_value = origin_countries[0].get('code') or [] if origin_countries else [] + elif attr == 'spokenLanguages': + spoken_languages = imdb_info.get('spokenLanguages') + info_value = spoken_languages[0].get('code') or [] if spoken_languages else [] + else: + info_value = imdb_info.get(attr) + if isinstance(info_value, list): + info_values = info_value + else: + info_values = [info_value] + if value.find(',') != -1: + values = [str(val) for val in value.split(',') if val] + else: + values = [str(value)] + if not set(values).intersection(set(info_values)): + match_flag = False + if match_flag: + return key + return "" def advanced_title_search(self, first_page: bool = True, @@ -489,7 +536,10 @@ class ImdbHelper: if r is None: return None if r.status_code != 200: - logger.warn(f"{r.json().get('message')}") + try: + logger.warn(f"{r.json().get('message')}") + except requests.exceptions.JSONDecodeError: + return None return None return r.json()