fix 支持刷新演员中文

This commit is contained in:
thsrite
2024-08-07 15:16:07 +08:00
parent a3a3400562
commit c3fef6e4ea
3 changed files with 411 additions and 4 deletions

View File

@@ -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)

View File

@@ -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": "添加远程交互命令",

View File

@@ -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))