mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-07-07 07:41:28 +08:00
sync main
This commit is contained in:
@@ -13,7 +13,7 @@ from app.modules import _ModuleBase
|
||||
from app.modules.douban.apiv2 import DoubanApi
|
||||
from app.modules.douban.douban_cache import DoubanCache
|
||||
from app.modules.douban.scraper import DoubanScraper
|
||||
from app.schemas import MediaPerson
|
||||
from app.schemas import MediaPerson, APIRateLimitException
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.common import retry
|
||||
from app.utils.http import RequestUtils
|
||||
@@ -145,11 +145,12 @@ class DoubanModule(_ModuleBase):
|
||||
|
||||
return None
|
||||
|
||||
def douban_info(self, doubanid: str, mtype: MediaType = None) -> Optional[dict]:
|
||||
def douban_info(self, doubanid: str, mtype: MediaType = None, raise_exception: bool = True) -> Optional[dict]:
|
||||
"""
|
||||
获取豆瓣信息
|
||||
:param doubanid: 豆瓣ID
|
||||
:param mtype: 媒体类型
|
||||
:param raise_exception: 触发速率限制时是否抛出异常
|
||||
:return: 豆瓣信息
|
||||
"""
|
||||
"""
|
||||
@@ -425,7 +426,10 @@ class DoubanModule(_ModuleBase):
|
||||
info = self.doubanapi.tv_detail(doubanid)
|
||||
if info:
|
||||
if "subject_ip_rate_limit" in info.get("msg", ""):
|
||||
logger.warn(f"触发豆瓣IP速率限制,错误信息:{info} ...")
|
||||
msg = f"触发豆瓣IP速率限制,错误信息:{info} ..."
|
||||
logger.warn(msg)
|
||||
if raise_exception:
|
||||
raise APIRateLimitException(msg)
|
||||
return None
|
||||
celebrities = self.doubanapi.tv_celebrities(doubanid)
|
||||
if celebrities:
|
||||
@@ -440,7 +444,10 @@ class DoubanModule(_ModuleBase):
|
||||
info = self.doubanapi.movie_detail(doubanid)
|
||||
if info:
|
||||
if "subject_ip_rate_limit" in info.get("msg", ""):
|
||||
logger.warn(f"触发豆瓣IP速率限制,错误信息:{info} ...")
|
||||
msg = f"触发豆瓣IP速率限制,错误信息:{info} ..."
|
||||
logger.warn(msg)
|
||||
if raise_exception:
|
||||
raise APIRateLimitException(msg)
|
||||
return None
|
||||
celebrities = self.doubanapi.movie_celebrities(doubanid)
|
||||
if celebrities:
|
||||
@@ -599,7 +606,8 @@ class DoubanModule(_ModuleBase):
|
||||
|
||||
@retry(Exception, 5, 3, 3, logger=logger)
|
||||
def match_doubaninfo(self, name: str, imdbid: str = None,
|
||||
mtype: MediaType = None, year: str = None, season: int = None) -> dict:
|
||||
mtype: MediaType = None, year: str = None, season: int = None,
|
||||
raise_exception: bool = False) -> dict:
|
||||
"""
|
||||
搜索和匹配豆瓣信息
|
||||
:param name: 名称
|
||||
@@ -607,6 +615,7 @@ class DoubanModule(_ModuleBase):
|
||||
:param mtype: 类型
|
||||
:param year: 年份
|
||||
:param season: 季号
|
||||
:param raise_exception: 触发速率限制时是否抛出异常
|
||||
"""
|
||||
if imdbid:
|
||||
# 优先使用IMDBID查询
|
||||
@@ -622,13 +631,19 @@ class DoubanModule(_ModuleBase):
|
||||
# 搜索
|
||||
logger.info(f"开始使用名称 {name} 匹配豆瓣信息 ...")
|
||||
result = self.doubanapi.search(f"{name} {year or ''}".strip())
|
||||
if not result or not result.get("items"):
|
||||
if not result:
|
||||
logger.warn(f"未找到 {name} 的豆瓣信息")
|
||||
return {}
|
||||
# 触发rate limit
|
||||
if "search_access_rate_limit" in result.values():
|
||||
logger.warn(f"触发豆瓣API速率限制 错误信息 {result} ...")
|
||||
raise Exception("触发豆瓣API速率限制")
|
||||
msg = f"触发豆瓣API速率限制,错误信息:{result} ..."
|
||||
logger.warn(msg)
|
||||
if raise_exception:
|
||||
raise APIRateLimitException(msg)
|
||||
return {}
|
||||
if not result.get("items"):
|
||||
logger.warn(f"未找到 {name} 的豆瓣信息")
|
||||
return {}
|
||||
for item_obj in result.get("items"):
|
||||
type_name = item_obj.get("type_name")
|
||||
if type_name not in [MediaType.TV.value, MediaType.MOVIE.value]:
|
||||
|
||||
@@ -487,12 +487,13 @@ class FileManagerModule(_ModuleBase):
|
||||
"""
|
||||
# 字幕正则式
|
||||
_zhcn_sub_re = r"([.\[(](((zh[-_])?(cn|ch[si]|sg|sc))|zho?" \
|
||||
r"|chinese|(cn|ch[si]|sg|zho?|eng)[-_&](cn|ch[si]|sg|zho?|eng)" \
|
||||
r"|chinese|(cn|ch[si]|sg|zho?|eng)[-_&]?(cn|ch[si]|sg|zho?|eng)" \
|
||||
r"|简[体中]?)[.\])])" \
|
||||
r"|([\u4e00-\u9fa5]{0,3}[中双][\u4e00-\u9fa5]{0,2}[字文语][\u4e00-\u9fa5]{0,3})" \
|
||||
r"|简体|简中|JPSC" \
|
||||
r"|(?<![a-z0-9])gb(?![a-z0-9])"
|
||||
_zhtw_sub_re = r"([.\[(](((zh[-_])?(hk|tw|cht|tc))" \
|
||||
r"|(cht|eng)[-_&]?(cht|eng)" \
|
||||
r"|繁[体中]?)[.\])])" \
|
||||
r"|繁体中[文字]|中[文字]繁体|繁体|JPTC" \
|
||||
r"|(?<![a-z0-9])big5(?![a-z0-9])"
|
||||
@@ -987,6 +988,10 @@ class FileManagerModule(_ModuleBase):
|
||||
"en_name": meta.en_name,
|
||||
# 年份
|
||||
"year": mediainfo.year or meta.year,
|
||||
# 季年份根据season值获取
|
||||
"season_year": mediainfo.season_years.get(
|
||||
int(meta.season_seq),
|
||||
None) if (mediainfo.season_years and meta.season_seq) else None,
|
||||
# 资源类型
|
||||
"resourceType": meta.resource_type,
|
||||
# 特效
|
||||
|
||||
@@ -268,25 +268,59 @@ class Plex:
|
||||
season_episodes[episode.seasonNumber].append(episode.index)
|
||||
return videos.key, season_episodes
|
||||
|
||||
def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]:
|
||||
def get_remote_image_by_id(self, item_id: str, image_type: str, depth: int = 0) -> Optional[str]:
|
||||
"""
|
||||
根据ItemId从Plex查询图片地址
|
||||
:param item_id: 在Emby中的ID
|
||||
:param image_type: 图片的类型,Poster或者Backdrop等
|
||||
:param depth: 当前递归深度,默认为0
|
||||
:return: 图片对应在TMDB中的URL
|
||||
"""
|
||||
if not self._plex:
|
||||
if not self._plex or depth > 2 or not item_id:
|
||||
return None
|
||||
try:
|
||||
if image_type == "Poster":
|
||||
images = self._plex.fetchItems('/library/metadata/%s/posters' % item_id,
|
||||
cls=media.Poster)
|
||||
image_url = None
|
||||
ekey = f"/library/metadata/{item_id}"
|
||||
item = self._plex.fetchItem(ekey=ekey)
|
||||
if not item:
|
||||
return None
|
||||
# 如果配置了外网播放地址以及Token,则默认从Plex媒体服务器获取图片,否则返回有外网地址的图片资源
|
||||
if self._playhost and self._token:
|
||||
query = {"X-Plex-Token": settings.PLEX_TOKEN}
|
||||
if image_type == "Poster":
|
||||
if item.thumb:
|
||||
image_url = RequestUtils.combine_url(host=settings.PLEX_PLAY_HOST, path=item.thumb, query=query)
|
||||
else:
|
||||
# 默认使用art也就是Backdrop进行处理
|
||||
if item.art:
|
||||
image_url = RequestUtils.combine_url(host=settings.PLEX_PLAY_HOST, path=item.art, query=query)
|
||||
# 这里对episode进行特殊处理,实际上episode的Backdrop是Poster
|
||||
# 也有个别情况,比如机智的凡人小子episode就是Poster,因此这里把episode的优先级降低,默认还是取art
|
||||
if not image_url and item.TYPE == "episode" and item.thumb:
|
||||
image_url = RequestUtils.combine_url(host=settings.PLEX_PLAY_HOST, path=item.thumb, query=query)
|
||||
else:
|
||||
images = self._plex.fetchItems('/library/metadata/%s/arts' % item_id,
|
||||
cls=media.Art)
|
||||
for image in images:
|
||||
if hasattr(image, 'key') and image.key.startswith('http'):
|
||||
return image.key
|
||||
if image_type == "Poster":
|
||||
images = self._plex.fetchItems(ekey=f"{ekey}/posters",
|
||||
cls=media.Poster)
|
||||
else:
|
||||
# 默认使用art也就是Backdrop进行处理
|
||||
images = self._plex.fetchItems(ekey=f"{ekey}/arts",
|
||||
cls=media.Art)
|
||||
# 这里对episode进行特殊处理,实际上episode的Backdrop是Poster
|
||||
# 也有个别情况,比如机智的凡人小子episode就是Poster,因此这里把episode的优先级降低,默认还是取art
|
||||
if not images and item.TYPE == "episode":
|
||||
images = self._plex.fetchItems(ekey=f"{ekey}/posters",
|
||||
cls=media.Poster)
|
||||
for image in images:
|
||||
if hasattr(image, "key") and image.key.startswith("http"):
|
||||
image_url = image.key
|
||||
break
|
||||
# 如果最后还是找不到,则递归父级进行查找
|
||||
if not image_url and hasattr(item, "parentRatingKey"):
|
||||
return self.get_remote_image_by_id(item_id=item.parentRatingKey,
|
||||
image_type=image_type,
|
||||
depth=depth + 1)
|
||||
return image_url
|
||||
except Exception as e:
|
||||
logger.error(f"获取封面出错:" + str(e))
|
||||
return None
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import re
|
||||
import threading
|
||||
import uuid
|
||||
from pathlib import Path
|
||||
from threading import Event
|
||||
from typing import Optional, List, Dict
|
||||
@@ -93,6 +94,8 @@ class Telegram:
|
||||
|
||||
try:
|
||||
if text:
|
||||
# 对text进行Markdown特殊字符转义
|
||||
text = re.sub(r"([_`])", r"\\\1", text)
|
||||
caption = f"*{title}*\n{text}"
|
||||
else:
|
||||
caption = f"*{title}*"
|
||||
@@ -205,13 +208,15 @@ class Telegram:
|
||||
"""
|
||||
|
||||
if image:
|
||||
req = RequestUtils(proxies=settings.PROXY).get_res(image)
|
||||
if req is None:
|
||||
res = RequestUtils(proxies=settings.PROXY).get_res(image)
|
||||
if res is None:
|
||||
raise Exception("获取图片失败")
|
||||
if req.content:
|
||||
image_file = Path(settings.TEMP_PATH) / Path(image).name
|
||||
image_file.write_bytes(req.content)
|
||||
if res.content:
|
||||
# 使用随机标识构建图片文件的完整路径,并写入图片内容到文件
|
||||
image_file = Path(settings.TEMP_PATH) / str(uuid.uuid4())
|
||||
image_file.write_bytes(res.content)
|
||||
photo = InputFile(image_file)
|
||||
# 发送图片到Telegram
|
||||
ret = self._bot.send_photo(chat_id=userid or self._telegram_chat_id,
|
||||
photo=photo,
|
||||
caption=caption,
|
||||
|
||||
@@ -365,9 +365,9 @@ class TheMovieDbModule(_ModuleBase):
|
||||
:param season: 季
|
||||
"""
|
||||
season_info = self.tmdb.get_tv_season_detail(tmdbid=tmdbid, season=season)
|
||||
if not season_info:
|
||||
if not season_info or not season_info.get("episodes"):
|
||||
return []
|
||||
return [schemas.TmdbEpisode(**episode) for episode in season_info.get("episodes", [])]
|
||||
return [schemas.TmdbEpisode(**episode) for episode in season_info.get("episodes")]
|
||||
|
||||
def scheduler_job(self) -> None:
|
||||
"""
|
||||
|
||||
@@ -112,28 +112,60 @@ class WeChat:
|
||||
"""
|
||||
message_url = self._send_msg_url % self.__get_access_token()
|
||||
if text:
|
||||
conent = "%s\n%s" % (title, text.replace("\n\n", "\n"))
|
||||
content = "%s\n%s" % (title, text.replace("\n\n", "\n"))
|
||||
else:
|
||||
conent = title
|
||||
content = title
|
||||
|
||||
if link:
|
||||
conent = f"{conent}\n点击查看:{link}"
|
||||
content = f"{content}\n点击查看:{link}"
|
||||
|
||||
if not userid:
|
||||
userid = "@all"
|
||||
|
||||
req_json = {
|
||||
"touser": userid,
|
||||
"msgtype": "text",
|
||||
"agentid": self._appid,
|
||||
"text": {
|
||||
"content": conent
|
||||
},
|
||||
"safe": 0,
|
||||
"enable_id_trans": 0,
|
||||
"enable_duplicate_check": 0
|
||||
}
|
||||
return self.__post_request(message_url, req_json)
|
||||
# Check if content exceeds 2048 bytes and split if necessary
|
||||
if len(content.encode('utf-8')) > 2048:
|
||||
content_chunks = []
|
||||
current_chunk = ""
|
||||
for line in content.splitlines():
|
||||
if len(current_chunk.encode('utf-8')) + len(line.encode('utf-8')) > 2048:
|
||||
content_chunks.append(current_chunk.strip())
|
||||
current_chunk = ""
|
||||
current_chunk += line + "\n"
|
||||
if current_chunk:
|
||||
content_chunks.append(current_chunk.strip())
|
||||
|
||||
# Send each chunk as a separate message
|
||||
result = True
|
||||
for chunk in content_chunks:
|
||||
req_json = {
|
||||
"touser": userid,
|
||||
"msgtype": "text",
|
||||
"agentid": self._appid,
|
||||
"text": {
|
||||
"content": chunk
|
||||
},
|
||||
"safe": 0,
|
||||
"enable_id_trans": 0,
|
||||
"enable_duplicate_check": 0
|
||||
}
|
||||
result = self.__post_request(message_url, req_json)
|
||||
if not result:
|
||||
return False
|
||||
else:
|
||||
req_json = {
|
||||
"touser": userid,
|
||||
"msgtype": "text",
|
||||
"agentid": self._appid,
|
||||
"text": {
|
||||
"content": content
|
||||
},
|
||||
"safe": 0,
|
||||
"enable_id_trans": 0,
|
||||
"enable_duplicate_check": 0
|
||||
}
|
||||
result = self.__post_request(message_url, req_json)
|
||||
|
||||
return result
|
||||
|
||||
def __send_image_message(self, title: str, text: str, image_url: str,
|
||||
userid: str = None, link: str = None) -> Optional[bool]:
|
||||
|
||||
Reference in New Issue
Block a user