From 1f80e3b0782a0d3fb518c26f6a2f884879b2caf4 Mon Sep 17 00:00:00 2001 From: wumode Date: Fri, 16 Jan 2026 22:30:21 +0800 Subject: [PATCH 1/3] =?UTF-8?q?fix(LexiAnnot):=20=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E6=BD=9C=E5=9C=A8=E7=9A=84=E6=95=B0=E6=8D=AE=E6=A0=A1=E9=AA=8C?= =?UTF-8?q?=E9=94=99=E8=AF=AF?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- package.v2.json | 3 ++- plugins.v2/lexiannot/__init__.py | 2 +- plugins.v2/lexiannot/pipeline.py | 8 -------- plugins.v2/lexiannot/schemas.py | 18 ++++++++++++------ 4 files changed, 15 insertions(+), 16 deletions(-) diff --git a/package.v2.json b/package.v2.json index 999a7f0..245c3bd 100644 --- a/package.v2.json +++ b/package.v2.json @@ -554,11 +554,12 @@ "name": "美剧生词标注", "description": "根据CEFR等级,为英语影视剧标注高级词汇。", "labels": "英语", - "version": "1.2.3", + "version": "1.2.4", "icon": "LexiAnnot.png", "author": "wumode", "level": 1, "history": { + "v1.2.4": "增强数据校验", "v1.2.3": "优化提示词", "v1.2.1": "改进字幕样式获取方法", "v1.2.0": "引入大模型候选词决策和词义丰富处理链; 支持读取系统智能体配置; 添加智能体工具; 优化通知样式; 改进 UI", diff --git a/plugins.v2/lexiannot/__init__.py b/plugins.v2/lexiannot/__init__.py index 385c0f5..c9b7d5d 100644 --- a/plugins.v2/lexiannot/__init__.py +++ b/plugins.v2/lexiannot/__init__.py @@ -60,7 +60,7 @@ class LexiAnnot(_PluginBase): # 插件图标 plugin_icon = "LexiAnnot.png" # 插件版本 - plugin_version = "1.2.3" + plugin_version = "1.2.4" # 插件作者 plugin_author = "wumode" # 作者主页 diff --git a/plugins.v2/lexiannot/pipeline.py b/plugins.v2/lexiannot/pipeline.py index 7ca1c23..45dd7af 100644 --- a/plugins.v2/lexiannot/pipeline.py +++ b/plugins.v2/lexiannot/pipeline.py @@ -509,10 +509,6 @@ Your goal is two-fold: * **Do NOT include** simple high-frequency words, common fillers ('gonna', 'gotta'), onomatopoeia, or basic swear words. ------------------------- -You MUST return output strictly matching the provided Pydantic schema. -Return ONLY valid JSON. - -**Here are the output format instructions you MUST follow strictly:** {format_instructions} """, ), @@ -556,10 +552,6 @@ For each word (identified by `WORD_ID`), provide: **Your judgment should be based strictly on the provided subtitle context. DO NOT fabricate context or forced explanation.** ------------------------- -You MUST return output strictly matching the provided Pydantic schema. -Return ONLY valid JSON. - -**Here are the output format instructions you MUST follow strictly:** {format_instructions} """, ), diff --git a/plugins.v2/lexiannot/schemas.py b/plugins.v2/lexiannot/schemas.py index 764949a..801e9c0 100644 --- a/plugins.v2/lexiannot/schemas.py +++ b/plugins.v2/lexiannot/schemas.py @@ -1,10 +1,10 @@ import re import uuid from collections import Counter -from enum import Enum +from enum import Enum, StrEnum from typing import Literal, Generator, Iterator -from pydantic import BaseModel, Field, RootModel, model_validator +from pydantic import BaseModel, Field, RootModel, model_validator, field_validator from app.utils.singleton import Singleton @@ -12,9 +12,8 @@ from app.utils.singleton import Singleton Cefr = Literal["C2", "C1", "B2", "B1", "A2", "A1"] -class UniversalPos(str, Enum): +class UniversalPos(StrEnum): """Universal Part-of-Speech tags""" - ADJ = "ADJ" # Adjective ADV = "ADV" # Adverb INTJ = "INTJ" # Interjection @@ -34,9 +33,8 @@ class UniversalPos(str, Enum): X = "X" # Other/unknown -class LexicalFeatures(str, Enum): +class LexicalFeatures(StrEnum): """Lexical features for words.""" - FORMAL = "formal" INFORMAL = "informal" SLANG = "slang" @@ -333,6 +331,14 @@ class LlmWordEnrichment(BaseModel): usage_context: str | None = Field(default=None, description="Usage or Cultural Context") lexical_features: list[LexicalFeatures] = Field(default_factory=list, description="Lexical features") + @field_validator("lexical_features", mode="before") + @classmethod + def filter_invalid_lexical_features(cls, v): + if isinstance(v, list): + valid_values = {f.value for f in LexicalFeatures} + return [item for item in v if item in valid_values] + return v + class LlmEnrichmentResult(BaseModel): enriched_words: list[LlmWordEnrichment] = Field(default_factory=list, description="List of enriched word data") From 323289aa740b447c9090f69160bba3da117e2db2 Mon Sep 17 00:00:00 2001 From: wumode Date: Fri, 16 Jan 2026 22:26:28 +0800 Subject: [PATCH 2/3] =?UTF-8?q?fix(ClashRuleProvider):=20=E8=A7=84?= =?UTF-8?q?=E5=88=99=E9=9B=86=E7=A6=81=E7=94=A8=E5=A4=B1=E6=95=88?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ....js => __federation_expose_Page-DhQfGEOD.js} | 17 ++++++++++++++++- .../dist/assets/remoteEntry.js | 2 +- plugins.v2/clashruleprovider/services.py | 6 +++--- 3 files changed, 20 insertions(+), 5 deletions(-) rename plugins.v2/clashruleprovider/dist/assets/{__federation_expose_Page-DeAFYy3o.js => __federation_expose_Page-DhQfGEOD.js} (99%) diff --git a/plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DeAFYy3o.js b/plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DhQfGEOD.js similarity index 99% rename from plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DeAFYy3o.js rename to plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DhQfGEOD.js index a6de46c..f1f8a6b 100644 --- a/plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DeAFYy3o.js +++ b/plugins.v2/clashruleprovider/dist/assets/__federation_expose_Page-DhQfGEOD.js @@ -6007,13 +6007,28 @@ const _sfc_main$q = /* @__PURE__ */ _defineComponent$q({ }, { default: _withCtx$q(() => [ _createVNode$q(_component_v_btn_group, { + class: "d-sm-none", + variant: "outlined", + rounded: "", + divided: "" + }, { + default: _withCtx$q(() => [ + _createVNode$q(_component_v_btn, { + icon: "mdi-plus", + disabled: loading.value, + onClick: openAddRuleDialog + }, null, 8, ["disabled"]) + ]), + _: 1 + }), + _createVNode$q(_component_v_btn_group, { + class: "d-none d-sm-flex", variant: "outlined", rounded: "", divided: "" }, { default: _withCtx$q(() => [ _createVNode$q(_component_v_btn, { - class: "d-none d-sm-flex", icon: group.value ? "mdi-format-list-bulleted" : "mdi-format-list-group", disabled: loading.value, onClick: _cache[2] || (_cache[2] = ($event) => group.value = !group.value) diff --git a/plugins.v2/clashruleprovider/dist/assets/remoteEntry.js b/plugins.v2/clashruleprovider/dist/assets/remoteEntry.js index 25bb6c7..64c2d34 100644 --- a/plugins.v2/clashruleprovider/dist/assets/remoteEntry.js +++ b/plugins.v2/clashruleprovider/dist/assets/remoteEntry.js @@ -3,7 +3,7 @@ const currentImports = {}; let moduleMap = { "./Page":()=>{ dynamicLoadingCss(["__federation_expose_Page-CJILOVp4.css"], false, './Page'); - return __federation_import('./__federation_expose_Page-DeAFYy3o.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)}, + return __federation_import('./__federation_expose_Page-DhQfGEOD.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)}, "./Config":()=>{ dynamicLoadingCss(["__federation_expose_Config-CwbjkOP2.css"], false, './Config'); return __federation_import('./__federation_expose_Config-CY46uj5g.js').then(module =>Object.keys(module).every(item => exportSet.has(item)) ? () => module.default : () => module)}, diff --git a/plugins.v2/clashruleprovider/services.py b/plugins.v2/clashruleprovider/services.py index 94aae6b..a4788c8 100644 --- a/plugins.v2/clashruleprovider/services.py +++ b/plugins.v2/clashruleprovider/services.py @@ -191,7 +191,7 @@ class ClashRuleProviderService: except ValueError: final_action = action rules = self.state.ruleset_rules_manager.filter_rules_by_action(final_action) - return [rule.rule.condition_string() for rule in rules] + return [rule.rule.condition_string() for rule in rules if rule.meta.available()] def sync_ruleset(self): outbounds = set() @@ -240,12 +240,12 @@ class ClashRuleProviderService: self.state.save_data(DataKey.TOP_RULES, self.state.top_rules_manager.export_rules()) def clash_outbound(self) -> list[str]: - outbound = [pg_data.data.name for pg_data in self.state.proxy_groups_from_subs()] + outbound = [pg.data.name for pg in self.state.proxy_groups] if self.state.clash_template: outbound.extend(pg.name for pg in self.state.clash_template.proxy_groups) if self.state.config.group_by_region or self.state.config.group_by_country: outbound.extend(pg.name for pg in self.proxy_groups_by_region()) - outbound.extend(pg.data.name for pg in self.state.proxy_groups) + outbound.extend(pg_data.data.name for pg_data in self.state.proxy_groups_from_subs()) outbound.extend(pg.data.name for pg in self.get_proxies()) return outbound From 0927d0388a6de744e7d379c839c56da611c4a255 Mon Sep 17 00:00:00 2001 From: wumode Date: Fri, 16 Jan 2026 15:32:28 +0800 Subject: [PATCH 3/3] feat(imdbsource): add production company filter and optimize year selection --- package.v2.json | 5 +- plugins.v2/imdbsource/__init__.py | 219 ++++++++++++++-------- plugins.v2/imdbsource/imdbapi.py | 23 ++- plugins.v2/imdbsource/imdbhelper.py | 7 +- plugins.v2/imdbsource/officialapi.py | 33 +++- plugins.v2/imdbsource/schema/__init__.py | 5 +- plugins.v2/imdbsource/schema/imdbapi.py | 17 ++ plugins.v2/imdbsource/schema/imdbtypes.py | 65 ++++++- 8 files changed, 289 insertions(+), 85 deletions(-) diff --git a/package.v2.json b/package.v2.json index 245c3bd..0172cf5 100644 --- a/package.v2.json +++ b/package.v2.json @@ -472,11 +472,12 @@ "name": "IMDb源", "description": "让探索,推荐和媒体识别支持IMDb数据源。", "labels": "探索", - "version": "1.6.6", + "version": "1.6.7", "icon": "IMDb_IOS-OSX_App.png", "author": "wumode", "level": 1, "history": { + "v1.6.7": "优化界面显示; 增加榜单排名显示; 添加制作公司过滤项", "v1.6.6": "优化主页组件链接跳转", "v1.6.5": "仪表盘组件支持图片缓存", "v1.6.4": "为元数据增加背景图", @@ -496,7 +497,7 @@ "v1.4.3": "为仪表盘组件添加缓存", "v1.4.2": "优化小屏幕组件显示", "v1.4.1": "优化亮色主题显示", - "v1.4.0":"添加仪表盘组件: IMDb 编辑精选", + "v1.4.0": "添加仪表盘组件: IMDb 编辑精选", "v1.3.3": "修复依赖问题", "v1.3.2": "更新 API query hash", "v1.3.1": "修复按日期排序错误", diff --git a/plugins.v2/imdbsource/__init__.py b/plugins.v2/imdbsource/__init__.py index 7a89fe3..456358e 100644 --- a/plugins.v2/imdbsource/__init__.py +++ b/plugins.v2/imdbsource/__init__.py @@ -34,7 +34,7 @@ class ImdbSource(_PluginBase): # 插件图标 plugin_icon = "IMDb_IOS-OSX_App.png" # 插件版本 - plugin_version = "1.6.6" + plugin_version = "1.6.7" # 插件作者 plugin_author = "wumode" # 作者主页 @@ -223,7 +223,7 @@ class ImdbSource(_PluginBase): height = 335 is_mobile = ImdbSource.is_mobile(kwargs.get('user_agent')) if is_mobile: - height *= 1.75 + height *= 1.80 # 全局配置 attrs = { "border": False @@ -320,7 +320,7 @@ class ImdbSource(_PluginBase): 'href': f"https://www.imdb.com/name/{cs.id}", 'target': '_blank', 'rel': 'noopener noreferrer', - 'class': 'text-h4 font-weight-bold mb-2 d-flex align-center', + 'class': 'text-h4 font-weight-bold mb-1 d-flex align-center', }, 'content': [ { @@ -344,7 +344,6 @@ class ImdbSource(_PluginBase): }, ] }, - { 'component': 'span', 'props': { @@ -361,6 +360,7 @@ class ImdbSource(_PluginBase): poster_url = next((f"{title.primary_image.url if title.primary_image else ''}" for title in titles if title.id == entry.ttconst), None) poster_url = f"{self._img_proxy_prefix}{quote(poster_url or '')}" + meter_ranking_url = imdb_title.meter_ranking.url if imdb_title.meter_ranking else None poster_com = { 'component': 'VImg', 'props': { @@ -375,9 +375,9 @@ class ImdbSource(_PluginBase): } poster_ui = { - 'component': 'div', + 'component': 'VRow', 'props': { - 'class': 'd-flex justify-center mt-2' + 'align': 'center' }, 'content': [ { @@ -394,11 +394,39 @@ class ImdbSource(_PluginBase): }, ] } - + meta_chips = [ + { + "component": "VChip", + "props": { + "append-icon": "mdi-trending-up", + "size": "small", + "href": meter_ranking_url, + "target": "_blank" + }, + "text": imdb_title.meter_ranking_text + }, + { + "component": "VChip", + "props": { + "size": "small", + }, + "text": imdb_title.title_type.text + }, + ] + if imdb_title.certificate_text: + meta_chips.append( + { + "component": "VChip", + "props": { + "size": "small" + }, + "text": imdb_title.certificate_text + } + ) rating_ui = { 'component': 'div', 'props': { - 'class': 'mb-2 d-flex align-center', + 'class': 'd-flex align-center mb-1', }, 'content': [ { @@ -418,21 +446,21 @@ class ImdbSource(_PluginBase): { 'component': 'span', 'props': { - 'class': 'text-body-2 ml-1', + 'class': 'text-truncate text-body-2 ml-1', 'style': 'color: rgba(231, 227, 252, 0.8);' }, - 'text': f"{imdb_title.rating_text}/10", + 'text': f"{imdb_title.rating_text}", }, ] }, { 'component': 'span', 'props': { - 'class': 'text-warning font-weight-bold ml-4', + 'class': 'text-truncate text-warning font-weight-bold ml-4', 'color': 'warning' }, 'text': entry.detail, - } + }, ] } @@ -455,7 +483,7 @@ class ImdbSource(_PluginBase): 'component': 'span', 'html': f"{entry.name}", 'props': { - 'class': 'line-clamp-2 overflow-hidden', + 'class': 'text-truncate overflow-hidden', } }, { @@ -468,6 +496,13 @@ class ImdbSource(_PluginBase): } ] }, + { + "component": 'div', + "props": { + "class": "d-flex align-center gap-1 mb-2", + }, + "content": meta_chips + }, rating_ui, { 'component': 'span', @@ -499,14 +534,16 @@ class ImdbSource(_PluginBase): { 'component': 'VCardText', 'props': { - 'class': 'd-flex flex-row absolute pa-4 text-white', + 'class': 'd-flex flex-row absolute pa-4 text-white h-100', 'style': 'z-index: 2; bottom: 0; max-width: 100%;', }, 'content': [ { 'component': 'VRow', 'props': { - 'class': 'w-100' + 'class': 'w-100', + 'align': "end", + 'align-md': "center" }, 'content': [ # 左图:海报 @@ -514,7 +551,8 @@ class ImdbSource(_PluginBase): 'component': 'VCol', 'props': { 'cols': 12, - 'md': 3 + 'md': 3, + 'class': 'd-flex justify-center align-center' }, 'content': [ poster_ui @@ -1057,16 +1095,17 @@ class ImdbSource(_PluginBase): return res async def imdb_discover(self, mtype: str = "series", - country: str = None, - lang: str = None, - genre: str = None, + country: str | None = None, + lang: str | None = None, + genre: str | None = None, + company: str | None = None, sort_by: str = 'POPULARITY', sort_order: str = 'DESC', using_rating: bool = False, user_rating: list[int] = Query(None, alias="user_rating[]"), - year: str = None, - award: str = None, - ranked_list: str = None, + year: str | None = None, + award: str | None = None, + ranked_list: str | None = None, page: int = 1) -> List[schemas.MediaInfo]: if not self._imdb_helper: @@ -1086,41 +1125,16 @@ class ImdbSource(_PluginBase): release_date_start = None release_date_end = None if year: - if year == "2025": - release_date_start = "2025-01-01" - elif year == "2024": - release_date_start = "2024-01-01" - release_date_end = "2024-12-31" - elif year == "2023": - release_date_start = "2023-01-01" - release_date_end = "2023-12-31" - elif year == "2022": - release_date_start = "2022-01-01" - release_date_end = "2022-12-31" - elif year == "2021": - release_date_start = "2021-01-01" - release_date_end = "2021-12-31" - elif year == "2020": - release_date_start = "2020-01-01" - release_date_end = "2020-12-31" - elif year == "2020s": - release_date_start = "2020-01-01" - release_date_end = "2029-12-31" - elif year == "2010s": - release_date_start = "2010-01-01" - release_date_end = "2019-12-31" - elif year == "2000s": - release_date_start = "2000-01-01" - release_date_end = "2009-12-31" - elif year == "1990s": - release_date_start = "1990-01-01" - release_date_end = "1999-12-31" - elif year == "1980s": - release_date_start = "1980-01-01" - release_date_end = "1989-12-31" - elif year == "1970s": - release_date_start = "1970-01-01" - release_date_end = "1979-12-31" + if year == f"{datetime.now().year}": + release_date_start = f"{datetime.now().year}-01-01" + elif year.endswith("s"): + decade = int(year[:-2]) + release_date_start = f"{decade}0-01-01" + release_date_end = f"{decade}9-12-31" + else: + release_date_start = f"{year}-01-01" + release_date_end = f"{year}-12-31" + if not release_date_end: release_date_end = datetime.now().date().strftime("%Y-%m-%d") if sort_by == 'POPULARITY': @@ -1142,7 +1156,8 @@ class ImdbSource(_PluginBase): release_date_end=release_date_end, release_date_start=release_date_start, award_constraint=awards, - ranked=ranked_lists + ranked=ranked_lists, + company=company ) results = await self._imdb_helper.async_advanced_title_search(search_params, first_page=first_page) res: List[schemas.MediaInfo] = [] @@ -1339,21 +1354,15 @@ class ImdbSource(_PluginBase): } for key, value in sort_order_dict.items() ] - year_dict = { - "2025": "2025", - "2024": "2024", - "2023": "2023", - "2022": "2022", - "2021": "2021", - "2020": "2020", + year_dict = {str(year): str(year) for year in range(datetime.now().year, 2019, -1)} + year_dict.update({ "2020s": "2020s", "2010s": "2010s", "2000s": "2000s", "1990s": "1990s", "1980s": "1980s", "1970s": "1970s", - } - + }) year_ui = [ { "component": "VChip", @@ -1394,12 +1403,12 @@ class ImdbSource(_PluginBase): ] ranked_list_dict = { - "TOP_RATED_MOVIES-100": "IMDb Top 100", - "TOP_RATED_MOVIES-250": "IMDb Top 250", - "TOP_RATED_MOVIES-1000": "IMDb Top 1000", - "LOWEST_RATED_MOVIES-100": "IMDb Bottom 100", - "LOWEST_RATED_MOVIES-250": "IMDb Bottom 250", - "LOWEST_RATED_MOVIES-1000": "IMDb Bottom 1000", + "TOP_RATED_MOVIES-100": "Top 100", + "TOP_RATED_MOVIES-250": "Top 250", + "TOP_RATED_MOVIES-1000": "Top 1000", + "LOWEST_RATED_MOVIES-100": "Bottom 100", + "LOWEST_RATED_MOVIES-250": "Bottom 250", + "LOWEST_RATED_MOVIES-1000": "Bottom 1000", } ranked_list_ui = [ @@ -1414,6 +1423,41 @@ class ImdbSource(_PluginBase): } for key, value in ranked_list_dict.items() ] + companies = { + "20th Century Fox": "20世纪福克斯", + "DreamWorks": "梦工厂", + "MGM": "米高梅", + "Paramount": "派拉蒙", + "Sony": "索尼", + "Universal": "环球", + "Walt Disney": "迪士尼", + "Warner Bros.": "华纳兄弟", + "HBO": "HBO", + "Netflix": "Netflix", + "Hulu": "Hulu", + "Amazon Prime Video": "Amazon Prime", + "Apple TV": "Apple TV", + "British Broadcasting Corporation (BBC)": "BBC", + "Tencent Video": "腾讯视频", + "Youku": "优酷", + "iQIYI": "爱奇艺", + "China Central Television (CCTV)": "CCTV", + "Huayi Brothers Media": "华谊兄弟", + "Beijing Enlight Pictures": "光线传媒", + "Bona Film Group": "博纳影业", + } + companies_ui = [ + { + "component": "VChip", + "props": { + "filter": True, + "tile": True, + "value": key + }, + "text": value + } for key, value in companies.items() + ] + return [ { "component": "div", @@ -1596,6 +1640,33 @@ class ImdbSource(_PluginBase): } ] }, + { + "component": "div", + "props": { + "class": "flex justify-start items-center", + }, + "content": [ + { + "component": "div", + "props": { + "class": "mr-5" + }, + "content": [ + { + "component": "VLabel", + "text": "出品方" + } + ] + }, + { + "component": "VChipGroup", + "props": { + "model": "company" + }, + "content": companies_ui + } + ] + }, { "component": "div", "props": { @@ -1750,7 +1821,7 @@ class ImdbSource(_PluginBase): "user_rating": [1, 10], "using_rating": False, "award": None, - "ranked_list": None + "ranked_list": None, }, depends={ "ranked_list": ["mtype"] diff --git a/plugins.v2/imdbsource/imdbapi.py b/plugins.v2/imdbsource/imdbapi.py index 7f4d301..4fb95c2 100644 --- a/plugins.v2/imdbsource/imdbapi.py +++ b/plugins.v2/imdbsource/imdbapi.py @@ -11,7 +11,7 @@ from app.utils.http import RequestUtils, AsyncRequestUtils from .schema.imdbapi import ImdbApiTitle, ImdbApiEpisode, ImdbApiCredit, ImdbapiImage from .schema.imdbapi import (ImdbApiSearchTitlesResponse, ImdbApiListTitlesResponse, ImdbApiListTitleEpisodesResponse, ImdbApiListTitleSeasonsResponse, ImdbApiListTitleCreditsResponse, - ImdbapiListTitleAKAsResponse, ImdbApiTitleImagesResponse) + ImdbapiListTitleAKAsResponse, ImdbApiTitleImagesResponse, ImdbapiCompanyCreditResponse) from .schema.imdbtypes import ImdbType @@ -769,3 +769,24 @@ class ImdbApiClient: page_token = response.next_page_token if not page_token: break + + async def company_credits(self, title_id: str, categories: list[str] | None = None + ) -> Optional[ImdbapiCompanyCreditResponse]: + """ + Retrieve the company credits associated with a specific title. + + :param title_id: Required. IMDb title ID in the format "tt1234567". + :param categories: Optional. The categories of company credit to filter by. + :return: Company Credits. + """ + path = "/titles/%s/companyCredits" + param: dict[str, Any] = {} + if categories: + param['categories'] = categories + try: + r = await self._async_free_imdb_api(path=path % title_id, params=param) + ret = ImdbapiCompanyCreditResponse.model_validate(r) + except Exception as e: + logger.debug(f"An error occurred while retrieving company credits: {e}") + return None + return ret diff --git a/plugins.v2/imdbsource/imdbhelper.py b/plugins.v2/imdbsource/imdbhelper.py index 138fd73..6099a49 100644 --- a/plugins.v2/imdbsource/imdbhelper.py +++ b/plugins.v2/imdbsource/imdbhelper.py @@ -113,7 +113,7 @@ class ImdbHelper: logger.error("Error getting staff picks") return None try: - data = StaffPickApiResponse.model_validate_json(res) + data = StaffPickApiResponse.model_validate_json(res, by_name=True) except (JSONDecodeError, ValidationError): return None return data @@ -210,7 +210,8 @@ class ImdbHelper: return key return "" - async def advanced_title_search_generator(self, params: SearchParams, first_page: bool = True) -> AsyncGenerator[TitleEdge, None]: + async def advanced_title_search_generator(self, params: SearchParams, first_page: bool = True + ) -> AsyncGenerator[TitleEdge, None]: await self._async_update_hash() sha256 = self._imdb_api_hash.advanced_title_search if not first_page and params in self._title_generators: @@ -253,7 +254,7 @@ class ImdbHelper: seasons_dict[s] = episode.release_date return seasons_dict - def match_by(self, name: str, mtype: Optional[MediaType] = None, year: Optional[str] = None) -> Optional[ImdbMediaInfo]: + def match_by(self, name: str, mtype: MediaType | None = None, year: str | None = None) -> ImdbMediaInfo | None: """ 根据名称同时查询电影和电视剧,没有类型也没有年份时使用 diff --git a/plugins.v2/imdbsource/officialapi.py b/plugins.v2/imdbsource/officialapi.py index 404ed8f..96aac39 100644 --- a/plugins.v2/imdbsource/officialapi.py +++ b/plugins.v2/imdbsource/officialapi.py @@ -275,10 +275,35 @@ INTERESTS_ID: Final[Dict[str, Dict[str, str]]] = { "Western Epic": "in0000189" } } + +COMPANY_ID = { + "20th Century Fox": ["co0000756", "co0176225", "co0201557", "co0017497"], + "DreamWorks": ["co0067641", "co0040938", "co0252576", "co0003158"], + "MGM": ["co0007143", "co0026841"], + "Paramount": ["co0023400"], + "Sony": ["co0050868", "co0026545", "co0121181"], + "Universal": ["co0005073", "co0055277", "co0042399"], + "Walt Disney": ["co0008970", "co0017902", "co0098836", "co0059516", "co0092035", "co0049348"], + "Warner Bros.": ["co0002663", "co0005035", "co0863266", "co0072876", "co0080422", "co0046718"], + "HBO": ["co0008693", "co0754095", "co0306346", "co0148466", "co0909975", "co0638197", "co0391378"], + "Netflix": ["co0144901", "co0805756"], + "Hulu": ["co0218858", "co0381648"], + "Amazon Prime Video": ["co0476953", "co1160313", "co0939864", "co0931938"], + "Apple TV": ["co0931939", "co0546168"], + "British Broadcasting Corporation (BBC)": ['co0043107'], + "Tencent Video": ["co0487058"], + "Youku": ["co0264223"], + "iQIYI": ["co0493506", "co0691262"], + "China Central Television (CCTV)": ['co0001524'], + "Huayi Brothers Media": ["co0099734"], + "Beijing Enlight Pictures": ["co0208796"], + "Bona Film Group": ["co0452101"], +} + CACHE_LIFETIME: Final[int] = 86400 IMDB_GRAPHQL_QUERY: Final[str] = dedent(""" query VerticalListPageItems( $titles: [ID!]! $names: [ID!]! $images: [ID!]! $videos: [ID!]!) { - titles(ids: $titles) { ...TitleParts meterRanking { currentRank meterType rankChange {changeDirection difference} } ratingsSummary { aggregateRating } } + titles(ids: $titles) { ...TitleParts meterRanking { currentRank meterType rankChange {changeDirection difference} } ratingsSummary { aggregateRating voteCount} } names(ids: $names) { ...NameParts } videos(ids: $videos) { ...VideoParts } images(ids: $images) { ...ImageParts } @@ -500,6 +525,12 @@ class OfficialApiClient: if in_id: constraints.append(in_id) variables["interestConstraint"] = {"allInterestIds": constraints, "excludeInterestIds": []} + + if params.company: + company_ids = COMPANY_ID.get(params.company) + if company_ids: + variables["creditedCompanyConstraint"] = {"anyCompanyIds": company_ids, "excludeCompanyIds": []} + if last_cursor: variables["after"] = last_cursor diff --git a/plugins.v2/imdbsource/schema/__init__.py b/plugins.v2/imdbsource/schema/__init__.py index c8b5396..fc90a90 100644 --- a/plugins.v2/imdbsource/schema/__init__.py +++ b/plugins.v2/imdbsource/schema/__init__.py @@ -13,11 +13,11 @@ class ErrorType(Enum): class StaffPickEntry(BaseModel): name: str - ttconst: str + ttconst: str = Field(..., alias='id') rmconst: str detail: Optional[str] = "" description: Optional[str] = "" - relatedconst: List[str] = Field(default_factory=list) + relatedconst: List[str] = Field(default_factory=list, alias='relatedConst') viconst: Optional[str] = None @@ -117,6 +117,7 @@ class SearchParams(BaseModel): award_constraint: Optional[Tuple[str, ...]] = None ranked: Optional[Tuple[str, ...]] = None interests: Optional[Tuple[str, ...]] = None + company: Optional[str] = None model_config = ConfigDict( frozen=True diff --git a/plugins.v2/imdbsource/schema/imdbapi.py b/plugins.v2/imdbsource/schema/imdbapi.py index 2bd1cc6..7cad74e 100644 --- a/plugins.v2/imdbsource/schema/imdbapi.py +++ b/plugins.v2/imdbsource/schema/imdbapi.py @@ -154,3 +154,20 @@ class ImdbapiListTitleAKAsResponse(BaseModel): class ImdbApiTitleImagesResponse(PagedResponse): images: List[ImdbapiImage] = Field(default_factory=list) + + +class ImdbapiCompany(BaseModel): + id: str + name: str + + +class ImdbapiCompanyCredit(BaseModel): + company: ImdbapiCompany + category: Optional[str] = Field( + default=None, + description="Category of the company credit, such as production, sales, distribution, etc." + ) + + +class ImdbapiCompanyCreditResponse(PagedResponse): + company_credits: List[ImdbapiCompanyCredit] = Field(default_factory=list, alias='companyCredits') diff --git a/plugins.v2/imdbsource/schema/imdbtypes.py b/plugins.v2/imdbsource/schema/imdbtypes.py index b3f0f5b..07e72aa 100644 --- a/plugins.v2/imdbsource/schema/imdbtypes.py +++ b/plugins.v2/imdbsource/schema/imdbtypes.py @@ -4,6 +4,15 @@ from typing import Optional, List from pydantic import BaseModel, Field +def format_number(n: int) -> str: + units = ["", "K", "M", "B", "T"] + idx = 0 + while n >= 1000 and idx < len(units) - 1: + n //= 1000 + idx += 1 + return f"{n}{units[idx]}" + + class ImdbType(Enum): TV_SERIES = "tvSeries" TV_MINI_SERIES = "tvMiniSeries" @@ -23,6 +32,25 @@ class ImdbType(Enum): class TitleType(BaseModel): id: ImdbType + @property + def text(self) -> str: + type_mapping = { + ImdbType.TV_SERIES: "TV Series", + ImdbType.TV_MINI_SERIES: "TV Mini Series", + ImdbType.MOVIE: "Movie", + ImdbType.TV_MOVIE: "TV Movie", + ImdbType.MUSIC_VIDEO: "Music Video", + ImdbType.TV_SHORT: "TV Short", + ImdbType.SHORT: "Short", + ImdbType.TV_EPISODE: "TV Episode", + ImdbType.TV_SPECIAL: "TV Special", + ImdbType.VIDEO_GAME: "Video Game", + ImdbType.VIDEO: "Video", + ImdbType.PODCAST_SERIES: "Podcast Series", + ImdbType.PODCAST_EPISODE: "Podcast Episode", + } + return type_mapping.get(self.id, "Unknown") + class ReleaseYear(BaseModel): year: Optional[int] = None @@ -89,6 +117,23 @@ class MeterRanking(BaseModel): meter_type: Optional[str] = Field(default=None, alias='meterType') rank_change: Optional[RankChange] = Field(default=None, alias='rankChange') + @property + def text(self) -> str: + if self.current_rank: + rank = self.current_rank + meter_rank = "" + if self.meter_type: + meter_rank = self.meter_type.replace("_", "").replace("METER", "Meter") + meter_rank = f" {meter_rank}" + return f"#{rank}{meter_rank}" + return "" + + @property + def url(self) -> str: + if self.current_rank and self.meter_type: + return f"https://www.imdb.com/chart/{self.meter_type.replace("_", "").lower()}/" + return "" + class RatingsSummary(BaseModel): aggregate_rating: Optional[float] = Field(default=None, alias='aggregateRating') @@ -154,8 +199,24 @@ class ImdbTitle(BaseModel): @property def rating_text(self) -> str: if self.ratings_summary and self.ratings_summary.aggregate_rating: - return f"{self.ratings_summary.aggregate_rating:.1f}" - return "-" + votes = "" + if self.ratings_summary.vote_count: + votes = f" ({format_number(self.ratings_summary.vote_count)})" + return f"{self.ratings_summary.aggregate_rating:.1f}{votes}" + return "-/10" + + @property + def meter_ranking_text(self) -> str: + if self.meter_ranking and self.meter_ranking.current_rank: + return self.meter_ranking.text + return "" + + @property + def certificate_text(self) -> str: + if self.certificate and self.certificate.rating: + return self.certificate.rating + return "" + class Thumbnail(BaseModel): url: str