diff --git a/package.json b/package.json
index b03dfce..d508c17 100644
--- a/package.json
+++ b/package.json
@@ -810,13 +810,15 @@
"name": "ntfy消息推送",
"description": "支持使用ntfy发送消息通知。",
"labels": "消息通知",
- "version": "1.1",
+ "version": "1.3",
"icon": "Ntfy_A.png",
"author": "lethargicScribe",
"level": 1,
"v2": true,
"history": {
- "v1.1": "添加Token认证和用户动作"
+ "v1.1": "添加Token认证和用户动作",
+ "v1.2": "修复 ntfy 通知图标链接失效的问题",
+ "v1.3": "修复标题或文本为空时,通知发送失败的问题"
}
},
"GotifyMsg": {
@@ -847,7 +849,6 @@
"icon": "Macos_Sierra.png",
"author": "jxxghp",
"level": 1,
- "v2": true,
"history": {
"v1.4.1": "修复Bing壁纸命名问题",
"v1.3": "适配MoviePilot v2.5.3+版本",
@@ -1053,4 +1054,4 @@
"level": 1,
"v2": true
}
-}
\ No newline at end of file
+}
diff --git a/package.v2.json b/package.v2.json
index ccb6fb1..c5e770f 100644
--- a/package.v2.json
+++ b/package.v2.json
@@ -468,11 +468,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 查询错误重试问题",
@@ -546,11 +547,12 @@
"name": "美剧生词标注",
"description": "根据CEFR等级,为英语影视剧标注高级词汇。",
"labels": "英语",
- "version": "1.2.0",
+ "version": "1.2.1",
"icon": "LexiAnnot.png",
"author": "wumode",
"level": 1,
"history": {
+ "v1.2.1": "改进字幕样式获取方法",
"v1.2.0": "引入大模型候选词决策和词义丰富处理链; 支持读取系统智能体配置; 添加智能体工具; 优化通知样式; 改进 UI",
"v1.1.4": "优化字幕选择决策",
"v1.1.3": "适配 Pydantic V2 (主程序版本需高于 2.8.1-1)",
@@ -587,5 +589,17 @@
"v1.2": "优化上报信息量",
"v1.1": "加强脱敏处理"
}
+ },
+ "TmdbWallpaper": {
+ "name": "登录壁纸本地化",
+ "description": "将MoviePilot的登录壁纸下载到本地。",
+ "labels": "壁纸,本地化",
+ "version": "1.4.1",
+ "icon": "Macos_Sierra.png",
+ "author": "jxxghp",
+ "level": 1,
+ "history": {
+ "v1.4.1": "MoviePilot V2 版本登录壁纸本地化插件"
+ }
}
}
diff --git a/plugins.v2/doubanrank/__init__.py b/plugins.v2/doubanrank/__init__.py
index 60fe9f3..9ab92af 100644
--- a/plugins.v2/doubanrank/__init__.py
+++ b/plugins.v2/doubanrank/__init__.py
@@ -47,14 +47,14 @@ class DoubanRank(_PluginBase):
# 私有属性
_scheduler = None
_douban_address = {
- 'movie-ustop': 'https://rsshub.app/douban/movie/ustop',
- 'movie-weekly': 'https://rsshub.app/douban/movie/weekly',
- 'movie-real-time': 'https://rsshub.app/douban/movie/weekly/movie_real_time_hotest',
- 'show-domestic': 'https://rsshub.app/douban/movie/weekly/show_domestic',
- 'movie-hot-gaia': 'https://rsshub.app/douban/movie/weekly/movie_hot_gaia',
- 'tv-hot': 'https://rsshub.app/douban/movie/weekly/tv_hot',
- 'movie-top250': 'https://rsshub.app/douban/movie/weekly/movie_top250',
- 'movie-top250-full': 'https://rsshub.app/douban/list/movie_top250',
+ 'movie-ustop': '/douban/movie/ustop',
+ 'movie-weekly': '/douban/movie/weekly',
+ 'movie-real-time': '/douban/movie/weekly/movie_real_time_hotest',
+ 'show-domestic': '/douban/movie/weekly/show_domestic',
+ 'movie-hot-gaia': '/douban/movie/weekly/movie_hot_gaia',
+ 'tv-hot': '/douban/movie/weekly/tv_hot',
+ 'movie-top250': '/douban/movie/weekly/movie_top250',
+ 'movie-top250-full': '/douban/list/movie_top250',
}
_enabled = False
_cron = ""
@@ -65,6 +65,7 @@ class DoubanRank(_PluginBase):
_clear = False
_clearflag = False
_proxy = False
+ _rsshub = "https://rsshub.app"
def init_plugin(self, config: dict = None):
@@ -74,6 +75,7 @@ class DoubanRank(_PluginBase):
self._proxy = config.get("proxy")
self._onlyonce = config.get("onlyonce")
self._vote = float(config.get("vote")) if config.get("vote") else 0
+ self._rsshub = config.get("rsshub") or "https://rsshub.app"
rss_addrs = config.get("rss_addrs")
if rss_addrs:
if isinstance(rss_addrs, str):
@@ -237,7 +239,7 @@ class DoubanRank(_PluginBase):
'component': 'VCol',
'props': {
'cols': 12,
- 'md': 6
+ 'md': 4
},
'content': [
{
@@ -254,7 +256,7 @@ class DoubanRank(_PluginBase):
'component': 'VCol',
'props': {
'cols': 12,
- 'md': 6
+ 'md': 4
},
'content': [
{
@@ -266,6 +268,23 @@ class DoubanRank(_PluginBase):
}
}
]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VTextField',
+ 'props': {
+ 'model': 'rsshub',
+ 'label': 'RSSHub地址',
+ 'placeholder': 'https://rsshub.app'
+ }
+ }
+ ]
}
]
},
@@ -345,6 +364,7 @@ class DoubanRank(_PluginBase):
"proxy": False,
"onlyonce": False,
"vote": "",
+ "rsshub": "https://rsshub.app",
"ranks": [],
"rss_addrs": "",
"clear": False
@@ -508,6 +528,7 @@ class DoubanRank(_PluginBase):
"cron": self._cron,
"onlyonce": self._onlyonce,
"vote": self._vote,
+ "rsshub": self._rsshub,
"ranks": self._ranks,
"rss_addrs": '\n'.join(map(str, self._rss_addrs)),
"clear": self._clear
@@ -518,7 +539,10 @@ class DoubanRank(_PluginBase):
刷新RSS
"""
logger.info(f"开始刷新豆瓣榜单 ...")
- addr_list = self._rss_addrs + [self._douban_address.get(rank) for rank in self._ranks]
+ # 构建完整的RSS地址
+ rsshub_base = self._rsshub.rstrip('/')
+ rank_addrs = [f"{rsshub_base}{self._douban_address.get(rank)}" for rank in self._ranks if self._douban_address.get(rank)]
+ addr_list = self._rss_addrs + rank_addrs
if not addr_list:
logger.info(f"未设置榜单RSS地址")
return
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
diff --git a/plugins.v2/lexiannot/__init__.py b/plugins.v2/lexiannot/__init__.py
index 4034033..98791a2 100644
--- a/plugins.v2/lexiannot/__init__.py
+++ b/plugins.v2/lexiannot/__init__.py
@@ -61,7 +61,7 @@ class LexiAnnot(_PluginBase):
# 插件图标
plugin_icon = "LexiAnnot.png"
# 插件版本
- plugin_version = "1.2.0"
+ plugin_version = "1.2.1"
# 插件作者
plugin_author = "wumode"
# 作者主页
@@ -756,6 +756,7 @@ class LexiAnnot(_PluginBase):
{"title": "0.3", "value": "0.3"},
{"title": "0.4", "value": "0.4"},
{"title": "0.5", "value": "0.5"},
+ {"title": "1.0", "value": "1.0"},
],
},
}
@@ -887,7 +888,7 @@ class LexiAnnot(_PluginBase):
"ffmpeg_path": "",
"english_only": True,
"when_file_trans": True,
- "model_temperature": "0.1",
+ "model_temperature": "0.3",
"custom_files": "",
"accent_color": "",
"font_scaling": "1",
@@ -1339,6 +1340,7 @@ class LexiAnnot(_PluginBase):
)
ret_message = ""
stat = None
+ ret_status: TaskStatus = TaskStatus.FAILED
if embedded_subtitles:
logger.info(f"提取到 {len(embedded_subtitles)} 条英语文本字幕")
for embedded_subtitle in embedded_subtitles:
@@ -1364,10 +1366,11 @@ class LexiAnnot(_PluginBase):
ass_subtitle.save(str(ass_file))
ret_message = "字幕已保存"
logger.info(f"字幕已保存:{str(ass_file)}")
+ ret_status = TaskStatus.COMPLETED
+ break
except Exception as e:
ret_message = f"字幕文件 {ass_file} 保存失败"
logger.error(f"字幕文件 {ass_file} 保存失败, {e}")
- break
else:
logger.info(
f"处理字幕{embedded_subtitle['codec_id']}-{embedded_subtitle['stream_id']}失败"
@@ -1378,7 +1381,7 @@ class LexiAnnot(_PluginBase):
ret_message = "未能找到可提取的英文字幕"
logger.info(f"✅ Finished: {path}")
- return ProcessResult(status=TaskStatus.COMPLETED, message=ret_message, statistics=stat)
+ return ProcessResult(status=ret_status, message=ret_message, statistics=stat)
@cached(maxsize=1, ttl=1800)
def __load_lexicon_version(self) -> Optional[str]:
@@ -1513,13 +1516,8 @@ class LexiAnnot(_PluginBase):
mediainfo: MediaInfo | None = event_info.get("mediainfo")
if self._english_only and mediainfo:
- if mediainfo.original_language and mediainfo.original_language not in {
- "en",
- "eng",
- }:
- logger.info(
- f"原始语言 ({mediainfo.original_language}) 不为英语, 跳过 {mediainfo.title}: "
- )
+ if mediainfo.original_language and mediainfo.original_language not in {"en","eng"}:
+ logger.info(f"原始语言 ({mediainfo.original_language}) 不为英语, 跳过 {mediainfo.title}: ")
return
for new_path in transfer_info.file_list_new or []:
self.add_media_file(new_path)
@@ -1537,10 +1535,7 @@ class LexiAnnot(_PluginBase):
new_list = []
replacements.sort(key=lambda x: x["end"] - x["start"], reverse=True)
for r in replacements:
- if any(
- (r["start"] >= new["start"] and r["end"] <= new["end"])
- for new in new_list
- ):
+ if any((r["start"] >= new["start"] and r["end"] <= new["end"]) for new in new_list):
continue
new_list.append(r)
return new_list
@@ -1591,12 +1586,21 @@ class LexiAnnot(_PluginBase):
@staticmethod
def analyze_ass_language(ass_file: SSAFile):
+
+ def _replace_with_spaces(_text):
+ """
+ 使用等长的空格替换文本中的 (xxx) 模式。
+ 例如:"(Hi)" 会被替换成 " " (4个空格)
+ """
+ pattern = r"(\([^()]*\)|\[[^\[\]]*\])"
+ return re.sub(pattern, lambda match: " " * len(match.group(1)), _text)
+
styles = {}
for style in ass_file.styles:
styles[style] = {"text": [], "duration": 0, "text_size": 0, "times": 0}
for dialogue in ass_file:
style = dialogue.style
- text = dialogue.plaintext
+ text = _replace_with_spaces(dialogue.plaintext)
sub_text = text.split("\n")
if style not in styles or not text:
continue
@@ -1638,13 +1642,11 @@ class LexiAnnot(_PluginBase):
return style_language_analysis
@staticmethod
- def select_main_style_weighted(
- language_analysis: Dict[str, Any], known_language: str, weights=None
- ):
+ def select_main_style_weighted(analysis: Dict[str, Any], known_language: str, weights = None):
"""
根据语言分析结果和已知的字幕语言,使用加权评分选择主要样式
- :params language_analysis: `analyze_ass_language` 函数的输出结果
+ :params analysis: `analyze_ass_language` 函数的输出结果
:params known_language: 已知的字幕语言代码
:params weights: 各个维度的权重,权重之和应为 1
:returns: 主要字幕的样式名称,如果没有匹配的样式则返回 None
@@ -1652,20 +1654,10 @@ class LexiAnnot(_PluginBase):
if weights is None:
weights = {"times": 0.5, "text_size": 0.4, "duration": 0.1}
matching_styles = []
- max_times = max([analysis.get("times", 0) for _, analysis in language_analysis.items() if analysis]) or 1
- max_text_size = (
- max([analysis.get("text_size", 0) for _, analysis in language_analysis.items() if analysis]) or 1)
- max_duration = (
- max(
- [
- analysis.get("duration", 0)
- for _, analysis in language_analysis.items()
- if analysis
- ]
- )
- or 1
- )
- for style, analysis in language_analysis.items():
+ max_times = max([analysis.get("times", 0) for _, analysis in analysis.items() if analysis] or [0]) or 1
+ max_text_size = max([analysis.get("text_size", 0) for _, analysis in analysis.items() if analysis] or [0]) or 1
+ max_duration = max([analysis.get("duration", 0) for _, analysis in analysis.items() if analysis] or [0]) or 1
+ for style, analysis in analysis.items():
if not analysis:
continue
if analysis.get("main_language") == known_language:
@@ -1898,7 +1890,7 @@ class LexiAnnot(_PluginBase):
)
)
- # model_temperature = float(self._model_temperature) if self._model_temperature else 0.1
+ model_temperature = float(self._model_temperature) if self._model_temperature else 0.3
logger.info("通过 spaCy 分词...")
for seg in segments:
if self._shutdown_event.is_set():
@@ -1925,7 +1917,7 @@ class LexiAnnot(_PluginBase):
model_name=llm_model_name,
base_url=llm_base_url,
api_key=llm_apikey,
- temperature=self._model_temperature,
+ temperature=model_temperature,
max_retries=self._max_retries,
proxy=self._use_proxy,
)
@@ -1958,9 +1950,7 @@ class LexiAnnot(_PluginBase):
) # &H00FFFFFF&
statistical_res = LexiAnnot.analyze_ass_language(ass_file)
- main_style: str | None = LexiAnnot.select_main_style_weighted(
- statistical_res, lang
- )
+ main_style: str | None = LexiAnnot.select_main_style_weighted(statistical_res, lang)
if not main_style:
logger.error("无法确定主要字幕样式")
return None, None
@@ -1996,16 +1986,8 @@ class LexiAnnot(_PluginBase):
dialogue.start = main_processor[seg.index].start
dialogue.end = main_processor[seg.index].end
dialogue.style = "Annotation EN"
- cefr_text = (
- f" {style_text('Annotation CEFR', word.cefr)}"
- if word.cefr
- else ""
- )
- exam_text = (
- f" {style_text('Annotation EXAM', ' '.join(exams))}"
- if exams
- else ""
- )
+ cefr_text = f" {style_text('Annotation CEFR', word.cefr)}" if word.cefr else ""
+ exam_text = f" {style_text('Annotation EXAM', ' '.join(exams))}" if exams else ""
phone_text = (
f"{__N}{style_text('Annotation PHONE', f'/{word.phonetics}/')}"
if word.phonetics and self._show_phonetics
@@ -2050,10 +2032,10 @@ class LexiAnnot(_PluginBase):
)
if self._sentence_translation:
chinese = seg.Chinese
- if chinese and chinese[-1] in ["。", ","]:
+ if chinese and chinese[-1] in {"。", ","}:
chinese = chinese[:-1]
main_processor[seg.index].text = (
- main_processor[seg.index].text + f"\\N{{\\fs{int(main_style_fs * 0.75)}}}{chinese}{{\\r}}"
+ main_processor[seg.index].text + f"\\N{{\\fs{int(main_style_fs * 0.75)}}}{chinese}{{\\r}}"
)
# 避免 Infuse 显示乱码
diff --git a/plugins.v2/lexiannot/agenttool.py b/plugins.v2/lexiannot/agenttool.py
index abdf4d2..21b540a 100644
--- a/plugins.v2/lexiannot/agenttool.py
+++ b/plugins.v2/lexiannot/agenttool.py
@@ -14,9 +14,7 @@ class VocabularyAnnotatingTool(MoviePilotTool):
# 工具名称
name: str = "vocabulary_annotating_tool"
# 工具描述
- description: str = (
- "Add new vocabulary annotation task to plugin LexiAnnot's task queue."
- )
+ description: str = "Add new vocabulary annotation task to plugin LexiAnnot's task queue."
# 输入参数模型
args_schema: Type[BaseModel] = VocabularyAnnotatingToolInput
@@ -74,9 +72,7 @@ class QueryAnnotationTasksTool(MoviePilotTool):
# 工具名称
name: str = "query_annotation_tasks_tool"
# 工具描述
- description: str = (
- "Query the latest vocabulary annotation tasks from plugin LexiAnnot."
- )
+ description: str = "Query the latest vocabulary annotation tasks from plugin LexiAnnot."
# 输入参数模型
args_schema: Type[BaseModel] = QueryAnnotationTasksToolInput
diff --git a/plugins.v2/lexiannot/pipeline.py b/plugins.v2/lexiannot/pipeline.py
index 6aac3e3..c328886 100644
--- a/plugins.v2/lexiannot/pipeline.py
+++ b/plugins.v2/lexiannot/pipeline.py
@@ -500,7 +500,7 @@ Your goal is to identify **only** content that helps them reach native-level pro
* Avoid repeating words already listed in `candidate_words`.
* Must exist in the exact form in `context_text`.
* Provide lemma and POS.
- * **Do NOT include** simple high-frequency words, common fillers ('gonna', 'gotta'), or basic swear words.
+ * **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.
diff --git a/plugins.v2/tmdbwallpaper/__init__.py b/plugins.v2/tmdbwallpaper/__init__.py
new file mode 100644
index 0000000..0b9f324
--- /dev/null
+++ b/plugins.v2/tmdbwallpaper/__init__.py
@@ -0,0 +1,256 @@
+from datetime import datetime, timedelta
+from pathlib import Path
+from typing import Any, List, Dict, Tuple
+from urllib.parse import urlparse, parse_qs
+
+import pytz
+from apscheduler.schedulers.background import BackgroundScheduler
+
+from app.core.config import settings
+from app.helper.wallpaper import WallpaperHelper
+from app.log import logger
+from app.plugins import _PluginBase
+from app.utils.http import RequestUtils
+
+
+class TmdbWallpaper(_PluginBase):
+ # 插件名称
+ plugin_name = "登录壁纸本地化"
+ # 插件描述
+ plugin_desc = "将MoviePilot的登录壁纸下载到本地。"
+ # 插件图标
+ plugin_icon = "Macos_Sierra.png"
+ # 插件版本
+ plugin_version = "1.4.1"
+ # 插件作者
+ plugin_author = "jxxghp"
+ # 作者主页
+ author_url = "https://github.com/jxxghp"
+ # 插件配置项ID前缀
+ plugin_config_prefix = "tmdbwallpaper_"
+ # 加载顺序
+ plugin_order = 99
+ # 可使用的用户级别
+ auth_level = 1
+
+ # 私有属性
+ _hours = None
+ _savepath = None
+ _enabled = False
+ _onlyonce = False
+ _scheduler = None
+
+ def init_plugin(self, config: dict = None):
+ if config:
+ self._enabled = config.get("enabled")
+ self._hours = int(config.get("hours")) if config.get("hours") else None
+ self._savepath = config.get('savepath')
+ self._onlyonce = config.get("onlyonce")
+ if self._enabled or self._onlyonce:
+ savepath = Path(self._savepath)
+ if self._savepath and not savepath.exists():
+ logger.info(f"创建保存目录:{self._savepath}")
+ savepath.mkdir(parents=True, exist_ok=True)
+ # 立即运行一次
+ if self._onlyonce:
+ # 定时服务
+ self._scheduler = BackgroundScheduler(timezone=settings.TZ)
+ logger.info(f"登录壁纸本地化服务启动,立即运行一次")
+ self._scheduler.add_job(self.wallpaper_local, 'date',
+ run_date=datetime.now(
+ tz=pytz.timezone(settings.TZ)) + timedelta(seconds=3)
+ )
+ # 关闭一次性开关
+ self._onlyonce = False
+
+ # 保存配置
+ self.update_config({
+ "enabled": self._enabled,
+ "hours": self._hours,
+ "savepath": self._savepath,
+ "onlyonce": self._onlyonce
+ })
+ if self._scheduler.get_jobs():
+ # 启动服务
+ self._scheduler.print_jobs()
+ self._scheduler.start()
+
+ def get_state(self) -> bool:
+ return True if self._enabled and self._hours and self._savepath else False
+
+ @staticmethod
+ def get_command() -> List[Dict[str, Any]]:
+ pass
+
+ def get_api(self) -> List[Dict[str, Any]]:
+ pass
+
+ def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
+ """
+ 拼装插件配置页面,需要返回两块数据:1、页面配置;2、数据结构
+ """
+ return [
+ {
+ 'component': 'VForm',
+ 'content': [
+ {
+ 'component': 'VRow',
+ 'content': [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 6
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'enabled',
+ 'label': '启用插件',
+ }
+ }
+ ]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VSwitch',
+ 'props': {
+ 'model': 'onlyonce',
+ 'label': '立即运行一次',
+ }
+ }
+ ]
+ }
+ ]
+ },
+ {
+ 'component': 'VRow',
+ 'content': [
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 4
+ },
+ 'content': [
+ {
+ 'component': 'VTextField',
+ 'props': {
+ 'model': 'hours',
+ 'label': '更新频率(小时)',
+ 'placeholder': '1'
+ }
+ }
+ ]
+ },
+ {
+ 'component': 'VCol',
+ 'props': {
+ 'cols': 12,
+ 'md': 8
+ },
+ 'content': [
+ {
+ 'component': 'VTextField',
+ 'props': {
+ 'model': 'savepath',
+ 'label': '保存路径',
+ 'placeholder': '/config/wallpapers'
+ }
+ }
+ ]
+ }
+ ]
+ },
+ ]
+ }
+ ], {
+ "enabled": False,
+ "hours": 1,
+ "savepath": "/config/wallpapers"
+ }
+
+ def get_page(self) -> List[dict]:
+ pass
+
+ def get_service(self) -> List[Dict[str, Any]]:
+ """
+ 注册插件公共服务
+ [{
+ "id": "服务ID",
+ "name": "服务名称",
+ "trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()",
+ "func": self.xxx,
+ "kwargs": {} # 定时器参数
+ }]
+ """
+ if self.get_state():
+ return [{
+ "id": "TmdbWallpaper",
+ "name": "登录壁纸本地化服务",
+ "trigger": "interval",
+ "func": self.wallpaper_local,
+ "kwargs": {
+ "minutes": self._hours * 60
+ }
+ }]
+ return []
+
+ def stop_service(self):
+ """
+ 退出插件
+ """
+ try:
+ if self._scheduler:
+ self._scheduler.remove_all_jobs()
+ if self._scheduler.running:
+ self._scheduler.shutdown()
+ self._scheduler = None
+ except Exception as e:
+ print(str(e))
+
+ def wallpaper_local(self):
+ """
+ 下载MoviePilot的登录壁纸到本地
+ """
+
+ def __save_file(_url: str, _filename: str):
+ """
+ 保存文件
+ """
+ try:
+ savepath = Path(self._savepath)
+ logger.info(f"下载壁纸:{_url}")
+ r = RequestUtils().get_res(_url)
+ if r and r.status_code == 200:
+ with open(savepath / _filename, "wb") as f:
+ f.write(r.content)
+ except Exception as e:
+ logger.error(f"下载壁纸失败:{str(e)}")
+
+ if not self._savepath:
+ return
+ urls = WallpaperHelper().get_wallpapers(10) or []
+ for url in urls:
+ if settings.WALLPAPER == "tmdb":
+ filename = url.split("/")[-1]
+ elif settings.WALLPAPER == "bing":
+ # 解析url参数,获取id的值
+ parsed_url = urlparse(url)
+ query_params = parse_qs(parsed_url.query)
+ param_value = query_params.get("id")
+ filename = param_value[0] if param_value else None
+ else:
+ # 其他壁纸类型,直接使用url的文件名hash
+ filename = url.split("/")[-1]
+ # 没有后缀的文件名,添加.jpg后缀
+ if not filename.endswith(".jpg"):
+ filename += ".jpg"
+ __save_file(url, filename)
diff --git a/plugins/ntfymsg/__init__.py b/plugins/ntfymsg/__init__.py
index 12af771..8105bc3 100644
--- a/plugins/ntfymsg/__init__.py
+++ b/plugins/ntfymsg/__init__.py
@@ -16,7 +16,7 @@ class NtfyClient:
headers = {
"Title": title.encode(encoding='utf-8'),
"Markdown": "true" if format_as_markdown else "false",
- "Icon": "https://movie-pilot.org/images/logo.png",
+ "Icon": "https://cdn.jsdelivr.net/gh/jxxghp/MoviePilot-Frontend@v2/public/logo.png",
}
if self._token:
@@ -62,7 +62,7 @@ class NtfyMsg(_PluginBase):
# 插件图标
plugin_icon = "Ntfy_A.png"
# 插件版本
- plugin_version = "1.1"
+ plugin_version = "1.3"
# 插件作者
plugin_author = "lethargicScribe"
# 作者主页
@@ -353,9 +353,9 @@ class NtfyMsg(_PluginBase):
# 类型
msg_type: NotificationType = msg_body.get("type")
# 标题
- title = msg_body.get("title")
+ title = msg_body.get("title") or "\u200b"
# 文本
- text = msg_body.get("text")
+ text = msg_body.get("text") or "\u200b"
if not title and not text:
logger.warn("标题和内容不能同时为空")