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("标题和内容不能同时为空")