mirror of
https://github.com/d0zingcat/MoviePilot-Plugins.git
synced 2026-05-17 15:09:25 +00:00
add: TheTVDB探索插件
This commit is contained in:
@@ -365,5 +365,17 @@
|
||||
"v2.0.1": "支持将豆瓣ID转换为MoviePilot中已有用户(在用户个人信息中绑定豆瓣ID),需要MoviePilot v2.2.6+",
|
||||
"v2.0.0": "优化cron表达式输入"
|
||||
}
|
||||
},
|
||||
"TvdbDiscover": {
|
||||
"name": "TheTVDB探索",
|
||||
"description": "让探索支持TheTVDB的数据浏览。",
|
||||
"labels": "探索",
|
||||
"version": "1.0",
|
||||
"icon": "https://www.thetvdb.com/images/logo.svg",
|
||||
"author": "jxxghp",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.0": "需要MoviePilot v2.2.7-1+ 版本,否则无法显示图片"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
643
plugins.v2/tvdbdiscover/__init__.py
Normal file
643
plugins.v2/tvdbdiscover/__init__.py
Normal file
@@ -0,0 +1,643 @@
|
||||
from typing import Any, List, Dict, Tuple, Optional
|
||||
|
||||
from cachetools import cached, TTLCache
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import DiscoverSourceEventData
|
||||
from app.schemas.types import ChainEventType
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class TvdbDiscover(_PluginBase):
|
||||
# 插件名称
|
||||
plugin_name = "TheTVDB探索"
|
||||
# 插件描述
|
||||
plugin_desc = "让探索支持TheTVDB的数据浏览。"
|
||||
# 插件图标
|
||||
plugin_icon = "https://www.thetvdb.com/images/logo.svg"
|
||||
# 插件版本
|
||||
plugin_version = "1.0"
|
||||
# 插件作者
|
||||
plugin_author = "jxxghp"
|
||||
# 作者主页
|
||||
author_url = "https://github.com/jxxghp"
|
||||
# 插件配置项ID前缀
|
||||
plugin_config_prefix = "tvdbdiscover_"
|
||||
# 加载顺序
|
||||
plugin_order = 99
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 私有属性
|
||||
_base_api = "https://api4.thetvdb.com/v4"
|
||||
_enabled = False
|
||||
_proxy = False
|
||||
_api_key = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._proxy = config.get("proxy")
|
||||
self._api_key = config.get("api_key")
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
|
||||
@staticmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
pass
|
||||
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件API
|
||||
[{
|
||||
"path": "/xx",
|
||||
"endpoint": self.xxx,
|
||||
"methods": ["GET", "POST"],
|
||||
"summary": "API说明"
|
||||
}]
|
||||
"""
|
||||
return [{
|
||||
"path": "/tvdb_discover",
|
||||
"endpoint": self.tvdb_discover,
|
||||
"methods": ["GET"],
|
||||
"summary": "TheTVDB探索数据源",
|
||||
"description": "获取TheTVDB探索数据",
|
||||
}]
|
||||
|
||||
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': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'enabled',
|
||||
'label': '启用插件',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'proxy',
|
||||
'label': '使用代理服务器',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VRow',
|
||||
'content': [
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'api_key',
|
||||
'label': 'API Key'
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"proxy": False,
|
||||
"api_key": "ed2aa66b-7899-4677-92a7-67bc9ce3d93a"
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
pass
|
||||
|
||||
@cached(cache=TTLCache(maxsize=1, ttl=30 * 24 * 3600))
|
||||
def __get_token(self) -> Optional[str]:
|
||||
"""
|
||||
根据APIKEY获取token使用
|
||||
"""
|
||||
api_url = f"{self._base_api}/login"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
data = {
|
||||
"apikey": self._api_key
|
||||
}
|
||||
res = RequestUtils(headers=headers).post_res(
|
||||
api_url,
|
||||
json=data,
|
||||
proxies=settings.PROXY if self._proxy else None
|
||||
)
|
||||
if not res:
|
||||
logger.error("获取TheMovieDB token失败")
|
||||
return None
|
||||
return res.json().get("data", {}).get("token")
|
||||
|
||||
@cached(cache=TTLCache(maxsize=32, ttl=1800))
|
||||
def __request(self, mtype: str, **kwargs):
|
||||
"""
|
||||
请求TheTVDB API
|
||||
"""
|
||||
api_url = f"{self._base_api}/{mtype}/filter"
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"Authorization": f"Bearer {self.__get_token()}"
|
||||
}
|
||||
res = RequestUtils(headers=headers).get_res(
|
||||
api_url,
|
||||
params=kwargs,
|
||||
proxies=settings.PROXY if self._proxy else None
|
||||
)
|
||||
if res is None:
|
||||
raise Exception("无法连接TheTVDB,请检查网络连接!")
|
||||
if not res.ok:
|
||||
raise Exception(f"请求TheTVDB API失败:{res.text}")
|
||||
return res.json().get("data")
|
||||
|
||||
def tvdb_discover(self, apikey: str, mtype: str = "series",
|
||||
company: int = None, contentRating: int = None, country: str = "usa",
|
||||
genre: int = None, lang: str = "eng", sort: str = "score", sortType: str = "desc",
|
||||
status: int = None, year: int = None,
|
||||
page: int = 1, count: int = 30) -> List[schemas.MediaInfo]:
|
||||
"""
|
||||
获取TheTVDB探索数据
|
||||
"""
|
||||
|
||||
def __movie_to_media(movie_info: dict) -> schemas.MediaInfo:
|
||||
"""
|
||||
电影数据转换为MediaInfo
|
||||
{
|
||||
"id": 353554,
|
||||
"name": "I Am: Celine Dion",
|
||||
"slug": "i-am-celine-dion",
|
||||
"image": "/banners/v4/movie/353554/posters/6656173b5167f.jpg",
|
||||
"nameTranslations": null,
|
||||
"overviewTranslations": null,
|
||||
"aliases": null,
|
||||
"score": 22669,
|
||||
"runtime": 102,
|
||||
"status": {
|
||||
"id": 5,
|
||||
"name": "Released",
|
||||
"recordType": "movie",
|
||||
"keepUpdated": true
|
||||
},
|
||||
"lastUpdated": "2024-08-10 10:37:05",
|
||||
"year": "2024"
|
||||
}
|
||||
"""
|
||||
return schemas.MediaInfo(
|
||||
type="电影",
|
||||
title=movie_info.get("name"),
|
||||
year=movie_info.get("year"),
|
||||
title_year=f"{movie_info.get('name')} ({movie_info.get('year')})",
|
||||
mediaid_prefix="tvdb",
|
||||
media_id=str(movie_info.get("id")),
|
||||
poster_path=f"https://www.thetvdb.com{movie_info.get('image')}",
|
||||
vote_average=movie_info.get("score"),
|
||||
runtime=movie_info.get("runtime"),
|
||||
overview=movie_info.get("overview")
|
||||
)
|
||||
|
||||
def __series_to_media(series_info: dict) -> schemas.MediaInfo:
|
||||
"""
|
||||
电视剧数据转换为MediaInfo
|
||||
{
|
||||
"id": 79399,
|
||||
"name": "Who Wants to Be a Superhero?",
|
||||
"slug": "who-wants-to-be-a-superhero",
|
||||
"image": "https://artworks.thetvdb.com/banners/posters/79399-1.jpg",
|
||||
"nameTranslations": null,
|
||||
"overviewTranslations": null,
|
||||
"aliases": null,
|
||||
"firstAired": "2006-07-27",
|
||||
"lastAired": "2007-09-06",
|
||||
"nextAired": "",
|
||||
"score": 190,
|
||||
"status": {
|
||||
"id": 2,
|
||||
"name": "Ended",
|
||||
"recordType": "series",
|
||||
"keepUpdated": false
|
||||
},
|
||||
"originalCountry": "usa",
|
||||
"originalLanguage": "eng",
|
||||
"defaultSeasonType": 1,
|
||||
"isOrderRandomized": false,
|
||||
"lastUpdated": "2022-01-16 03:32:39",
|
||||
"averageRuntime": 45,
|
||||
"episodes": null,
|
||||
"overview": "",
|
||||
"year": "2006"
|
||||
}
|
||||
"""
|
||||
return schemas.MediaInfo(
|
||||
type="电视剧",
|
||||
title=series_info.get("name"),
|
||||
year=series_info.get("year"),
|
||||
title_year=f"{series_info.get('name')} ({series_info.get('year')})",
|
||||
mediaid_prefix="tvdb",
|
||||
media_id=str(series_info.get("id")),
|
||||
release_date=series_info.get("firstAired"),
|
||||
poster_path=series_info.get("image"),
|
||||
vote_average=series_info.get("score"),
|
||||
runtime=series_info.get("averageRuntime"),
|
||||
overview=series_info.get("overview")
|
||||
)
|
||||
|
||||
if apikey != settings.API_TOKEN:
|
||||
return []
|
||||
try:
|
||||
# 计算页码,TVDB为固定每页500条
|
||||
if page * count > 500:
|
||||
req_page = 500 // count
|
||||
else:
|
||||
req_page = page - 1
|
||||
result = self.__request(
|
||||
mtype,
|
||||
company=company,
|
||||
contentRating=contentRating,
|
||||
country=country,
|
||||
genre=genre,
|
||||
lang=lang,
|
||||
sort=sort,
|
||||
sortType=sortType,
|
||||
status=status,
|
||||
year=year,
|
||||
page=req_page
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(str(err))
|
||||
return []
|
||||
if not result:
|
||||
return []
|
||||
if mtype == "movies":
|
||||
results = [__movie_to_media(movie) for movie in result]
|
||||
else:
|
||||
results = [__series_to_media(series) for series in result]
|
||||
return results[(page - 1) * count:page * count]
|
||||
|
||||
@staticmethod
|
||||
def tvdb_filter_ui() -> List[dict]:
|
||||
"""
|
||||
TheTVDB过滤参数UI配置
|
||||
"""
|
||||
# 国家字典
|
||||
country_dict = {
|
||||
"usa": "美国",
|
||||
"chn": "中国",
|
||||
"jpn": "日本",
|
||||
"kor": "韩国",
|
||||
"ind": "印度",
|
||||
"fra": "法国",
|
||||
"ger": "德国",
|
||||
"ita": "意大利",
|
||||
"esp": "西班牙",
|
||||
"uk": "英国",
|
||||
"aus": "澳大利亚",
|
||||
"can": "加拿大",
|
||||
"rus": "俄罗斯",
|
||||
"bra": "巴西",
|
||||
"mex": "墨西哥",
|
||||
"arg": "阿根廷",
|
||||
"other": "其他"
|
||||
}
|
||||
|
||||
cuntry_ui = [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": key
|
||||
},
|
||||
"text": value
|
||||
} for key, value in country_dict.items()
|
||||
]
|
||||
|
||||
# 原始语种字典
|
||||
lang_dict = {
|
||||
"eng": "英语",
|
||||
"chi": "中文",
|
||||
"jpn": "日语",
|
||||
"kor": "韩语",
|
||||
"hin": "印地语",
|
||||
"fra": "法语",
|
||||
"deu": "德语",
|
||||
"ita": "意大利语",
|
||||
"spa": "西班牙语",
|
||||
"por": "葡萄牙语",
|
||||
"rus": "俄语",
|
||||
"other": "其他"
|
||||
}
|
||||
|
||||
lang_ui = [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": key
|
||||
},
|
||||
"text": value
|
||||
} for key, value in lang_dict.items()
|
||||
]
|
||||
|
||||
# 风格字典
|
||||
genre_dict = {
|
||||
"1": "Soap",
|
||||
"2": "Science Fiction",
|
||||
"3": "Reality",
|
||||
"4": "News",
|
||||
"5": "Mini-Series",
|
||||
"6": "Horror",
|
||||
"7": "Home and Garden",
|
||||
"8": "Game Show",
|
||||
"9": "Food",
|
||||
"10": "Fantasy",
|
||||
"11": "Family",
|
||||
"12": "Drama",
|
||||
"13": "Documentary",
|
||||
"14": "Crime",
|
||||
"15": "Comedy",
|
||||
"16": "Children",
|
||||
"17": "Animation",
|
||||
"18": "Adventure",
|
||||
"19": "Action",
|
||||
"21": "Sport",
|
||||
"22": "Suspense",
|
||||
"23": "Talk Show",
|
||||
"24": "Thriller",
|
||||
"25": "Travel",
|
||||
"26": "Western",
|
||||
"27": "Anime",
|
||||
"28": "Romance",
|
||||
"29": "Musical",
|
||||
"30": "Podcast",
|
||||
"31": "Mystery",
|
||||
"32": "Indie",
|
||||
"33": "History",
|
||||
"34": "War",
|
||||
"35": "Martial Arts",
|
||||
"36": "Awards Show"
|
||||
}
|
||||
|
||||
genre_ui = [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": key
|
||||
},
|
||||
"text": value
|
||||
} for key, value in genre_dict.items()
|
||||
]
|
||||
|
||||
# 排序字典
|
||||
sort_dict = {
|
||||
"score": "评分",
|
||||
"firstAired": "首播日期",
|
||||
"name": "名称"
|
||||
}
|
||||
|
||||
sort_ui = [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": key
|
||||
},
|
||||
"text": value
|
||||
} for key, value in sort_dict.items()
|
||||
]
|
||||
|
||||
return [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "flex justify-start items-center"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "mr-5"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VLabel",
|
||||
"text": "类型"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "VChipGroup",
|
||||
"props": {
|
||||
"model": "mtype"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": "movies"
|
||||
},
|
||||
"text": "电影"
|
||||
},
|
||||
{
|
||||
"component": "VChip",
|
||||
"props": {
|
||||
"filter": True,
|
||||
"tile": True,
|
||||
"value": "series"
|
||||
},
|
||||
"text": "电视剧"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "flex justify-start items-center"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "mr-5"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VLabel",
|
||||
"text": "风格"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "VChipGroup",
|
||||
"props": {
|
||||
"model": "genre"
|
||||
},
|
||||
"content": genre_ui
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "flex justify-start items-center"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "mr-5"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VLabel",
|
||||
"text": "国家"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "VChipGroup",
|
||||
"props": {
|
||||
"model": "country"
|
||||
},
|
||||
"content": cuntry_ui
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "flex justify-start items-center"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "mr-5"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VLabel",
|
||||
"text": "语言"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "VChipGroup",
|
||||
"props": {
|
||||
"model": "lang"
|
||||
},
|
||||
"content": lang_ui
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "flex justify-start items-center"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "div",
|
||||
"props": {
|
||||
"class": "mr-5"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"component": "VLabel",
|
||||
"text": "排序"
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"component": "VChipGroup",
|
||||
"props": {
|
||||
"model": "sort"
|
||||
},
|
||||
"content": sort_ui
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
|
||||
@eventmanager.register(ChainEventType.DiscoverSource)
|
||||
def discover_source(self, event: Event):
|
||||
"""
|
||||
监听识别事件,使用ChatGPT辅助识别名称
|
||||
"""
|
||||
if not self._enabled or not self._api_key:
|
||||
return
|
||||
event_data: DiscoverSourceEventData = event.event_data
|
||||
tvdb_source = schemas.DiscoverMediaSource(
|
||||
name="TheTVDB",
|
||||
mediaid_prefix="tvdb",
|
||||
api_path=f"plugin/TvdbDiscover/tvdb_discover?apikey={settings.API_TOKEN}",
|
||||
filter_params={
|
||||
"mtype": "series",
|
||||
"company": None,
|
||||
"contentRating": None,
|
||||
"country": "usa",
|
||||
"genre": None,
|
||||
"lang": "eng",
|
||||
"sort": "score",
|
||||
"sortType": "desc",
|
||||
"status": None,
|
||||
"year": None,
|
||||
},
|
||||
filter_ui=self.tvdb_filter_ui()
|
||||
)
|
||||
if not event_data.extra_sources:
|
||||
event_data.extra_sources = [tvdb_source]
|
||||
else:
|
||||
event_data.extra_sources.append(tvdb_source)
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
"""
|
||||
pass
|
||||
Reference in New Issue
Block a user