diff --git a/package.v2.json b/package.v2.json
index 1adf0d7..6f9f8cd 100644
--- a/package.v2.json
+++ b/package.v2.json
@@ -465,11 +465,12 @@
"name": "IMDb源",
"description": "让探索,推荐和媒体识别支持IMDb数据源。",
"labels": "探索",
- "version": "1.6.4",
+ "version": "1.6.5",
"icon": "IMDb_IOS-OSX_App.png",
"author": "wumode",
"level": 1,
"history": {
+ "v1.6.5": "仪表盘组件支持图片缓存",
"v1.6.4": "为元数据增加背景图",
"v1.6.3": "优化媒体识别速度; 适配 Pydantic V2 (主程序版本需高于 2.8.1-1)",
"v1.6.2": "修复 API 查询错误重试问题",
diff --git a/plugins.v2/imdbsource/__init__.py b/plugins.v2/imdbsource/__init__.py
index 5c09aee..cf444d4 100644
--- a/plugins.v2/imdbsource/__init__.py
+++ b/plugins.v2/imdbsource/__init__.py
@@ -2,6 +2,7 @@ import re
import urllib.parse
from datetime import datetime
from typing import Any, Callable, Coroutine, Dict, Optional, List, Tuple
+from urllib.parse import quote
import zhconv
from apscheduler.triggers.cron import CronTrigger
@@ -20,7 +21,8 @@ from app.plugins.imdbsource.officialapi import INTERESTS_ID
from app.plugins.imdbsource.schema import StaffPickEntry, ImdbTitle, StaffPickApiResponse, ImdbMediaInfo, SearchParams
from app.log import logger
from app.schemas import DiscoverSourceEventData, MediaRecognizeConvertEventData, RecommendSourceEventData
-from app.schemas.types import ChainEventType, MediaType
+from app.schemas.types import ChainEventType, MediaType, EventType
+from app.scheduler import Scheduler
from app.utils.http import AsyncRequestUtils, RequestUtils
@@ -32,7 +34,7 @@ class ImdbSource(_PluginBase):
# 插件图标
plugin_icon = "IMDb_IOS-OSX_App.png"
# 插件版本
- plugin_version = "1.6.4"
+ plugin_version = "1.6.5"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -57,7 +59,7 @@ class ImdbSource(_PluginBase):
# 私有属性
_imdb_helper: ImdbHelper = None
- _img_proxy_prefix: str = ''
+ _img_proxy_prefix: str = '/api/v1/system/cache/image?url='
_original_method: Optional[Callable] = None
_original_async_method: Optional[Callable[..., Coroutine[Any, Any, Optional[MediaInfo]]]] = None
_staff_picks_cache: Optional[StaffPickApiResponse] = None
@@ -134,7 +136,6 @@ class ImdbSource(_PluginBase):
if "media-imdb.com" not in settings.SECURITY_IMAGE_DOMAINS:
settings.SECURITY_IMAGE_DOMAINS.append("media-imdb.com")
if self._enabled:
-
if self._recognize_media and self._recognition_mode == 'auxiliary':
# 替换 ChainBase.recognize_media
if not (getattr(ChainBase.recognize_media, "_patched_by", object()) == id(self)):
@@ -203,15 +204,11 @@ class ImdbSource(_PluginBase):
if not self._staff_picks:
return None
- def year_and_type(imdb_entry: StaffPickEntry, imdb_titles: List[ImdbTitle]) -> Tuple[MediaType, str | None, str | None]:
- title = next((t for t in imdb_titles if t.id == imdb_entry.ttconst), None)
- if not title:
- return MediaType.MOVIE, datetime.now().date().strftime("%Y"), ''
+ def year_and_type(title: ImdbTitle) -> Tuple[MediaType, str | None]:
media_id = title.title_type.id
release_year = f"{title.release_year.year}" if title.release_year else datetime.now().date().strftime("%Y")
media_type = ImdbHelper.type_to_mtype(media_id.value)
- media_plot = title.plot.plot_text.plain_text if title.plot and title.plot.plot_text else ''
- return media_type, release_year, media_plot
+ return media_type, release_year
# 列配置
size_config = {
@@ -264,12 +261,15 @@ class ImdbSource(_PluginBase):
titles = imdb_items.titles
contents = []
for entry in entries:
+ imdb_title = next((t for t in titles if t.id == entry.ttconst), None)
+ if not imdb_title:
+ continue
cast = [name for related in entry.relatedconst for name in names if name.id == related]
- mtype, year, plot = year_and_type(entry, titles)
+ mtype, year = year_and_type(imdb_title)
mp_url = f"/media?mediaid=imdb:{entry.ttconst}&title={entry.name}&year={year}&type={mtype.value}"
primary_img_url = next((f"{image.url}" for image in images
if image.id == entry.rmconst), '')
- primary_img_url = f'{self._img_proxy_prefix}{primary_img_url}'
+ primary_img_url = f'{self._img_proxy_prefix}{quote(primary_img_url)}'
item1 = {
'component': 'VCarouselItem',
'props': {
@@ -290,19 +290,20 @@ class ImdbSource(_PluginBase):
'to': mp_url,
'class': 'no-underline'
},
- 'content': [{
- 'component': 'h1',
- 'props': {
- 'class': 'mb-1 text-white text-shadow font-extrabold text-2xl line-clamp-2 overflow-hidden text-ellipsis ...'
+ 'content': [
+ {
+ 'component': 'h1',
+ 'props': {
+ 'class': 'mb-1 text-white text-shadow font-extrabold text-2xl line-clamp-2 overflow-hidden text-ellipsis ...'
+ },
+ 'html': f"{entry.name} {year}",
},
- 'html': f"{entry.name} {year_and_type(entry, titles)[1]}",
- },
{
'component': 'span',
'props': {
'class': 'text-shadow line-clamp-2 overflow-hidden text-ellipsis ...'
},
- 'html': plot,
+ 'html': imdb_title.plot_text,
}
]
},
@@ -334,15 +335,16 @@ class ImdbSource(_PluginBase):
{
'component': 'VAvatar',
'props': {
- 'size': f'{48 if is_mobile else 64}',
- 'class': 'mb-1'
+ 'size': f'{54 if is_mobile else 64}',
+ 'class': 'mb-1 hover-card',
},
'content': [
{
'component': 'VImg',
'props': {
'src': f"{self._img_proxy_prefix}"
- f"{cs.primary_image.url if cs.primary_image else ''}",
+ f"{quote(cs.primary_image.url
+ if cs.primary_image else '')}",
'alt': cs.name_text.text,
'cover': True
}
@@ -367,14 +369,14 @@ 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}{poster_url}"
+ poster_url = f"{self._img_proxy_prefix}{quote(poster_url or '')}"
poster_com = {
'component': 'VImg',
'props': {
'src': poster_url,
'alt': '海报',
'cover': True,
- 'class': 'rounded',
+ 'class': 'rounded hover-poster',
'max-width': '160',
'max-height': '240',
'style': 'height: auto; aspect-ratio: 2/3;',
@@ -395,8 +397,49 @@ class ImdbSource(_PluginBase):
'style': 'display: flex; justify-content: center;'
},
'content': [
- poster_com
+ poster_com,
]
+ },
+ ]
+ }
+
+ rating_ui = {
+ 'component': 'div',
+ 'props': {
+ 'class': 'mb-2 d-flex align-center',
+ },
+ 'content': [
+ {
+ 'component': 'div',
+ 'props': {
+ 'class': 'd-flex align-center',
+ },
+ 'content': [
+ {
+ 'component': 'VIcon',
+ 'props': {
+ 'color': 'warning',
+ 'size': 16
+ },
+ 'text': 'mdi-star'
+ },
+ {
+ 'component': 'span',
+ 'props': {
+ 'class': 'text-body-2 ml-1',
+ 'style': 'color: rgba(231, 227, 252, 0.8);'
+ },
+ 'text': f"{imdb_title.rating_text}/10",
+ },
+ ]
+ },
+ {
+ 'component': 'span',
+ 'props': {
+ 'class': 'text-warning font-weight-bold ml-4',
+ 'color': 'warning'
+ },
+ 'text': entry.detail,
}
]
}
@@ -431,13 +474,7 @@ class ImdbSource(_PluginBase):
}
]
},
- {
- 'component': 'div',
- 'props': {
- 'class': 'text-yellow font-weight-bold mb-2',
- },
- 'html': entry.detail
- },
+ rating_ui,
{
'component': 'span',
'props': {
@@ -510,7 +547,33 @@ class ImdbSource(_PluginBase):
contents.append(item1)
contents.append(item2)
+ style = {
+ 'component': 'style',
+ 'text': """
+.hover-card {
+ border: 2px solid transparent;
+ transition: border-color 0.3s ease-in-out;
+ box-sizing: border-box;
+}
+.hover-card:hover {
+ border-color: #ff8400;
+ cursor: pointer;
+}
+.hover-poster {
+ box-shadow: 0 4px 6px rgba(0,0,0,0.3);
+ transition: all 0.3s ease;
+ backface-visibility: hidden;
+}
+.hover-poster:hover {
+ transform: translateY(-6px);
+ box-shadow: 0 20px 25px -5px rgba(0,0,0,0.4),
+ 0 10px 10px -5px rgba(0,0,0,0.2);
+ cursor: pointer;
+}
+"""
+ }
elements = [
+ style,
{
'component': 'VCard',
'props': {
@@ -1988,3 +2051,13 @@ class ImdbSource(_PluginBase):
if not data:
return None
return ImdbSource._match_results(data, media_info)
+
+ @eventmanager.register(EventType.PluginReload)
+ def reload(self, event):
+ """
+ 响应插件重载事件
+ """
+ plugin_id = event.event_data.get("plugin_id")
+
+ if plugin_id == self.__class__.__name__:
+ Scheduler().update_plugin_job(plugin_id)
diff --git a/plugins.v2/imdbsource/schema/imdbtypes.py b/plugins.v2/imdbsource/schema/imdbtypes.py
index 6bbc2ee..b3f0f5b 100644
--- a/plugins.v2/imdbsource/schema/imdbtypes.py
+++ b/plugins.v2/imdbsource/schema/imdbtypes.py
@@ -147,6 +147,15 @@ class ImdbTitle(BaseModel):
original_title_text: Optional[TextField] = Field(default=None, alias='originalTitleText')
runtime: Optional[SecondsField] = Field(default=None, alias='runtime')
+ @property
+ def plot_text(self) -> str:
+ return self.plot.plot_text.plain_text if self.plot and self.plot.plot_text else ''
+
+ @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 "-"
class Thumbnail(BaseModel):
url: str