mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-03-20 03:57:30 +08:00
- 缓存键支持自定义命名,使异步与同步函数可共享缓存结果 - 内存缓存改为类变量,实现多个cache装饰器共享同一缓存空间 - 重构AsyncMemoryBackend,减少重复代码 - 补齐部分模块的缓存清理功能
467 lines
17 KiB
Python
467 lines
17 KiB
Python
import re
|
||
from typing import Optional, Tuple, Union
|
||
|
||
from app.core.cache import cached
|
||
from app.core.context import MediaInfo, settings
|
||
from app.log import logger
|
||
from app.modules import _ModuleBase
|
||
from app.schemas.types import MediaType, ModuleType, OtherModulesType
|
||
from app.utils.http import RequestUtils
|
||
|
||
|
||
class FanartModule(_ModuleBase):
|
||
"""
|
||
{
|
||
"name": "The Wheel of Time",
|
||
"thetvdb_id": "355730",
|
||
"tvposter": [
|
||
{
|
||
"id": "174068",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-64b009de9548d.jpg",
|
||
"lang": "en",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "176424",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-64de44fe42073.jpg",
|
||
"lang": "00",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "176407",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-64dde63c7c941.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "177321",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-64eda10599c3d.jpg",
|
||
"lang": "cz",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "155050",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-6313adbd1fd58.jpg",
|
||
"lang": "pl",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "140198",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-61a0d7b11952e.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "140034",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvposter/the-wheel-of-time-619e65b73871d.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
}
|
||
],
|
||
"hdtvlogo": [
|
||
{
|
||
"id": "139835",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-6197d9392faba.png",
|
||
"lang": "en",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "140039",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-619e87941a128.png",
|
||
"lang": "pt",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "140092",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-619fa2347bada.png",
|
||
"lang": "en",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "164312",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-63c8185cb8824.png",
|
||
"lang": "hu",
|
||
"likes": "1"
|
||
},
|
||
{
|
||
"id": "139827",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-6197539658a9e.png",
|
||
"lang": "en",
|
||
"likes": "1"
|
||
},
|
||
{
|
||
"id": "177214",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-64ebae44c23a6.png",
|
||
"lang": "cz",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "177215",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-64ebae472deef.png",
|
||
"lang": "cz",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "156163",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-63316bef1ff9d.png",
|
||
"lang": "cz",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "155051",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-6313add04ca92.png",
|
||
"lang": "pl",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "152668",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-62ced3775a40a.png",
|
||
"lang": "pl",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "142266",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdtvlogo/the-wheel-of-time-61ccd93eeac2b.png",
|
||
"lang": "de",
|
||
"likes": "0"
|
||
}
|
||
],
|
||
"hdclearart": [
|
||
{
|
||
"id": "164313",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdclearart/the-wheel-of-time-63c81871c982c.png",
|
||
"lang": "en",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "140284",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdclearart/the-wheel-of-time-61a2128ed1df2.png",
|
||
"lang": "pt",
|
||
"likes": "3"
|
||
},
|
||
{
|
||
"id": "139828",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdclearart/the-wheel-of-time-61975401e894c.png",
|
||
"lang": "en",
|
||
"likes": "1"
|
||
},
|
||
{
|
||
"id": "164314",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdclearart/the-wheel-of-time-63c8188488a5f.png",
|
||
"lang": "hu",
|
||
"likes": "1"
|
||
},
|
||
{
|
||
"id": "177322",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdclearart/the-wheel-of-time-64eda135933b6.png",
|
||
"lang": "cz",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "142267",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/hdclearart/the-wheel-of-time-61ccda9918c5c.png",
|
||
"lang": "de",
|
||
"likes": "0"
|
||
}
|
||
],
|
||
"seasonposter": [
|
||
{
|
||
"id": "140199",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonposter/the-wheel-of-time-61a0d7c2976de.jpg",
|
||
"lang": "en",
|
||
"likes": "1",
|
||
"season": "1"
|
||
},
|
||
{
|
||
"id": "176395",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonposter/the-wheel-of-time-64dd80b3d79a9.jpg",
|
||
"lang": "en",
|
||
"likes": "0",
|
||
"season": "1"
|
||
},
|
||
{
|
||
"id": "140035",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonposter/the-wheel-of-time-619e65c4d5357.jpg",
|
||
"lang": "en",
|
||
"likes": "0",
|
||
"season": "1"
|
||
}
|
||
],
|
||
"tvthumb": [
|
||
{
|
||
"id": "140242",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvthumb/the-wheel-of-time-61a1813035506.jpg",
|
||
"lang": "en",
|
||
"likes": "1"
|
||
},
|
||
{
|
||
"id": "177323",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvthumb/the-wheel-of-time-64eda15b6dce6.jpg",
|
||
"lang": "cz",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "176399",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvthumb/the-wheel-of-time-64dd85c9b618c.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "152669",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvthumb/the-wheel-of-time-62ced53d16574.jpg",
|
||
"lang": "pl",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "141983",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvthumb/the-wheel-of-time-61c6d04a6d701.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
}
|
||
],
|
||
"showbackground": [
|
||
{
|
||
"id": "177324",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/showbackground/the-wheel-of-time-64eda1833ccb1.jpg",
|
||
"lang": "",
|
||
"likes": "0",
|
||
"season": "all"
|
||
},
|
||
{
|
||
"id": "141986",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/showbackground/the-wheel-of-time-61c6d08f7c7e2.jpg",
|
||
"lang": "",
|
||
"likes": "0",
|
||
"season": "all"
|
||
},
|
||
{
|
||
"id": "139868",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/showbackground/the-wheel-of-time-6198ce358b98a.jpg",
|
||
"lang": "",
|
||
"likes": "0",
|
||
"season": "all"
|
||
}
|
||
],
|
||
"seasonthumb": [
|
||
{
|
||
"id": "176396",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonthumb/the-wheel-of-time-64dd80c8593f9.jpg",
|
||
"lang": "en",
|
||
"likes": "0",
|
||
"season": "1"
|
||
},
|
||
{
|
||
"id": "176400",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonthumb/the-wheel-of-time-64dd85da7c5e9.jpg",
|
||
"lang": "en",
|
||
"likes": "0",
|
||
"season": "0"
|
||
}
|
||
],
|
||
"tvbanner": [
|
||
{
|
||
"id": "176397",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvbanner/the-wheel-of-time-64dd80da9a255.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "176401",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvbanner/the-wheel-of-time-64dd85e8904ea.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "141988",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvbanner/the-wheel-of-time-61c6d34bceb5f.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
},
|
||
{
|
||
"id": "141984",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/tvbanner/the-wheel-of-time-61c6d06c1c21c.jpg",
|
||
"lang": "en",
|
||
"likes": "0"
|
||
}
|
||
],
|
||
"seasonbanner": [
|
||
{
|
||
"id": "176398",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonbanner/the-wheel-of-time-64dd80e7dbd9f.jpg",
|
||
"lang": "en",
|
||
"likes": "0",
|
||
"season": "1"
|
||
},
|
||
{
|
||
"id": "176402",
|
||
"url": "http://assets.fanart.tv/fanart/tv/355730/seasonbanner/the-wheel-of-time-64dd85fb4f1b1.jpg",
|
||
"lang": "en",
|
||
"likes": "0",
|
||
"season": "0"
|
||
}
|
||
]
|
||
}
|
||
"""
|
||
|
||
# 代理
|
||
_proxies: dict = settings.PROXY
|
||
|
||
# Fanart Api
|
||
_movie_url: str = f'https://webservice.fanart.tv/v3/movies/%s?api_key={settings.FANART_API_KEY}'
|
||
_tv_url: str = f'https://webservice.fanart.tv/v3/tv/%s?api_key={settings.FANART_API_KEY}'
|
||
|
||
def init_module(self) -> None:
|
||
pass
|
||
|
||
def stop(self):
|
||
pass
|
||
|
||
def test(self) -> Tuple[bool, str]:
|
||
"""
|
||
测试模块连接性
|
||
"""
|
||
ret = RequestUtils().get_res("https://webservice.fanart.tv")
|
||
if ret and ret.status_code == 200:
|
||
return True, ""
|
||
elif ret:
|
||
return False, f"无法连接fanart,错误码:{ret.status_code}"
|
||
return False, "fanart网络连接失败"
|
||
|
||
def init_setting(self) -> Tuple[str, Union[str, bool]]:
|
||
return "FANART_API_KEY", True
|
||
|
||
@staticmethod
|
||
def get_name() -> str:
|
||
return "Fanart"
|
||
|
||
@staticmethod
|
||
def get_type() -> ModuleType:
|
||
"""
|
||
获取模块类型
|
||
"""
|
||
return ModuleType.Other
|
||
|
||
@staticmethod
|
||
def get_subtype() -> OtherModulesType:
|
||
"""
|
||
获取模块子类型
|
||
"""
|
||
return OtherModulesType.Fanart
|
||
|
||
@staticmethod
|
||
def get_priority() -> int:
|
||
"""
|
||
获取模块优先级,数字越小优先级越高,只有同一接口下优先级才生效
|
||
"""
|
||
return 0
|
||
|
||
def obtain_images(self, mediainfo: MediaInfo) -> Optional[MediaInfo]:
|
||
"""
|
||
获取图片
|
||
:param mediainfo: 识别的媒体信息
|
||
:return: 更新后的媒体信息
|
||
"""
|
||
if not settings.FANART_ENABLE:
|
||
return None
|
||
if not mediainfo.tmdb_id and not mediainfo.tvdb_id:
|
||
return None
|
||
if mediainfo.type == MediaType.MOVIE:
|
||
result = self.__request_fanart(mediainfo.type, mediainfo.tmdb_id)
|
||
else:
|
||
if mediainfo.tvdb_id:
|
||
result = self.__request_fanart(mediainfo.type, mediainfo.tvdb_id)
|
||
else:
|
||
logger.info(f"{mediainfo.title_year} 没有tvdbid,无法获取fanart图片")
|
||
return None
|
||
if not result or result.get('status') == 'error':
|
||
logger.warn(f"没有获取到 {mediainfo.title_year} 的fanart图片数据")
|
||
return None
|
||
# 获取所有图片
|
||
for name, images in result.items():
|
||
if not images:
|
||
continue
|
||
if not isinstance(images, list):
|
||
continue
|
||
|
||
# 图片属性xx_path
|
||
image_name = self.__name(name)
|
||
if image_name.startswith("season"):
|
||
# 季图片,图片格式seasonxx-xxxx/season-specials-xxxx
|
||
for image_obj in images:
|
||
image_season = image_obj.get('season')
|
||
if image_season is not None:
|
||
# 包括poster,thumb,banner
|
||
if image_season == '0':
|
||
season_image = f"season-specials-{image_name[6:]}"
|
||
else:
|
||
season_image = f"season{str(image_season).rjust(2, '0')}-{image_name[6:]}"
|
||
# 设置图片,没有图片才设置
|
||
if not mediainfo.get_image(season_image):
|
||
mediainfo.set_image(season_image, image_obj.get('url'))
|
||
else:
|
||
|
||
# 其他图片,优先环境变量指定语言,再like最多
|
||
def __pick_best_image(_images):
|
||
lang_env = settings.FANART_LANG
|
||
if lang_env:
|
||
langs = [lang.strip() for lang in lang_env.split(",") if lang.strip()]
|
||
for lang in langs:
|
||
lang_images = [img for img in _images if img.get('lang') == lang]
|
||
if lang_images:
|
||
lang_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||
return lang_images[0]
|
||
# 没设置或没找到,按原逻辑 zh、en、like最多
|
||
zh_images = [img for img in _images if img.get('lang') == 'zh']
|
||
if zh_images:
|
||
zh_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||
return zh_images[0]
|
||
en_images = [img for img in _images if img.get('lang') == 'en']
|
||
if en_images:
|
||
en_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||
return en_images[0]
|
||
_images.sort(key=lambda x: int(x.get('likes', 0)), reverse=True)
|
||
return _images[0]
|
||
|
||
image_obj = __pick_best_image(images)
|
||
# 设置图片,没有图片才设置
|
||
if not mediainfo.get_image(image_name):
|
||
mediainfo.set_image(image_name, image_obj.get('url'))
|
||
|
||
return mediainfo
|
||
|
||
@staticmethod
|
||
def __name(fanart_name: str) -> str:
|
||
"""
|
||
转换Fanart图片的名字
|
||
"""
|
||
words_to_remove = r'tv|movie|hdmovie|hdtv|show|hd'
|
||
pattern = re.compile(words_to_remove, re.IGNORECASE)
|
||
result = re.sub(pattern, '', fanart_name)
|
||
return result
|
||
|
||
@classmethod
|
||
@cached(maxsize=settings.CONF.fanart, ttl=settings.CONF.meta, shared_key="get")
|
||
def __request_fanart(cls, media_type: MediaType, queryid: Union[str, int]) -> Optional[dict]:
|
||
if media_type == MediaType.MOVIE:
|
||
image_url = cls._movie_url % queryid
|
||
else:
|
||
image_url = cls._tv_url % queryid
|
||
try:
|
||
ret = RequestUtils(proxies=cls._proxies, timeout=10).get_res(image_url, raise_exception=True)
|
||
if ret:
|
||
return ret.json()
|
||
else:
|
||
logger.debug(f"未能获取到 {queryid} 的Fanart图片")
|
||
return {}
|
||
except Exception as err:
|
||
logger.error(f"获取{queryid}的Fanart图片失败:{str(err)}")
|
||
return None
|
||
|
||
def clear_cache(self):
|
||
"""
|
||
清除缓存
|
||
"""
|
||
logger.info(f"开始清除{self.get_name()}缓存 ...")
|
||
self.__request_fanart.cache_clear()
|
||
logger.info(f"{self.get_name()}缓存清除完成")
|