Merge pull request #977 from wumode/imdbsource

This commit is contained in:
jxxghp
2026-01-21 21:35:14 +08:00
committed by GitHub
14 changed files with 324 additions and 106 deletions

View File

@@ -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": "修复按日期排序错误",
@@ -554,11 +555,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",

View File

@@ -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)

View File

@@ -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)},

View File

@@ -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

View File

@@ -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"]

View File

@@ -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

View File

@@ -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:
"""
根据名称同时查询电影和电视剧,没有类型也没有年份时使用

View File

@@ -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

View File

@@ -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

View File

@@ -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')

View File

@@ -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

View File

@@ -60,7 +60,7 @@ class LexiAnnot(_PluginBase):
# 插件图标
plugin_icon = "LexiAnnot.png"
# 插件版本
plugin_version = "1.2.3"
plugin_version = "1.2.4"
# 插件作者
plugin_author = "wumode"
# 作者主页

View File

@@ -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}
""",
),

View File

@@ -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")