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()}缓存清除完成")