mirror of
https://github.com/jxxghp/MoviePilot-Plugins.git
synced 2026-03-27 10:05:57 +00:00
Merge branch 'main' of https://github.com/Seed680/MoviePilot-Plugins-main
This commit is contained in:
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 版本登录壁纸本地化插件"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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} <span class='text-base font-normal'>{year}</span>",
|
||||
},
|
||||
'html': f"{entry.name} <span class='text-base font-normal'>{year_and_type(entry, titles)[1]}</span>",
|
||||
},
|
||||
{
|
||||
'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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 显示乱码
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
256
plugins.v2/tmdbwallpaper/__init__.py
Normal file
256
plugins.v2/tmdbwallpaper/__init__.py
Normal file
@@ -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)
|
||||
@@ -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("标题和内容不能同时为空")
|
||||
|
||||
Reference in New Issue
Block a user