mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 23:47:41 +08:00
Merge remote-tracking branch 'origin/v2' into v2
This commit is contained in:
@@ -949,7 +949,7 @@ class SubscribeChain(ChainBase):
|
|||||||
and torrent_mediainfo.douban_id != mediainfo.douban_id:
|
and torrent_mediainfo.douban_id != mediainfo.douban_id:
|
||||||
continue
|
continue
|
||||||
logger.info(
|
logger.info(
|
||||||
f'{mediainfo.title_year} 通过媒体信ID匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}')
|
f'{mediainfo.title_year} 通过媒体ID匹配到可选资源:{torrent_info.site_name} - {torrent_info.title}')
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
|||||||
@@ -640,7 +640,7 @@ class Emby:
|
|||||||
item_type=item.get("Type"),
|
item_type=item.get("Type"),
|
||||||
title=item.get("Name"),
|
title=item.get("Name"),
|
||||||
original_title=item.get("OriginalTitle"),
|
original_title=item.get("OriginalTitle"),
|
||||||
year=str(item.get("ProductionYear")),
|
year=item.get("ProductionYear"),
|
||||||
tmdbid=int(tmdbid) if tmdbid else None,
|
tmdbid=int(tmdbid) if tmdbid else None,
|
||||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||||
|
|||||||
@@ -732,7 +732,7 @@ class Jellyfin:
|
|||||||
item_type=item.get("Type"),
|
item_type=item.get("Type"),
|
||||||
title=item.get("Name"),
|
title=item.get("Name"),
|
||||||
original_title=item.get("OriginalTitle"),
|
original_title=item.get("OriginalTitle"),
|
||||||
year=str(item.get("ProductionYear")),
|
year=item.get("ProductionYear"),
|
||||||
tmdbid=int(tmdbid) if tmdbid else None,
|
tmdbid=int(tmdbid) if tmdbid else None,
|
||||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||||
|
|||||||
@@ -509,7 +509,7 @@ class Plex:
|
|||||||
item_type=item.type,
|
item_type=item.type,
|
||||||
title=item.title,
|
title=item.title,
|
||||||
original_title=item.originalTitle,
|
original_title=item.originalTitle,
|
||||||
year=str(item.year),
|
year=item.year,
|
||||||
tmdbid=ids.get("tmdb_id"),
|
tmdbid=ids.get("tmdb_id"),
|
||||||
imdbid=ids.get("imdb_id"),
|
imdbid=ids.get("imdb_id"),
|
||||||
tvdbid=ids.get("tvdb_id"),
|
tvdbid=ids.get("tvdb_id"),
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ class SynologyChat:
|
|||||||
def send_msg(self, title: str, text: Optional[str] = None, image: Optional[str] = None,
|
def send_msg(self, title: str, text: Optional[str] = None, image: Optional[str] = None,
|
||||||
userid: Optional[str] = None, link: Optional[str] = None) -> Optional[bool]:
|
userid: Optional[str] = None, link: Optional[str] = None) -> Optional[bool]:
|
||||||
"""
|
"""
|
||||||
发送Telegram消息
|
发送SynologyChat消息
|
||||||
:param title: 消息标题
|
:param title: 消息标题
|
||||||
:param text: 消息内容
|
:param text: 消息内容
|
||||||
:param image: 消息图片地址
|
:param image: 消息图片地址
|
||||||
|
|||||||
@@ -1,17 +1,18 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
|
import io
|
||||||
import re
|
import re
|
||||||
import threading
|
import threading
|
||||||
from threading import Event
|
from pathlib import Path
|
||||||
from typing import Optional, List, Dict, Callable
|
from typing import Optional, List, Dict, Callable
|
||||||
from urllib.parse import urljoin
|
from urllib.parse import urljoin
|
||||||
|
|
||||||
import telebot
|
from PIL import Image
|
||||||
|
from telebot import TeleBot, apihelper
|
||||||
|
from telebot.types import BotCommand, InlineKeyboardMarkup, InlineKeyboardButton, InputMediaPhoto
|
||||||
from telegramify_markdown import standardize, telegramify
|
from telegramify_markdown import standardize, telegramify
|
||||||
from telegramify_markdown.type import ContentTypes, SentType
|
from telegramify_markdown.type import ContentTypes, SentType
|
||||||
from telebot import apihelper
|
|
||||||
from telebot.types import InlineKeyboardMarkup, InlineKeyboardButton
|
|
||||||
from telebot.types import InputMediaPhoto
|
|
||||||
|
|
||||||
|
from app.core.cache import FileCache
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.core.context import MediaInfo, Context
|
from app.core.context import MediaInfo, Context
|
||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
@@ -19,6 +20,7 @@ from app.helper.thread import ThreadHelper
|
|||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.utils.common import retry
|
from app.utils.common import retry
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
|
from app.utils.security import SecurityUtils
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
|
|
||||||
|
|
||||||
@@ -28,8 +30,7 @@ class RetryException(Exception):
|
|||||||
|
|
||||||
class Telegram:
|
class Telegram:
|
||||||
_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}"
|
_ds_url = f"http://127.0.0.1:{settings.PORT}/api/v1/message?token={settings.API_TOKEN}"
|
||||||
_event = Event()
|
_bot: TeleBot = None
|
||||||
_bot: telebot.TeleBot = None
|
|
||||||
_callback_handlers: Dict[str, Callable] = {} # 存储回调处理器
|
_callback_handlers: Dict[str, Callable] = {} # 存储回调处理器
|
||||||
_user_chat_mapping: Dict[str, str] = {} # userid -> chat_id mapping for reply targeting
|
_user_chat_mapping: Dict[str, str] = {} # userid -> chat_id mapping for reply targeting
|
||||||
_bot_username: Optional[str] = None # Bot username for mention detection
|
_bot_username: Optional[str] = None # Bot username for mention detection
|
||||||
@@ -54,7 +55,7 @@ class Telegram:
|
|||||||
else:
|
else:
|
||||||
apihelper.proxy = settings.PROXY
|
apihelper.proxy = settings.PROXY
|
||||||
# bot
|
# bot
|
||||||
_bot = telebot.TeleBot(self._telegram_token, parse_mode="MarkdownV2")
|
_bot = TeleBot(self._telegram_token, parse_mode="MarkdownV2")
|
||||||
# 记录句柄
|
# 记录句柄
|
||||||
self._bot = _bot
|
self._bot = _bot
|
||||||
# 获取并存储bot用户名用于@检测
|
# 获取并存储bot用户名用于@检测
|
||||||
@@ -539,6 +540,9 @@ class Telegram:
|
|||||||
try:
|
try:
|
||||||
# 处理图片
|
# 处理图片
|
||||||
image = self.__process_image(image) if image else None
|
image = self.__process_image(image) if image else None
|
||||||
|
except RetryException as e:
|
||||||
|
logger.error(f"{str(e)}, 达到重试次数上限, 仅发送文本消息")
|
||||||
|
image = None
|
||||||
|
|
||||||
# 图片消息的标题长度限制为1024,文本消息为4096
|
# 图片消息的标题长度限制为1024,文本消息为4096
|
||||||
caption_limit = 1024 if image else 4096
|
caption_limit = 1024 if image else 4096
|
||||||
@@ -554,22 +558,41 @@ class Telegram:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
@retry(RetryException, logger=logger)
|
@retry(RetryException, logger=logger)
|
||||||
def __process_image(self, image_url: str) -> bytes:
|
def __process_image(self, image_url: str) -> Optional[bytes]:
|
||||||
"""
|
"""
|
||||||
处理图片URL,获取图片内容
|
处理图片URL,获取图片内容
|
||||||
"""
|
"""
|
||||||
|
# 缓存路径
|
||||||
|
sanitized_path = SecurityUtils.sanitize_url_path(image_url)
|
||||||
|
cache_path = Path("images") / sanitized_path
|
||||||
|
# 没有文件类型,则添加后缀
|
||||||
|
if not cache_path.suffix:
|
||||||
|
cache_path = cache_path.with_suffix(".jpg")
|
||||||
|
|
||||||
|
cache_backend = FileCache(base=settings.CACHE_PATH,
|
||||||
|
ttl=settings.GLOBAL_IMAGE_CACHE_DAYS * 24 * 3600)
|
||||||
|
|
||||||
|
content = cache_backend.get(cache_path.as_posix(), region="images")
|
||||||
|
if content:
|
||||||
|
return content
|
||||||
|
|
||||||
|
# 请求远程图片
|
||||||
|
referer = "https://movie.douban.com/" if "doubanio.com" in image_url else None
|
||||||
|
proxies = settings.PROXY if not referer else None
|
||||||
|
res = RequestUtils(ua=settings.NORMAL_USER_AGENT, proxies=proxies, referer=referer).get_res(url=image_url)
|
||||||
|
if not res or not res.content:
|
||||||
|
raise RetryException("获取图片失败")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
res = RequestUtils(
|
# 验证内容是否为有效图片
|
||||||
proxies=settings.PROXY,
|
Image.open(io.BytesIO(res.content)).verify()
|
||||||
ua=settings.NORMAL_USER_AGENT
|
# 保存缓存
|
||||||
).get_res(image_url)
|
cache_backend.set(cache_path.as_posix(), res.content, region="images")
|
||||||
|
|
||||||
if not res or not res.content:
|
|
||||||
raise RetryException("获取图片失败")
|
|
||||||
|
|
||||||
return res.content
|
return res.content
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise
|
logger.error(f"图片验证失败:{str(e)}, 仅发送文本消息")
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
@retry(RetryException, logger=logger)
|
@retry(RetryException, logger=logger)
|
||||||
def __send_short_message(self, image: Optional[bytes], caption: str, **kwargs):
|
def __send_short_message(self, image: Optional[bytes], caption: str, **kwargs):
|
||||||
@@ -637,7 +660,7 @@ class Telegram:
|
|||||||
try:
|
try:
|
||||||
raise RetryException(f"消息 [{i + 1}/{len(boxs)}] 发送失败") from e
|
raise RetryException(f"消息 [{i + 1}/{len(boxs)}] 发送失败") from e
|
||||||
except NameError:
|
except NameError:
|
||||||
raise RetryException("发送长消息失败") from e
|
raise
|
||||||
|
|
||||||
def register_commands(self, commands: Dict[str, dict]):
|
def register_commands(self, commands: Dict[str, dict]):
|
||||||
"""
|
"""
|
||||||
@@ -650,7 +673,7 @@ class Telegram:
|
|||||||
self._bot.delete_my_commands()
|
self._bot.delete_my_commands()
|
||||||
self._bot.set_my_commands(
|
self._bot.set_my_commands(
|
||||||
commands=[
|
commands=[
|
||||||
telebot.types.BotCommand(cmd[1:], str(desc.get("description"))) for cmd, desc in
|
BotCommand(cmd[1:], str(desc.get("description"))) for cmd, desc in
|
||||||
commands.items()
|
commands.items()
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -643,17 +643,23 @@ class TmdbApi:
|
|||||||
reverse=True
|
reverse=True
|
||||||
)
|
)
|
||||||
for tv in tvs:
|
for tv in tvs:
|
||||||
# 年份
|
# 使用年份、名称匹配
|
||||||
tv_year = tv.get('first_air_date')[0:4] if tv.get('first_air_date') else None
|
tv_year = tv.get('first_air_date')[0:4] if tv.get('first_air_date') else None
|
||||||
if (self.__compare_names(name, tv.get('name'))
|
if (self.__compare_names(name, tv.get('name'))
|
||||||
or self.__compare_names(name, tv.get('original_name'))) \
|
or self.__compare_names(name, tv.get('original_name'))) \
|
||||||
and (tv_year == str(season_year)):
|
and (tv_year == str(season_year)):
|
||||||
return tv
|
return tv
|
||||||
# 匹配别名、译名
|
# 获取别名、译名重新匹配
|
||||||
if not tv.get("names"):
|
if not tv.get("names"):
|
||||||
tv = self.get_info(mtype=MediaType.TV, tmdbid=tv.get("id"))
|
tv = self.get_info(mtype=MediaType.TV, tmdbid=tv.get("id"))
|
||||||
if not tv or not self.__compare_names(name, tv.get("names")):
|
if not tv or not (
|
||||||
|
self.__compare_names(name, tv.get("name"))
|
||||||
|
or self.__compare_names(name, tv.get("original_name"))
|
||||||
|
or self.__compare_names(name, tv.get("names"))):
|
||||||
continue
|
continue
|
||||||
|
if tv_year == str(season_year):
|
||||||
|
return tv
|
||||||
|
# 季年份匹配
|
||||||
if __season_match(tv_info=tv, _season_year=season_year):
|
if __season_match(tv_info=tv, _season_year=season_year):
|
||||||
return tv
|
return tv
|
||||||
return {}
|
return {}
|
||||||
@@ -744,11 +750,11 @@ class TmdbApi:
|
|||||||
if validation_result is not None:
|
if validation_result is not None:
|
||||||
return validation_result
|
return validation_result
|
||||||
|
|
||||||
logger.info("正在从TheDbMovie网站查询:%s ..." % name)
|
logger.info("正在从TheMovieDb网站查询:%s ..." % name)
|
||||||
tmdb_url = self._build_tmdb_search_url(name)
|
tmdb_url = self._build_tmdb_search_url(name)
|
||||||
res = RequestUtils(timeout=5, ua=settings.NORMAL_USER_AGENT, proxies=settings.PROXY).get_res(url=tmdb_url)
|
res = RequestUtils(timeout=5, ua=settings.NORMAL_USER_AGENT, proxies=settings.PROXY).get_res(url=tmdb_url)
|
||||||
if res is None:
|
if res is None:
|
||||||
logger.error("无法连接TheDbMovie")
|
logger.error("无法连接TheMovieDb")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 响应验证
|
# 响应验证
|
||||||
|
|||||||
@@ -449,7 +449,7 @@ class TrimeMedia:
|
|||||||
item_type=item_type,
|
item_type=item_type,
|
||||||
title=item.title,
|
title=item.title,
|
||||||
original_title=item.original_title,
|
original_title=item.original_title,
|
||||||
year=str(year),
|
year=year,
|
||||||
tmdbid=item.tmdb_id,
|
tmdbid=item.tmdb_id,
|
||||||
imdbid=item.imdb_id,
|
imdbid=item.imdb_id,
|
||||||
user_state=user_state,
|
user_state=user_state,
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ class RefreshMediaItem(BaseModel):
|
|||||||
# 标题
|
# 标题
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
# 年份
|
# 年份
|
||||||
year: Optional[str] = None
|
year: Optional[Union[str, int]] = None
|
||||||
# 类型
|
# 类型
|
||||||
type: Optional[MediaType] = None
|
type: Optional[MediaType] = None
|
||||||
# 类别
|
# 类别
|
||||||
@@ -110,7 +110,7 @@ class MediaServerItem(BaseModel):
|
|||||||
# 原标题
|
# 原标题
|
||||||
original_title: Optional[str] = None
|
original_title: Optional[str] = None
|
||||||
# 年份
|
# 年份
|
||||||
year: Optional[str] = None
|
year: Optional[Union[str, int]] = None
|
||||||
# TMDBID
|
# TMDBID
|
||||||
tmdbid: Optional[int] = None
|
tmdbid: Optional[int] = None
|
||||||
# IMDBID
|
# IMDBID
|
||||||
|
|||||||
Reference in New Issue
Block a user