mirror of
https://github.com/thsrite/MoviePilot-Plugins.git
synced 2026-03-27 10:05:57 +00:00
fix 支持刷新演员中文
This commit is contained in:
@@ -35,7 +35,7 @@ MoviePilot三方插件市场:https://github.com/thsrite/MoviePilot-Plugins/
|
||||
- 插件彻底卸载 v1.0
|
||||
- 实时软连接 v2.0.1
|
||||
- 订阅规则自动填充 v2.7
|
||||
- Emby元数据刷新 v1.3
|
||||
- Emby元数据刷新 v1.4
|
||||
- Emby媒体标签 v1.2
|
||||
- 热门媒体订阅 v1.7
|
||||
- [HomePage v1.2](docs%2FHomePage.md)
|
||||
|
||||
@@ -446,11 +446,12 @@
|
||||
"name": "Emby元数据刷新",
|
||||
"description": "定时刷新Emby媒体库元数据。",
|
||||
"labels": "Emby",
|
||||
"version": "1.3",
|
||||
"version": "1.4",
|
||||
"icon": "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/emby-icon.png",
|
||||
"author": "thsrite",
|
||||
"level": 1,
|
||||
"history": {
|
||||
"v1.4": "支持刷新演员中文",
|
||||
"v1.3": "支持自定义覆盖元数据、图片",
|
||||
"v1.2": "支持获取Emby最新媒体刷新",
|
||||
"v1.1": "添加远程交互命令",
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import base64
|
||||
import copy
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
from datetime import datetime, timedelta
|
||||
from typing import Optional, Any, List, Dict, Tuple
|
||||
|
||||
import pytz
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
from requests import RequestException
|
||||
from zhconv import zhconv
|
||||
|
||||
from app import schemas
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.db.transferhistory_oper import TransferHistoryOper
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.modules.emby import Emby
|
||||
from app.schemas.types import EventType
|
||||
from app.schemas.types import EventType, MediaType
|
||||
from app.utils.common import retry
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class EmbyMetaRefresh(_PluginBase):
|
||||
@@ -23,7 +35,7 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
# 插件图标
|
||||
plugin_icon = "https://raw.githubusercontent.com/thsrite/MoviePilot-Plugins/main/icons/emby-icon.png"
|
||||
# 插件版本
|
||||
plugin_version = "1.3"
|
||||
plugin_version = "1.4"
|
||||
# 插件作者
|
||||
plugin_author = "thsrite"
|
||||
# 作者主页
|
||||
@@ -35,26 +47,33 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 退出事件
|
||||
_event = threading.Event()
|
||||
# 私有属性
|
||||
_enabled = False
|
||||
tmdbchain = None
|
||||
_onlyonce = False
|
||||
_cron = None
|
||||
_actor_chi = False
|
||||
_num = None
|
||||
_refresh_type = None
|
||||
_ReplaceAllMetadata = "true"
|
||||
_ReplaceAllImages = "true"
|
||||
_EMBY_HOST = settings.EMBY_HOST
|
||||
_EMBY_USER = Emby().get_user()
|
||||
_EMBY_APIKEY = settings.EMBY_API_KEY
|
||||
_scheduler: Optional[BackgroundScheduler] = None
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
self.tmdbchain = TmdbChain()
|
||||
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._cron = config.get("cron")
|
||||
self._actor_chi = config.get("actor_chi")
|
||||
self._num = config.get("num") or 5
|
||||
self._refresh_type = config.get("refresh_type") or "历史记录"
|
||||
self._ReplaceAllMetadata = config.get("ReplaceAllMetadata") or "true"
|
||||
@@ -110,6 +129,7 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
"onlyonce": self._onlyonce,
|
||||
"cron": self._cron,
|
||||
"enabled": self._enabled,
|
||||
"actor_chi": self._actor_chi,
|
||||
"num": self._num,
|
||||
"refresh_type": self._refresh_type,
|
||||
"ReplaceAllMetadata": self._ReplaceAllMetadata,
|
||||
@@ -150,8 +170,371 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
for item in latest:
|
||||
logger.info(f"开始刷新媒体库元数据,最新媒体:{item.type} {item.title} ({item.subtitle})")
|
||||
self.__refresh_emby_library_by_id(item.id)
|
||||
|
||||
# 刮演员中文
|
||||
if self._actor_chi:
|
||||
self.__update_people_chi(item_id=item.id, title=item.title, type=item.type)
|
||||
|
||||
logger.info(f"刷新媒体库元数据完成")
|
||||
|
||||
def __update_people_chi(self, item_id, title, type):
|
||||
"""
|
||||
刮削演员中文名
|
||||
"""
|
||||
# 刮演员中文
|
||||
item_info = self.__get_item_info(item_id)
|
||||
if item_info:
|
||||
imdb_id = item_info.get("ProviderIds", {}).get("Imdb")
|
||||
if imdb_id and self.__need_trans_actor(item_info):
|
||||
logger.info(f"开始获取 {title} ({item_info.get('ProductionYear')}) 的豆瓣演员信息 ...")
|
||||
douban_actors = self.__get_douban_actors(title=title,
|
||||
imdb_id=imdb_id,
|
||||
type=type,
|
||||
year=item_info.get("ProductionYear"))
|
||||
logger.debug(f"获取 {title} ({item_info.get('ProductionYear')}) 的豆瓣演员信息 完成,演员:{douban_actors}")
|
||||
self.__update_peoples(itemid=item_id, iteminfo=item_info,
|
||||
douban_actors=douban_actors)
|
||||
|
||||
|
||||
def __update_peoples(self, itemid: str, iteminfo: dict, douban_actors):
|
||||
# 处理媒体项中的人物信息
|
||||
"""
|
||||
"People": [
|
||||
{
|
||||
"Name": "丹尼尔·克雷格",
|
||||
"Id": "33625",
|
||||
"Role": "James Bond",
|
||||
"Type": "Actor",
|
||||
"PrimaryImageTag": "bef4f764540f10577f804201d8d27918"
|
||||
}
|
||||
]
|
||||
"""
|
||||
peoples = []
|
||||
# 更新当前媒体项人物
|
||||
for people in iteminfo["People"] or []:
|
||||
if self._event.is_set():
|
||||
logger.info(f"演职人员刮削服务停止")
|
||||
return
|
||||
if not people.get("Name"):
|
||||
continue
|
||||
if StringUtils.is_chinese(people.get("Name")) \
|
||||
and StringUtils.is_chinese(people.get("Role")):
|
||||
peoples.append(people)
|
||||
continue
|
||||
info = self.__update_people(people=people,
|
||||
douban_actors=douban_actors)
|
||||
if info:
|
||||
peoples.append(info)
|
||||
# 保存媒体项信息
|
||||
if peoples:
|
||||
iteminfo["People"] = peoples
|
||||
self.set_iteminfo(itemid=itemid, iteminfo=iteminfo)
|
||||
|
||||
def __update_people(self, people: dict, douban_actors: list = None) -> Optional[dict]:
|
||||
"""
|
||||
更新人物信息,返回替换后的人物信息
|
||||
"""
|
||||
|
||||
def __get_emby_iteminfo() -> dict:
|
||||
"""
|
||||
获得Emby媒体项详情
|
||||
"""
|
||||
try:
|
||||
url = f'[HOST]emby/Users/[USER]/Items/{people.get("Id")}?' \
|
||||
f'Fields=ChannelMappingInfo&api_key=[APIKEY]'
|
||||
res = Emby().get_data(url=url)
|
||||
if res:
|
||||
return res.json()
|
||||
except Exception as err:
|
||||
logger.error(f"获取Emby媒体项详情失败:{str(err)}")
|
||||
return {}
|
||||
|
||||
def __get_peopleid(p: dict) -> Tuple[Optional[str], Optional[str]]:
|
||||
"""
|
||||
获取人物的TMDBID、IMDBID
|
||||
"""
|
||||
if not p.get("ProviderIds"):
|
||||
return None, None
|
||||
peopletmdbid, peopleimdbid = None, None
|
||||
if "Tmdb" in p["ProviderIds"]:
|
||||
peopletmdbid = p["ProviderIds"]["Tmdb"]
|
||||
if "tmdb" in p["ProviderIds"]:
|
||||
peopletmdbid = p["ProviderIds"]["tmdb"]
|
||||
if "Imdb" in p["ProviderIds"]:
|
||||
peopleimdbid = p["ProviderIds"]["Imdb"]
|
||||
if "imdb" in p["ProviderIds"]:
|
||||
peopleimdbid = p["ProviderIds"]["imdb"]
|
||||
return peopletmdbid, peopleimdbid
|
||||
|
||||
# 返回的人物信息
|
||||
ret_people = copy.deepcopy(people)
|
||||
|
||||
try:
|
||||
# 查询媒体库人物详情
|
||||
personinfo = __get_emby_iteminfo()
|
||||
if not personinfo:
|
||||
logger.debug(f"未找到人物 {people.get('Name')} 的信息")
|
||||
return None
|
||||
|
||||
# 是否更新标志
|
||||
updated_name = False
|
||||
updated_overview = False
|
||||
update_character = False
|
||||
profile_path = None
|
||||
|
||||
# 从TMDB信息中更新人物信息
|
||||
person_tmdbid, person_imdbid = __get_peopleid(personinfo)
|
||||
if person_tmdbid:
|
||||
person_detail = self.tmdbchain.person_detail(int(person_tmdbid))
|
||||
if person_detail:
|
||||
cn_name = self.__get_chinese_name(person_detail)
|
||||
# 图片优先从TMDB获取
|
||||
profile_path = person_detail.profile_path
|
||||
if profile_path:
|
||||
logger.debug(f"{people.get('Name')} 从TMDB获取到图片:{profile_path}")
|
||||
profile_path = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{profile_path}"
|
||||
if cn_name:
|
||||
# 更新中文名
|
||||
logger.debug(f"{people.get('Name')} 从TMDB获取到中文名:{cn_name}")
|
||||
personinfo["Name"] = cn_name
|
||||
ret_people["Name"] = cn_name
|
||||
updated_name = True
|
||||
# 更新中文描述
|
||||
biography = person_detail.biography
|
||||
if biography and StringUtils.is_chinese(biography):
|
||||
logger.debug(f"{people.get('Name')} 从TMDB获取到中文描述")
|
||||
personinfo["Overview"] = biography
|
||||
updated_overview = True
|
||||
|
||||
# 从豆瓣信息中更新人物信息
|
||||
"""
|
||||
{
|
||||
"name": "丹尼尔·克雷格",
|
||||
"roles": [
|
||||
"演员",
|
||||
"制片人",
|
||||
"配音"
|
||||
],
|
||||
"title": "丹尼尔·克雷格(同名)英国,英格兰,柴郡,切斯特影视演员",
|
||||
"url": "https://movie.douban.com/celebrity/1025175/",
|
||||
"user": null,
|
||||
"character": "饰 詹姆斯·邦德 James Bond 007",
|
||||
"uri": "douban://douban.com/celebrity/1025175?subject_id=27230907",
|
||||
"avatar": {
|
||||
"large": "https://qnmob3.doubanio.com/view/celebrity/raw/public/p42588.jpg?imageView2/2/q/80/w/600/h/3000/format/webp",
|
||||
"normal": "https://qnmob3.doubanio.com/view/celebrity/raw/public/p42588.jpg?imageView2/2/q/80/w/200/h/300/format/webp"
|
||||
},
|
||||
"sharing_url": "https://www.douban.com/doubanapp/dispatch?uri=/celebrity/1025175/",
|
||||
"type": "celebrity",
|
||||
"id": "1025175",
|
||||
"latin_name": "Daniel Craig"
|
||||
}
|
||||
"""
|
||||
if douban_actors and (not updated_name
|
||||
or not updated_overview
|
||||
or not update_character):
|
||||
# 从豆瓣演员中匹配中文名称、角色和简介
|
||||
for douban_actor in douban_actors:
|
||||
if douban_actor.get("latin_name") == people.get("Name") \
|
||||
or douban_actor.get("name") == people.get("Name"):
|
||||
# 名称
|
||||
if not updated_name:
|
||||
logger.debug(f"{people.get('Name')} 从豆瓣中获取到中文名:{douban_actor.get('name')}")
|
||||
personinfo["Name"] = douban_actor.get("name")
|
||||
ret_people["Name"] = douban_actor.get("name")
|
||||
updated_name = True
|
||||
# 描述
|
||||
if not updated_overview:
|
||||
if douban_actor.get("title"):
|
||||
logger.debug(f"{people.get('Name')} 从豆瓣中获取到中文描述:{douban_actor.get('title')}")
|
||||
personinfo["Overview"] = douban_actor.get("title")
|
||||
updated_overview = True
|
||||
# 饰演角色
|
||||
if not update_character:
|
||||
if douban_actor.get("character"):
|
||||
# "饰 詹姆斯·邦德 James Bond 007"
|
||||
character = re.sub(r"饰\s+", "",
|
||||
douban_actor.get("character"))
|
||||
character = re.sub("演员", "",
|
||||
character)
|
||||
if character:
|
||||
logger.debug(f"{people.get('Name')} 从豆瓣中获取到饰演角色:{character}")
|
||||
ret_people["Role"] = character
|
||||
update_character = True
|
||||
# 图片
|
||||
if not profile_path:
|
||||
avatar = douban_actor.get("avatar") or {}
|
||||
if avatar.get("large"):
|
||||
logger.debug(f"{people.get('Name')} 从豆瓣中获取到图片:{avatar.get('large')}")
|
||||
profile_path = avatar.get("large")
|
||||
break
|
||||
|
||||
# 更新人物图片
|
||||
if profile_path:
|
||||
logger.debug(f"更新人物 {people.get('Name')} 的图片:{profile_path}")
|
||||
self.set_item_image(itemid=people.get("Id"), imageurl=profile_path)
|
||||
|
||||
# 锁定人物信息
|
||||
if updated_name:
|
||||
if "Name" not in personinfo["LockedFields"]:
|
||||
personinfo["LockedFields"].append("Name")
|
||||
if updated_overview:
|
||||
if "Overview" not in personinfo["LockedFields"]:
|
||||
personinfo["LockedFields"].append("Overview")
|
||||
|
||||
# 更新人物信息
|
||||
if updated_name or updated_overview or update_character:
|
||||
logger.info(f"更新人物 {people.get('Name')} 的信息:{personinfo}")
|
||||
ret = self.set_iteminfo(itemid=people.get("Id"), iteminfo=personinfo)
|
||||
if ret:
|
||||
return ret_people
|
||||
else:
|
||||
logger.debug(f"人物 {people.get('Name')} 未找到中文数据")
|
||||
except Exception as err:
|
||||
logger.error(f"更新人物信息失败:{str(err)}")
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def set_iteminfo(itemid: str, iteminfo: dict):
|
||||
"""
|
||||
更新媒体项详情
|
||||
"""
|
||||
|
||||
def __set_emby_iteminfo():
|
||||
"""
|
||||
更新Emby媒体项详情
|
||||
"""
|
||||
try:
|
||||
res = Emby().post_data(
|
||||
url=f'[HOST]emby/Items/{itemid}?api_key=[APIKEY]&reqformat=json',
|
||||
data=json.dumps(iteminfo),
|
||||
headers={
|
||||
"Content-Type": "application/json"
|
||||
}
|
||||
)
|
||||
if res and res.status_code in [200, 204]:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"更新Emby媒体项详情失败,错误码:{res.status_code}")
|
||||
return False
|
||||
except Exception as err:
|
||||
logger.error(f"更新Emby媒体项详情失败:{str(err)}")
|
||||
return False
|
||||
|
||||
return __set_emby_iteminfo()
|
||||
|
||||
@staticmethod
|
||||
@retry(RequestException, logger=logger)
|
||||
def set_item_image(itemid: str, imageurl: str):
|
||||
"""
|
||||
更新媒体项图片
|
||||
"""
|
||||
|
||||
def __download_image():
|
||||
"""
|
||||
下载图片
|
||||
"""
|
||||
try:
|
||||
if "doubanio.com" in imageurl:
|
||||
r = RequestUtils(headers={
|
||||
'Referer': "https://movie.douban.com/"
|
||||
}, ua=settings.USER_AGENT).get_res(url=imageurl, raise_exception=True)
|
||||
else:
|
||||
r = RequestUtils().get_res(url=imageurl, raise_exception=True)
|
||||
if r:
|
||||
return base64.b64encode(r.content).decode()
|
||||
else:
|
||||
logger.warn(f"{imageurl} 图片下载失败,请检查网络连通性")
|
||||
except Exception as err:
|
||||
logger.error(f"下载图片失败:{str(err)}")
|
||||
return None
|
||||
|
||||
def __set_emby_item_image(_base64: str):
|
||||
"""
|
||||
更新Emby媒体项图片
|
||||
"""
|
||||
try:
|
||||
url = f'[HOST]emby/Items/{itemid}/Images/Primary?api_key=[APIKEY]'
|
||||
res = Emby().post_data(
|
||||
url=url,
|
||||
data=_base64,
|
||||
headers={
|
||||
"Content-Type": "image/png"
|
||||
}
|
||||
)
|
||||
if res and res.status_code in [200, 204]:
|
||||
return True
|
||||
else:
|
||||
logger.error(f"更新Emby媒体项图片失败,错误码:{res.status_code}")
|
||||
return False
|
||||
except Exception as result:
|
||||
logger.error(f"更新Emby媒体项图片失败:{result}")
|
||||
return False
|
||||
|
||||
# 下载图片获取base64
|
||||
image_base64 = __download_image()
|
||||
if image_base64:
|
||||
return __set_emby_item_image(image_base64)
|
||||
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __get_chinese_name(personinfo: schemas.MediaPerson) -> str:
|
||||
"""
|
||||
获取TMDB别名中的中文名
|
||||
"""
|
||||
try:
|
||||
also_known_as = personinfo.also_known_as or []
|
||||
if also_known_as:
|
||||
for name in also_known_as:
|
||||
if name and StringUtils.is_chinese(name):
|
||||
# 使用cn2an将繁体转化为简体
|
||||
return zhconv.convert(name, "zh-hans")
|
||||
except Exception as err:
|
||||
logger.error(f"获取人物中文名失败:{err}")
|
||||
return ""
|
||||
|
||||
def __get_douban_actors(self, title, imdb_id, type, year, season: int = None) -> List[dict]:
|
||||
"""
|
||||
获取豆瓣演员信息
|
||||
"""
|
||||
# 随机休眠 3-10 秒
|
||||
sleep_time = 3 + int(time.time()) % 7
|
||||
logger.debug(f"随机休眠 {sleep_time}秒 ...")
|
||||
time.sleep(sleep_time)
|
||||
# 匹配豆瓣信息
|
||||
doubaninfo = self.chain.match_doubaninfo(name=title,
|
||||
imdbid=imdb_id,
|
||||
mtype=type,
|
||||
year=year,
|
||||
season=season)
|
||||
# 豆瓣演员
|
||||
if doubaninfo:
|
||||
doubanitem = self.chain.douban_info(doubaninfo.get("id")) or {}
|
||||
return (doubanitem.get("actors") or []) + (doubanitem.get("directors") or [])
|
||||
else:
|
||||
logger.debug(f"未找到豆瓣信息:{title} {year}")
|
||||
return []
|
||||
|
||||
@staticmethod
|
||||
def __need_trans_actor(item):
|
||||
"""
|
||||
是否需要处理人物信息
|
||||
"""
|
||||
_peoples = [x for x in item.get("People", []) if
|
||||
(x.get("Name") and not StringUtils.is_chinese(x.get("Name")))
|
||||
or (x.get("Role") and not StringUtils.is_chinese(x.get("Role")))]
|
||||
if _peoples:
|
||||
return True
|
||||
return False
|
||||
|
||||
def __get_item_info(self, item_id):
|
||||
res = RequestUtils().get_res(
|
||||
f"{self._EMBY_HOST}/emby/Users/{self._EMBY_USER}/Items/{item_id}?api_key={self._EMBY_APIKEY}")
|
||||
if res and res.status_code == 200:
|
||||
return res.json()
|
||||
return {}
|
||||
|
||||
@eventmanager.register(EventType.PluginAction)
|
||||
def remote_sync(self, event: Event):
|
||||
"""
|
||||
@@ -181,6 +564,8 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
for movie in movies:
|
||||
self.__refresh_emby_library_by_id(item_id=movie.item_id)
|
||||
logger.info(f"已通知刷新Emby电影:{movie.title} ({movie.year}) item_id:{movie.item_id}")
|
||||
if self._actor_chi:
|
||||
self.__update_people_chi(item_id=movie.item_id, title=movie.title, type=MediaType.MOVIE)
|
||||
else:
|
||||
item_id = self.__get_emby_series_id_by_name(name=transferinfo.title, year=transferinfo.year)
|
||||
if not item_id or item_id is None:
|
||||
@@ -207,6 +592,8 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
self.__refresh_emby_library_by_id(item_id=episode_item_id)
|
||||
logger.info(
|
||||
f"已通知刷新Emby电视剧:{transferinfo.title} ({transferinfo.year}) {transferinfo.seasons}{transferinfo.episodes} item_id:{episode_item_id}")
|
||||
if self._actor_chi:
|
||||
self.__update_people_chi(item_id=item_id, title=transferinfo.title, type=MediaType.TV)
|
||||
|
||||
def __get_emby_episode_item_id(self, item_id: str, season: int, episode: int) -> Optional[str]:
|
||||
"""
|
||||
@@ -453,6 +840,22 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 4
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'actor_chi',
|
||||
'label': '刮削演员中文',
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -481,6 +884,7 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
], {
|
||||
"enabled": False,
|
||||
"onlyonce": False,
|
||||
"actor_chi": False,
|
||||
"ReplaceAllMetadata": "true",
|
||||
"ReplaceAllImages": "true",
|
||||
"cron": "5 1 * * *",
|
||||
@@ -499,7 +903,9 @@ class EmbyMetaRefresh(_PluginBase):
|
||||
if self._scheduler:
|
||||
self._scheduler.remove_all_jobs()
|
||||
if self._scheduler.running:
|
||||
self._event.set()
|
||||
self._scheduler.shutdown()
|
||||
self._event.clear()
|
||||
self._scheduler = None
|
||||
except Exception as e:
|
||||
logger.error("退出插件失败:%s" % str(e))
|
||||
|
||||
Reference in New Issue
Block a user