fix mediaserver
This commit is contained in:
parent
c8d6de3e9b
commit
d1841d8f15
@ -28,12 +28,18 @@ class MediaServerChain(ChainBase):
|
||||
"""
|
||||
return self.run_module("mediaserver_librarys", server=server)
|
||||
|
||||
def items(self, server: str, library_id: Union[str, int]) -> Generator:
|
||||
def items(self, server: str, library_id: Union[str, int]) -> Generator[schemas.MediaServerItem]:
|
||||
"""
|
||||
获取媒体服务器所有项目
|
||||
"""
|
||||
return self.run_module("mediaserver_items", server=server, library_id=library_id)
|
||||
|
||||
def iteminfo(self, server: str, item_id: Union[str, int]) -> schemas.MediaServerItem:
|
||||
"""
|
||||
获取媒体服务器项目信息
|
||||
"""
|
||||
return self.run_module("mediaserver_iteminfo", server=server, item_id=item_id)
|
||||
|
||||
def episodes(self, server: str, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||
"""
|
||||
获取媒体服务器剧集信息
|
||||
|
@ -6,7 +6,6 @@ from app.core.context import MediaInfo
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.modules.emby.emby import Emby
|
||||
from app.schemas import ExistMediaInfo, RefreshMediaItem, WebhookEventInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
@ -40,7 +39,7 @@ class EmbyModule(_ModuleBase):
|
||||
# Emby认证
|
||||
return self.emby.authenticate(name, password)
|
||||
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
|
||||
"""
|
||||
解析Webhook报文体
|
||||
:param body: 请求体
|
||||
@ -50,7 +49,7 @@ class EmbyModule(_ModuleBase):
|
||||
"""
|
||||
return self.emby.get_webhook_message(form, args)
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]:
|
||||
"""
|
||||
判断媒体文件是否存在
|
||||
:param mediainfo: 识别的媒体信息
|
||||
@ -62,16 +61,26 @@ class EmbyModule(_ModuleBase):
|
||||
movie = self.emby.get_iteminfo(itemid)
|
||||
if movie:
|
||||
logger.info(f"媒体库中已存在:{movie}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
movies = self.emby.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
server="emby",
|
||||
itemid=movie.item_id
|
||||
)
|
||||
movies = self.emby.get_movies(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
tmdb_id=mediainfo.tmdb_id)
|
||||
if not movies:
|
||||
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
|
||||
return None
|
||||
else:
|
||||
logger.info(f"媒体库中已存在:{movies}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
server="emby",
|
||||
itemid=movies[0].item_id
|
||||
)
|
||||
else:
|
||||
tvs = self.emby.get_tv_episodes(title=mediainfo.title,
|
||||
itemid, tvs = self.emby.get_tv_episodes(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
tmdb_id=mediainfo.tmdb_id,
|
||||
item_id=itemid)
|
||||
@ -80,7 +89,12 @@ class EmbyModule(_ModuleBase):
|
||||
return None
|
||||
else:
|
||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
||||
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.TV,
|
||||
seasons=tvs,
|
||||
server="emby",
|
||||
itemid=itemid
|
||||
)
|
||||
|
||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||
"""
|
||||
@ -90,7 +104,7 @@ class EmbyModule(_ModuleBase):
|
||||
:return: 成功或失败
|
||||
"""
|
||||
items = [
|
||||
RefreshMediaItem(
|
||||
schemas.RefreshMediaItem(
|
||||
title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
type=mediainfo.type,
|
||||
@ -105,52 +119,32 @@ class EmbyModule(_ModuleBase):
|
||||
媒体数量统计
|
||||
"""
|
||||
media_statistic = self.emby.get_medias_count()
|
||||
user_count = self.emby.get_user_count()
|
||||
return [schemas.Statistic(
|
||||
movie_count=media_statistic.get("MovieCount") or 0,
|
||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
user_count=user_count or 0
|
||||
)]
|
||||
media_statistic.user_count = self.emby.get_user_count()
|
||||
return [media_statistic]
|
||||
|
||||
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
def mediaserver_librarys(self, server: str) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
if server != "emby":
|
||||
return None
|
||||
librarys = self.emby.get_librarys()
|
||||
if not librarys:
|
||||
return []
|
||||
return [schemas.MediaServerLibrary(
|
||||
server="emby",
|
||||
id=library.get("id"),
|
||||
name=library.get("name"),
|
||||
type=library.get("type"),
|
||||
path=library.get("path")
|
||||
) for library in librarys]
|
||||
return self.emby.get_librarys()
|
||||
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator[schemas.MediaServerItem]]:
|
||||
"""
|
||||
媒体库项目列表
|
||||
"""
|
||||
if server != "emby":
|
||||
return None
|
||||
items = self.emby.get_items(library_id)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
server="emby",
|
||||
library=item.get("library"),
|
||||
item_id=item.get("id"),
|
||||
item_type=item.get("type"),
|
||||
title=item.get("title"),
|
||||
original_title=item.get("original_title"),
|
||||
year=item.get("year"),
|
||||
tmdbid=int(item.get("tmdbid")) if item.get("tmdbid") else None,
|
||||
imdbid=item.get("imdbid"),
|
||||
tvdbid=item.get("tvdbid"),
|
||||
path=item.get("path"),
|
||||
)
|
||||
return self.emby.get_items(library_id)
|
||||
|
||||
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[Generator[schemas.MediaServerItem]]:
|
||||
"""
|
||||
媒体库项目详情
|
||||
"""
|
||||
if server != "emby":
|
||||
return None
|
||||
return self.emby.get_iteminfo(item_id)
|
||||
|
||||
def mediaserver_tv_episodes(self, server: str,
|
||||
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
|
||||
@ -159,7 +153,7 @@ class EmbyModule(_ModuleBase):
|
||||
"""
|
||||
if server != "emby":
|
||||
return None
|
||||
seasoninfo = self.emby.get_tv_episodes(item_id=item_id)
|
||||
_, seasoninfo = self.emby.get_tv_episodes(item_id=item_id)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
return [schemas.MediaServerSeasonInfo(
|
||||
|
@ -1,17 +1,16 @@
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union, Dict, Generator
|
||||
from typing import List, Optional, Union, Dict, Generator, Tuple
|
||||
|
||||
from requests import Response
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas import RefreshMediaItem, WebhookEventInfo
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class Emby(metaclass=Singleton):
|
||||
@ -78,7 +77,7 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接User/Views 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_librarys(self):
|
||||
def get_librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
@ -93,12 +92,15 @@ class Emby(metaclass=Singleton):
|
||||
library_type = MediaType.TV.value
|
||||
case _:
|
||||
continue
|
||||
libraries.append({
|
||||
"id": library.get("Id"),
|
||||
"name": library.get("Name"),
|
||||
"path": library.get("Path"),
|
||||
"type": library_type
|
||||
})
|
||||
libraries.append(
|
||||
schemas.MediaServerLibrary(
|
||||
server="emby",
|
||||
id=library.get("Id"),
|
||||
name=library.get("Name"),
|
||||
path=library.get("Path"),
|
||||
type=library_type
|
||||
)
|
||||
)
|
||||
return libraries
|
||||
|
||||
def get_user(self, user_name: str = None) -> Optional[Union[str, int]]:
|
||||
@ -200,59 +202,29 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接Users/Query出错:" + str(e))
|
||||
return 0
|
||||
|
||||
def get_activity_log(self, num: int = 30) -> List[dict]:
|
||||
"""
|
||||
获取Emby活动记录
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
req_url = "%semby/System/ActivityLog/Entries?api_key=%s&" % (self._host, self._apikey)
|
||||
ret_array = []
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
ret_json = res.json()
|
||||
items = ret_json.get('Items')
|
||||
for item in items:
|
||||
if item.get("Type") == "AuthenticationSucceeded":
|
||||
event_type = "LG"
|
||||
event_date = StringUtils.get_time(item.get("Date"))
|
||||
event_str = "%s, %s" % (item.get("Name"), item.get("ShortOverview"))
|
||||
activity = {"type": event_type, "event": event_str, "date": event_date}
|
||||
ret_array.append(activity)
|
||||
if item.get("Type") in ["VideoPlayback", "VideoPlaybackStopped"]:
|
||||
event_type = "PL"
|
||||
event_date = StringUtils.get_time(item.get("Date"))
|
||||
event_str = item.get("Name")
|
||||
activity = {"type": event_type, "event": event_str, "date": event_date}
|
||||
ret_array.append(activity)
|
||||
else:
|
||||
logger.error(f"System/ActivityLog/Entries 未获取到返回数据")
|
||||
return []
|
||||
except Exception as e:
|
||||
|
||||
logger.error(f"连接System/ActivityLog/Entries出错:" + str(e))
|
||||
return []
|
||||
return ret_array[:num]
|
||||
|
||||
def get_medias_count(self) -> dict:
|
||||
def get_medias_count(self) -> schemas.Statistic:
|
||||
"""
|
||||
获得电影、电视剧、动漫媒体数量
|
||||
:return: MovieCount SeriesCount SongCount
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return {}
|
||||
return schemas.Statistic()
|
||||
req_url = "%semby/Items/Counts?api_key=%s" % (self._host, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
return res.json()
|
||||
result = res.json()
|
||||
return schemas.Statistic(
|
||||
movie_count=result.get("MovieCount") or 0,
|
||||
tv_count=result.get("SeriesCount") or 0,
|
||||
episode_count=result.get("EpisodeCount") or 0
|
||||
)
|
||||
else:
|
||||
logger.error(f"Items/Counts 未获取到返回数据")
|
||||
return {}
|
||||
return schemas.Statistic()
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Counts出错:" + str(e))
|
||||
return {}
|
||||
return schemas.Statistic()
|
||||
|
||||
def __get_emby_series_id_by_name(self, name: str, year: str) -> Optional[str]:
|
||||
"""
|
||||
@ -282,7 +254,7 @@ class Emby(metaclass=Singleton):
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]:
|
||||
"""
|
||||
根据标题和年份,检查电影是否在Emby中存在,存在则返回列表
|
||||
:param title: 标题
|
||||
@ -303,17 +275,28 @@ class Emby(metaclass=Singleton):
|
||||
ret_movies = []
|
||||
for res_item in res_items:
|
||||
item_tmdbid = res_item.get("ProviderIds", {}).get("Tmdb")
|
||||
mediaserver_item = schemas.MediaServerItem(
|
||||
server="emby",
|
||||
library=res_item.get("ParentId"),
|
||||
item_id=res_item.get("Id"),
|
||||
item_type=res_item.get("Type"),
|
||||
title=res_item.get("Name"),
|
||||
original_title=res_item.get("OriginalTitle"),
|
||||
year=res_item.get("ProductionYear"),
|
||||
tmdbid=int(item_tmdbid) if item_tmdbid else None,
|
||||
imdbid=res_item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=res_item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=res_item.get("Path")
|
||||
)
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(item_tmdbid) != str(tmdb_id):
|
||||
continue
|
||||
else:
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
ret_movies.append(mediaserver_item)
|
||||
continue
|
||||
if res_item.get('Name') == title and (
|
||||
not year or str(res_item.get('ProductionYear')) == str(year)):
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
if (mediaserver_item.title == title
|
||||
and (not year or str(mediaserver_item.year) == str(year))):
|
||||
ret_movies.append(mediaserver_item)
|
||||
return ret_movies
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items出错:" + str(e))
|
||||
@ -325,7 +308,8 @@ class Emby(metaclass=Singleton):
|
||||
title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None,
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
season: int = None
|
||||
) -> Tuple[Optional[str], Optional[Dict[int, List[Dict[int, list]]]]]:
|
||||
"""
|
||||
根据标题和年份和季,返回Emby中的剧集列表
|
||||
:param item_id: Emby中的ID
|
||||
@ -336,21 +320,20 @@ class Emby(metaclass=Singleton):
|
||||
:return: 每一季的已有集数
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
return None, None
|
||||
# 电视剧
|
||||
if not item_id:
|
||||
item_id = self.__get_emby_series_id_by_name(title, year)
|
||||
if item_id is None:
|
||||
return None
|
||||
return None, None
|
||||
if not item_id:
|
||||
return {}
|
||||
return None, {}
|
||||
# 验证tmdbid是否相同
|
||||
item_info = self.get_iteminfo(item_id)
|
||||
if item_info:
|
||||
item_tmdbid = (item_info.get("ProviderIds") or {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
return {}
|
||||
if tmdb_id and item_info.tmdbid:
|
||||
if str(tmdb_id) != str(item_info.tmdbid):
|
||||
return None, {}
|
||||
# /Shows/Id/Episodes 查集的信息
|
||||
if not season:
|
||||
season = ""
|
||||
@ -359,7 +342,8 @@ class Emby(metaclass=Singleton):
|
||||
self._host, item_id, season, self._apikey)
|
||||
res_json = RequestUtils().get_res(req_url)
|
||||
if res_json:
|
||||
res_items = res_json.json().get("Items")
|
||||
tv_item = res_json.json()
|
||||
res_items = tv_item.get("Items")
|
||||
season_episodes = {}
|
||||
for res_item in res_items:
|
||||
season_index = res_item.get("ParentIndexNumber")
|
||||
@ -374,11 +358,11 @@ class Emby(metaclass=Singleton):
|
||||
season_episodes[season_index] = []
|
||||
season_episodes[season_index].append(episode_index)
|
||||
# 返回
|
||||
return season_episodes
|
||||
return tv_item.get("Id"), season_episodes
|
||||
except Exception as e:
|
||||
logger.error(f"连接Shows/Id/Episodes出错:" + str(e))
|
||||
return None
|
||||
return {}
|
||||
return None, None
|
||||
return None, {}
|
||||
|
||||
def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]:
|
||||
"""
|
||||
@ -441,7 +425,7 @@ class Emby(metaclass=Singleton):
|
||||
return False
|
||||
return False
|
||||
|
||||
def refresh_library_by_items(self, items: List[RefreshMediaItem]) -> bool:
|
||||
def refresh_library_by_items(self, items: List[schemas.RefreshMediaItem]) -> bool:
|
||||
"""
|
||||
按类型、名称、年份来刷新媒体库
|
||||
:param items: 已识别的需要刷新媒体库的媒体信息列表
|
||||
@ -463,7 +447,7 @@ class Emby(metaclass=Singleton):
|
||||
return self.__refresh_emby_library_by_id(library_id)
|
||||
logger.info(f"Emby媒体库刷新完成")
|
||||
|
||||
def __get_emby_library_id_by_item(self, item: RefreshMediaItem) -> Optional[str]:
|
||||
def __get_emby_library_id_by_item(self, item: schemas.RefreshMediaItem) -> Optional[str]:
|
||||
"""
|
||||
根据媒体信息查询在哪个媒体库,返回要刷新的位置的ID
|
||||
:param item: {title, year, type, category, target_path}
|
||||
@ -499,31 +483,45 @@ class Emby(metaclass=Singleton):
|
||||
# 刷新根目录
|
||||
return "/"
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> dict:
|
||||
def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]:
|
||||
"""
|
||||
获取单个项目详情
|
||||
"""
|
||||
if not itemid:
|
||||
return {}
|
||||
return None
|
||||
if not self._host or not self._apikey:
|
||||
return {}
|
||||
return None
|
||||
req_url = "%semby/Users/%s/Items/%s?api_key=%s" % (self._host, self.user, itemid, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res and res.status_code == 200:
|
||||
return res.json()
|
||||
item = res.json()
|
||||
tmdbid = item.get("ProviderIds", {}).get("Tmdb")
|
||||
return schemas.MediaServerItem(
|
||||
server="emby",
|
||||
library=item.get("ParentId"),
|
||||
item_id=item.get("Id"),
|
||||
item_type=item.get("Type"),
|
||||
title=item.get("Name"),
|
||||
original_title=item.get("OriginalTitle"),
|
||||
year=item.get("ProductionYear"),
|
||||
tmdbid=int(tmdbid) if tmdbid else None,
|
||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=item.get("Path")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Id出错:" + str(e))
|
||||
return {}
|
||||
return None
|
||||
|
||||
def get_items(self, parent: str) -> Generator:
|
||||
def get_items(self, parent: str) -> Generator[Optional[schemas.MediaServerItem]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not parent:
|
||||
yield {}
|
||||
yield None
|
||||
if not self._host or not self._apikey:
|
||||
yield {}
|
||||
yield None
|
||||
req_url = "%semby/Users/%s/Items?ParentId=%s&api_key=%s" % (self._host, self.user, parent, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
@ -533,26 +531,15 @@ class Emby(metaclass=Singleton):
|
||||
if not result:
|
||||
continue
|
||||
if result.get("Type") in ["Movie", "Series"]:
|
||||
item_info = self.get_iteminfo(result.get("Id"))
|
||||
yield {"id": result.get("Id"),
|
||||
"library": item_info.get("ParentId"),
|
||||
"type": item_info.get("Type"),
|
||||
"title": item_info.get("Name"),
|
||||
"original_title": item_info.get("OriginalTitle"),
|
||||
"year": item_info.get("ProductionYear"),
|
||||
"tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"),
|
||||
"imdbid": item_info.get("ProviderIds", {}).get("Imdb"),
|
||||
"tvdbid": item_info.get("ProviderIds", {}).get("Tvdb"),
|
||||
"path": item_info.get("Path"),
|
||||
"json": str(item_info)}
|
||||
yield self.get_iteminfo(result.get("Id"))
|
||||
elif "Folder" in result.get("Type"):
|
||||
for item in self.get_items(parent=result.get('Id')):
|
||||
yield item
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
yield {}
|
||||
yield None
|
||||
|
||||
def get_webhook_message(self, form: any, args: dict) -> Optional[WebhookEventInfo]:
|
||||
def get_webhook_message(self, form: any, args: dict) -> Optional[schemas.WebhookEventInfo]:
|
||||
"""
|
||||
解析Emby Webhook报文
|
||||
电影:
|
||||
@ -805,7 +792,7 @@ class Emby(metaclass=Singleton):
|
||||
if not eventType:
|
||||
return None
|
||||
logger.info(f"接收到emby webhook:{message}")
|
||||
eventItem = WebhookEventInfo(event=eventType, channel="emby")
|
||||
eventItem = schemas.WebhookEventInfo(event=eventType, channel="emby")
|
||||
if message.get('Item'):
|
||||
if message.get('Item', {}).get('Type') == 'Episode':
|
||||
eventItem.item_type = "TV"
|
||||
|
@ -6,7 +6,6 @@ from app.core.context import MediaInfo
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.modules.jellyfin.jellyfin import Jellyfin
|
||||
from app.schemas import ExistMediaInfo, WebhookEventInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
@ -40,7 +39,7 @@ class JellyfinModule(_ModuleBase):
|
||||
# Jellyfin认证
|
||||
return self.jellyfin.authenticate(name, password)
|
||||
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
|
||||
"""
|
||||
解析Webhook报文体
|
||||
:param body: 请求体
|
||||
@ -50,7 +49,7 @@ class JellyfinModule(_ModuleBase):
|
||||
"""
|
||||
return self.jellyfin.get_webhook_message(body)
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]:
|
||||
"""
|
||||
判断媒体文件是否存在
|
||||
:param mediainfo: 识别的媒体信息
|
||||
@ -62,16 +61,24 @@ class JellyfinModule(_ModuleBase):
|
||||
movie = self.jellyfin.get_iteminfo(itemid)
|
||||
if movie:
|
||||
logger.info(f"媒体库中已存在:{movie}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
server="jellyfin",
|
||||
itemid=movie.item_id
|
||||
)
|
||||
movies = self.jellyfin.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id)
|
||||
if not movies:
|
||||
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
|
||||
return None
|
||||
else:
|
||||
logger.info(f"媒体库中已存在:{movies}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
server="jellyfin",
|
||||
itemid=movies[0].item_id
|
||||
)
|
||||
else:
|
||||
tvs = self.jellyfin.get_tv_episodes(title=mediainfo.title,
|
||||
itemid, tvs = self.jellyfin.get_tv_episodes(title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
tmdb_id=mediainfo.tmdb_id,
|
||||
item_id=itemid)
|
||||
@ -80,7 +87,12 @@ class JellyfinModule(_ModuleBase):
|
||||
return None
|
||||
else:
|
||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
||||
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.TV,
|
||||
seasons=tvs,
|
||||
server="jellyfin",
|
||||
itemid=itemid
|
||||
)
|
||||
|
||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||
"""
|
||||
@ -96,13 +108,8 @@ class JellyfinModule(_ModuleBase):
|
||||
媒体数量统计
|
||||
"""
|
||||
media_statistic = self.jellyfin.get_medias_count()
|
||||
user_count = self.jellyfin.get_user_count()
|
||||
return [schemas.Statistic(
|
||||
movie_count=media_statistic.get("MovieCount") or 0,
|
||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
user_count=user_count or 0
|
||||
)]
|
||||
media_statistic.user_count = self.jellyfin.get_user_count()
|
||||
return media_statistic
|
||||
|
||||
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
@ -121,27 +128,21 @@ class JellyfinModule(_ModuleBase):
|
||||
path=library.get("path")
|
||||
) for library in librarys]
|
||||
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator[schemas.MediaServerItem]]:
|
||||
"""
|
||||
媒体库项目列表
|
||||
"""
|
||||
if server != "jellyfin":
|
||||
return None
|
||||
items = self.jellyfin.get_items(library_id)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
server="jellyfin",
|
||||
library=item.get("library"),
|
||||
item_id=item.get("id"),
|
||||
item_type=item.get("type"),
|
||||
title=item.get("title"),
|
||||
original_title=item.get("original_title"),
|
||||
year=item.get("year"),
|
||||
tmdbid=item.get("tmdbid"),
|
||||
imdbid=item.get("imdbid"),
|
||||
tvdbid=item.get("tvdbid"),
|
||||
path=item.get("path"),
|
||||
)
|
||||
return self.jellyfin.get_items(library_id)
|
||||
|
||||
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
||||
"""
|
||||
媒体库项目详情
|
||||
"""
|
||||
if server != "jellyfin":
|
||||
return None
|
||||
return self.jellyfin.get_iteminfo(item_id)
|
||||
|
||||
def mediaserver_tv_episodes(self, server: str,
|
||||
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
|
||||
@ -150,7 +151,7 @@ class JellyfinModule(_ModuleBase):
|
||||
"""
|
||||
if server != "jellyfin":
|
||||
return None
|
||||
seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id)
|
||||
_, seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
return [schemas.MediaServerSeasonInfo(
|
||||
|
@ -1,15 +1,14 @@
|
||||
import json
|
||||
import re
|
||||
from typing import List, Union, Optional, Dict, Generator
|
||||
from typing import List, Union, Optional, Dict, Generator, Tuple
|
||||
|
||||
from requests import Response
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType, WebhookEventInfo
|
||||
from app.schemas import MediaType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
class Jellyfin(metaclass=Singleton):
|
||||
@ -73,12 +72,14 @@ class Jellyfin(metaclass=Singleton):
|
||||
library_type = MediaType.TV.value
|
||||
case _:
|
||||
continue
|
||||
libraries.append({
|
||||
"id": library.get("Id"),
|
||||
"name": library.get("Name"),
|
||||
"path": library.get("Path"),
|
||||
"type": library_type
|
||||
})
|
||||
libraries.append(
|
||||
schemas.MediaServerLibrary(
|
||||
server="emby",
|
||||
id=library.get("Id"),
|
||||
name=library.get("Name"),
|
||||
path=library.get("Path"),
|
||||
type=library_type
|
||||
))
|
||||
return libraries
|
||||
|
||||
def get_user_count(self) -> int:
|
||||
@ -179,59 +180,29 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接System/Info出错:" + str(e))
|
||||
return None
|
||||
|
||||
def get_activity_log(self, num: int = 30) -> List[dict]:
|
||||
"""
|
||||
获取Jellyfin活动记录
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
req_url = "%sSystem/ActivityLog/Entries?api_key=%s&Limit=%s" % (self._host, self._apikey, num)
|
||||
ret_array = []
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
ret_json = res.json()
|
||||
items = ret_json.get('Items')
|
||||
for item in items:
|
||||
if item.get("Type") == "SessionStarted":
|
||||
event_type = "LG"
|
||||
event_date = re.sub(r'\dZ', 'Z', item.get("Date"))
|
||||
event_str = "%s, %s" % (item.get("Name"), item.get("ShortOverview"))
|
||||
activity = {"type": event_type, "event": event_str,
|
||||
"date": StringUtils.get_time(event_date)}
|
||||
ret_array.append(activity)
|
||||
if item.get("Type") in ["VideoPlayback", "VideoPlaybackStopped"]:
|
||||
event_type = "PL"
|
||||
event_date = re.sub(r'\dZ', 'Z', item.get("Date"))
|
||||
activity = {"type": event_type, "event": item.get("Name"),
|
||||
"date": StringUtils.get_time(event_date)}
|
||||
ret_array.append(activity)
|
||||
else:
|
||||
logger.error(f"System/ActivityLog/Entries 未获取到返回数据")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"连接System/ActivityLog/Entries出错:" + str(e))
|
||||
return []
|
||||
return ret_array
|
||||
|
||||
def get_medias_count(self) -> Optional[dict]:
|
||||
def get_medias_count(self) -> schemas.Statistic:
|
||||
"""
|
||||
获得电影、电视剧、动漫媒体数量
|
||||
:return: MovieCount SeriesCount SongCount
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
return schemas.Statistic()
|
||||
req_url = "%sItems/Counts?api_key=%s" % (self._host, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
return res.json()
|
||||
result = res.json()
|
||||
schemas.Statistic(
|
||||
movie_count=result.get("MovieCount") or 0,
|
||||
tv_count=result.get("SeriesCount") or 0,
|
||||
episode_count=result.get("EpisodeCount") or 0
|
||||
)
|
||||
else:
|
||||
logger.error(f"Items/Counts 未获取到返回数据")
|
||||
return {}
|
||||
return schemas.Statistic()
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items/Counts出错:" + str(e))
|
||||
return {}
|
||||
return schemas.Statistic()
|
||||
|
||||
def __get_jellyfin_series_id_by_name(self, name: str, year: str) -> Optional[str]:
|
||||
"""
|
||||
@ -258,7 +229,7 @@ class Jellyfin(metaclass=Singleton):
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]:
|
||||
"""
|
||||
根据标题和年份,检查电影是否在Jellyfin中存在,存在则返回列表
|
||||
:param title: 标题
|
||||
@ -276,19 +247,30 @@ class Jellyfin(metaclass=Singleton):
|
||||
res_items = res.json().get("Items")
|
||||
if res_items:
|
||||
ret_movies = []
|
||||
for res_item in res_items:
|
||||
item_tmdbid = res_item.get("ProviderIds", {}).get("Tmdb")
|
||||
for item in res_items:
|
||||
item_tmdbid = item.get("ProviderIds", {}).get("Tmdb")
|
||||
mediaserver_item = schemas.MediaServerItem(
|
||||
server="emby",
|
||||
library=item.get("ParentId"),
|
||||
item_id=item.get("Id"),
|
||||
item_type=item.get("Type"),
|
||||
title=item.get("Name"),
|
||||
original_title=item.get("OriginalTitle"),
|
||||
year=item.get("ProductionYear"),
|
||||
tmdbid=int(item_tmdbid) if item_tmdbid else None,
|
||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=item.get("Path")
|
||||
)
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(item_tmdbid) != str(tmdb_id):
|
||||
continue
|
||||
else:
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
ret_movies.append(mediaserver_item)
|
||||
continue
|
||||
if res_item.get('Name') == title and (
|
||||
not year or str(res_item.get('ProductionYear')) == str(year)):
|
||||
ret_movies.append(
|
||||
{'title': res_item.get('Name'), 'year': str(res_item.get('ProductionYear'))})
|
||||
if mediaserver_item.title == title and (
|
||||
not year or str(mediaserver_item.year) == str(year)):
|
||||
ret_movies.append(mediaserver_item)
|
||||
return ret_movies
|
||||
except Exception as e:
|
||||
logger.error(f"连接Items出错:" + str(e))
|
||||
@ -300,7 +282,7 @@ class Jellyfin(metaclass=Singleton):
|
||||
title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None,
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
season: int = None) -> Tuple[Optional[str], Optional[Dict[int, list]]]:
|
||||
"""
|
||||
根据标题和年份和季,返回Jellyfin中的剧集列表
|
||||
:param item_id: Jellyfin中的Id
|
||||
@ -311,19 +293,21 @@ class Jellyfin(metaclass=Singleton):
|
||||
:return: 集号的列表
|
||||
"""
|
||||
if not self._host or not self._apikey or not self.user:
|
||||
return None
|
||||
return None, None
|
||||
# 查TVID
|
||||
if not item_id:
|
||||
item_id = self.__get_jellyfin_series_id_by_name(title, year)
|
||||
if item_id is None:
|
||||
return None
|
||||
return None, None
|
||||
if not item_id:
|
||||
return {}
|
||||
return None, {}
|
||||
# 验证tmdbid是否相同
|
||||
item_tmdbid = (self.get_iteminfo(item_id).get("ProviderIds") or {}).get("Tmdb")
|
||||
item_info = self.get_iteminfo(item_id) or {}
|
||||
if item_info:
|
||||
item_tmdbid = (item_info.get("ProviderIds") or {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
return {}
|
||||
return None, {}
|
||||
if not season:
|
||||
season = ""
|
||||
try:
|
||||
@ -331,7 +315,8 @@ class Jellyfin(metaclass=Singleton):
|
||||
self._host, item_id, season, self.user, self._apikey)
|
||||
res_json = RequestUtils().get_res(req_url)
|
||||
if res_json:
|
||||
res_items = res_json.json().get("Items")
|
||||
tv_info = res_json.json()
|
||||
res_items = tv_info.get("Items")
|
||||
# 返回的季集信息
|
||||
season_episodes = {}
|
||||
for res_item in res_items:
|
||||
@ -346,11 +331,11 @@ class Jellyfin(metaclass=Singleton):
|
||||
if not season_episodes.get(season_index):
|
||||
season_episodes[season_index] = []
|
||||
season_episodes[season_index].append(episode_index)
|
||||
return season_episodes
|
||||
return tv_info.get('Id'), season_episodes
|
||||
except Exception as e:
|
||||
logger.error(f"连接Shows/Id/Episodes出错:" + str(e))
|
||||
return None
|
||||
return {}
|
||||
return None, None
|
||||
return None, {}
|
||||
|
||||
def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]:
|
||||
"""
|
||||
@ -394,7 +379,7 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接Library/Refresh出错:" + str(e))
|
||||
return False
|
||||
|
||||
def get_webhook_message(self, body: any) -> Optional[WebhookEventInfo]:
|
||||
def get_webhook_message(self, body: any) -> Optional[schemas.WebhookEventInfo]:
|
||||
"""
|
||||
解析Jellyfin报文
|
||||
{
|
||||
@ -470,7 +455,7 @@ class Jellyfin(metaclass=Singleton):
|
||||
eventType = message.get('NotificationType')
|
||||
if not eventType:
|
||||
return None
|
||||
eventItem = WebhookEventInfo(
|
||||
eventItem = schemas.WebhookEventInfo(
|
||||
event=eventType,
|
||||
channel="jellyfin"
|
||||
)
|
||||
@ -506,32 +491,46 @@ class Jellyfin(metaclass=Singleton):
|
||||
|
||||
return eventItem
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> dict:
|
||||
def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]:
|
||||
"""
|
||||
获取单个项目详情
|
||||
"""
|
||||
if not itemid:
|
||||
return {}
|
||||
return None
|
||||
if not self._host or not self._apikey:
|
||||
return {}
|
||||
return None
|
||||
req_url = "%sUsers/%s/Items/%s?api_key=%s" % (
|
||||
self._host, self.user, itemid, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res and res.status_code == 200:
|
||||
return res.json()
|
||||
item = res.json()
|
||||
tmdbid = item.get("ProviderIds", {}).get("Tmdb")
|
||||
return schemas.MediaServerItem(
|
||||
server="emby",
|
||||
library=item.get("ParentId"),
|
||||
item_id=item.get("Id"),
|
||||
item_type=item.get("Type"),
|
||||
title=item.get("Name"),
|
||||
original_title=item.get("OriginalTitle"),
|
||||
year=item.get("ProductionYear"),
|
||||
tmdbid=int(tmdbid) if tmdbid else None,
|
||||
imdbid=item.get("ProviderIds", {}).get("Imdb"),
|
||||
tvdbid=item.get("ProviderIds", {}).get("Tvdb"),
|
||||
path=item.get("Path")
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
return {}
|
||||
return None
|
||||
|
||||
def get_items(self, parent: str) -> Generator:
|
||||
def get_items(self, parent: str) -> Generator[schemas.MediaServerItem]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not parent:
|
||||
yield {}
|
||||
yield None
|
||||
if not self._host or not self._apikey:
|
||||
yield {}
|
||||
yield None
|
||||
req_url = "%sUsers/%s/Items?parentId=%s&api_key=%s" % (self._host, self.user, parent, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
@ -541,24 +540,13 @@ class Jellyfin(metaclass=Singleton):
|
||||
if not result:
|
||||
continue
|
||||
if result.get("Type") in ["Movie", "Series"]:
|
||||
item_info = self.get_iteminfo(result.get("Id"))
|
||||
yield {"id": result.get("Id"),
|
||||
"library": item_info.get("ParentId"),
|
||||
"type": item_info.get("Type"),
|
||||
"title": item_info.get("Name"),
|
||||
"original_title": item_info.get("OriginalTitle"),
|
||||
"year": item_info.get("ProductionYear"),
|
||||
"tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"),
|
||||
"imdbid": item_info.get("ProviderIds", {}).get("Imdb"),
|
||||
"tvdbid": item_info.get("ProviderIds", {}).get("Tvdb"),
|
||||
"path": item_info.get("Path"),
|
||||
"json": str(item_info)}
|
||||
yield self.get_iteminfo(result.get("Id"))
|
||||
elif "Folder" in result.get("Type"):
|
||||
for item in self.get_items(result.get("Id")):
|
||||
yield item
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
yield {}
|
||||
yield None
|
||||
|
||||
def get_data(self, url: str) -> Optional[Response]:
|
||||
"""
|
||||
|
@ -6,12 +6,10 @@ from app.core.context import MediaInfo
|
||||
from app.log import logger
|
||||
from app.modules import _ModuleBase
|
||||
from app.modules.plex.plex import Plex
|
||||
from app.schemas import ExistMediaInfo, RefreshMediaItem, WebhookEventInfo
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
class PlexModule(_ModuleBase):
|
||||
|
||||
plex: Plex = None
|
||||
|
||||
def init_module(self) -> None:
|
||||
@ -31,7 +29,7 @@ class PlexModule(_ModuleBase):
|
||||
if not self.plex.is_inactive():
|
||||
self.plex.reconnect()
|
||||
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[WebhookEventInfo]:
|
||||
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
|
||||
"""
|
||||
解析Webhook报文体
|
||||
:param body: 请求体
|
||||
@ -41,7 +39,7 @@ class PlexModule(_ModuleBase):
|
||||
"""
|
||||
return self.plex.get_webhook_message(form)
|
||||
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[ExistMediaInfo]:
|
||||
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]:
|
||||
"""
|
||||
判断媒体文件是否存在
|
||||
:param mediainfo: 识别的媒体信息
|
||||
@ -53,7 +51,11 @@ class PlexModule(_ModuleBase):
|
||||
movie = self.plex.get_iteminfo(itemid)
|
||||
if movie:
|
||||
logger.info(f"媒体库中已存在:{movie}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
server="plex",
|
||||
itemid=movie.item_id
|
||||
)
|
||||
movies = self.plex.get_movies(title=mediainfo.title,
|
||||
original_title=mediainfo.original_title,
|
||||
year=mediainfo.year,
|
||||
@ -63,9 +65,13 @@ class PlexModule(_ModuleBase):
|
||||
return None
|
||||
else:
|
||||
logger.info(f"媒体库中已存在:{movies}")
|
||||
return ExistMediaInfo(type=MediaType.MOVIE)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.MOVIE,
|
||||
server="plex",
|
||||
itemid=movies[0].item_id
|
||||
)
|
||||
else:
|
||||
tvs = self.plex.get_tv_episodes(title=mediainfo.title,
|
||||
item_id, tvs = self.plex.get_tv_episodes(title=mediainfo.title,
|
||||
original_title=mediainfo.original_title,
|
||||
year=mediainfo.year,
|
||||
tmdb_id=mediainfo.tmdb_id,
|
||||
@ -75,7 +81,12 @@ class PlexModule(_ModuleBase):
|
||||
return None
|
||||
else:
|
||||
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}")
|
||||
return ExistMediaInfo(type=MediaType.TV, seasons=tvs)
|
||||
return schemas.ExistMediaInfo(
|
||||
type=MediaType.TV,
|
||||
seasons=tvs,
|
||||
server="plex",
|
||||
itemid=item_id
|
||||
)
|
||||
|
||||
def refresh_mediaserver(self, mediainfo: MediaInfo, file_path: Path) -> None:
|
||||
"""
|
||||
@ -85,7 +96,7 @@ class PlexModule(_ModuleBase):
|
||||
:return: 成功或失败
|
||||
"""
|
||||
items = [
|
||||
RefreshMediaItem(
|
||||
schemas.RefreshMediaItem(
|
||||
title=mediainfo.title,
|
||||
year=mediainfo.year,
|
||||
type=mediainfo.type,
|
||||
@ -100,12 +111,8 @@ class PlexModule(_ModuleBase):
|
||||
媒体数量统计
|
||||
"""
|
||||
media_statistic = self.plex.get_medias_count()
|
||||
return [schemas.Statistic(
|
||||
movie_count=media_statistic.get("MovieCount") or 0,
|
||||
tv_count=media_statistic.get("SeriesCount") or 0,
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
user_count=1
|
||||
)]
|
||||
media_statistic.user_count = 1
|
||||
return [media_statistic]
|
||||
|
||||
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
@ -113,38 +120,23 @@ class PlexModule(_ModuleBase):
|
||||
"""
|
||||
if server != "plex":
|
||||
return None
|
||||
librarys = self.plex.get_librarys()
|
||||
if not librarys:
|
||||
return []
|
||||
return [schemas.MediaServerLibrary(
|
||||
server="plex",
|
||||
id=library.get("id"),
|
||||
name=library.get("name"),
|
||||
type=library.get("type"),
|
||||
path=library.get("path")
|
||||
) for library in librarys]
|
||||
return self.plex.get_librarys()
|
||||
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator[schemas.MediaServerItem]]:
|
||||
"""
|
||||
媒体库项目列表
|
||||
"""
|
||||
if server != "plex":
|
||||
return None
|
||||
items = self.plex.get_items(library_id)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
server="plex",
|
||||
library=item.get("library"),
|
||||
item_id=item.get("id"),
|
||||
item_type=item.get("type"),
|
||||
title=item.get("title"),
|
||||
original_title=item.get("original_title"),
|
||||
year=item.get("year"),
|
||||
tmdbid=item.get("tmdbid"),
|
||||
imdbid=item.get("imdbid"),
|
||||
tvdbid=item.get("tvdbid"),
|
||||
path=item.get("path"),
|
||||
)
|
||||
return self.plex.get_items(library_id)
|
||||
|
||||
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
||||
"""
|
||||
媒体库项目详情
|
||||
"""
|
||||
if server != "plex":
|
||||
return None
|
||||
return self.plex.get_iteminfo(item_id)
|
||||
|
||||
def mediaserver_tv_episodes(self, server: str,
|
||||
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
|
||||
@ -153,7 +145,7 @@ class PlexModule(_ModuleBase):
|
||||
"""
|
||||
if server != "plex":
|
||||
return None
|
||||
seasoninfo = self.plex.get_tv_episodes(item_id=item_id)
|
||||
_, seasoninfo = self.plex.get_tv_episodes(item_id=item_id)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
return [schemas.MediaServerSeasonInfo(
|
||||
|
@ -6,9 +6,10 @@ from urllib.parse import quote_plus
|
||||
from plexapi import media
|
||||
from plexapi.server import PlexServer
|
||||
|
||||
from app import schemas
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas import RefreshMediaItem, MediaType, WebhookEventInfo
|
||||
from app.schemas import MediaType
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
@ -49,7 +50,7 @@ class Plex(metaclass=Singleton):
|
||||
self._plex = None
|
||||
logger.error(f"Plex服务器连接失败:{str(e)}")
|
||||
|
||||
def get_librarys(self):
|
||||
def get_librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
@ -69,81 +70,42 @@ class Plex(metaclass=Singleton):
|
||||
library_type = MediaType.TV.value
|
||||
case _:
|
||||
continue
|
||||
libraries.append({
|
||||
"id": library.key,
|
||||
"name": library.title,
|
||||
"path": library.locations,
|
||||
"type": library_type
|
||||
})
|
||||
libraries.append(
|
||||
schemas.MediaServerLibrary(
|
||||
id=library.key,
|
||||
name=library.title,
|
||||
path=library.locations,
|
||||
type=library_type
|
||||
)
|
||||
)
|
||||
return libraries
|
||||
|
||||
def get_activity_log(self, num: int = 30) -> Optional[List[dict]]:
|
||||
"""
|
||||
获取Plex活动记录
|
||||
"""
|
||||
if not self._plex:
|
||||
return []
|
||||
ret_array = []
|
||||
try:
|
||||
# type的含义: 1 电影 4 剧集单集 详见 plexapi/utils.py中SEARCHTYPES的定义
|
||||
# 根据最后播放时间倒序获取数据
|
||||
historys = self._plex.library.search(sort='lastViewedAt:desc', limit=num, type='1,4')
|
||||
for his in historys:
|
||||
# 过滤掉最后播放时间为空的
|
||||
if his.lastViewedAt:
|
||||
if his.type == "episode":
|
||||
event_title = "%s %s%s %s" % (
|
||||
his.grandparentTitle,
|
||||
"S" + str(his.parentIndex),
|
||||
"E" + str(his.index),
|
||||
his.title
|
||||
)
|
||||
event_str = "开始播放剧集 %s" % event_title
|
||||
else:
|
||||
event_title = "%s %s" % (
|
||||
his.title, "(" + str(his.year) + ")")
|
||||
event_str = "开始播放电影 %s" % event_title
|
||||
|
||||
event_type = "PL"
|
||||
event_date = his.lastViewedAt.strftime('%Y-%m-%d %H:%M:%S')
|
||||
activity = {"type": event_type, "event": event_str, "date": event_date}
|
||||
ret_array.append(activity)
|
||||
except Exception as e:
|
||||
logger.error(f"连接System/ActivityLog/Entries出错:" + str(e))
|
||||
return []
|
||||
if ret_array:
|
||||
ret_array = sorted(ret_array, key=lambda x: x['date'], reverse=True)
|
||||
return ret_array
|
||||
|
||||
def get_medias_count(self) -> dict:
|
||||
def get_medias_count(self) -> schemas.Statistic:
|
||||
"""
|
||||
获得电影、电视剧、动漫媒体数量
|
||||
:return: MovieCount SeriesCount SongCount
|
||||
"""
|
||||
if not self._plex:
|
||||
return {}
|
||||
return schemas.Statistic()
|
||||
sections = self._plex.library.sections()
|
||||
MovieCount = SeriesCount = SongCount = EpisodeCount = 0
|
||||
MovieCount = SeriesCount = EpisodeCount = 0
|
||||
for sec in sections:
|
||||
if sec.type == "movie":
|
||||
MovieCount += sec.totalSize
|
||||
if sec.type == "show":
|
||||
SeriesCount += sec.totalSize
|
||||
EpisodeCount += sec.totalViewSize(libtype='episode')
|
||||
if sec.type == "artist":
|
||||
SongCount += sec.totalSize
|
||||
return {
|
||||
"MovieCount": MovieCount,
|
||||
"SeriesCount": SeriesCount,
|
||||
"SongCount": SongCount,
|
||||
"EpisodeCount": EpisodeCount
|
||||
}
|
||||
return schemas.Statistic(
|
||||
movie_count=MovieCount,
|
||||
tv_count=SeriesCount,
|
||||
episode_count=EpisodeCount
|
||||
)
|
||||
|
||||
def get_movies(self,
|
||||
title: str,
|
||||
original_title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None) -> Optional[List[dict]]:
|
||||
tmdb_id: int = None) -> Optional[List[schemas.MediaServerItem]]:
|
||||
"""
|
||||
根据标题和年份,检查电影是否在Plex中存在,存在则返回列表
|
||||
:param title: 标题
|
||||
@ -163,13 +125,30 @@ class Plex(metaclass=Singleton):
|
||||
else:
|
||||
movies = self._plex.library.search(title=title, libtype="movie")
|
||||
if original_title and str(original_title) != str(title):
|
||||
movies.extend(self._plex.library.search(title=original_title, year=year, libtype="movie"))
|
||||
for movie in set(movies):
|
||||
movie_tmdbid = self.__get_ids(movie.guids).get("tmdb_id")
|
||||
if tmdb_id and movie_tmdbid:
|
||||
if str(movie_tmdbid) != str(tmdb_id):
|
||||
movies.extend(self._plex.library.search(title=original_title, libtype="movie"))
|
||||
for item in set(movies):
|
||||
ids = self.__get_ids(item.guids)
|
||||
if tmdb_id and ids['tmdb_id']:
|
||||
if str(ids['tmdb_id']) != str(tmdb_id):
|
||||
continue
|
||||
ret_movies.append({'title': movie.title, 'year': movie.year})
|
||||
path = None
|
||||
if item.locations:
|
||||
path = item.locations[0]
|
||||
ret_movies.append(
|
||||
schemas.MediaServerItem(
|
||||
server="plex",
|
||||
library=item.librarySectionID,
|
||||
item_id=item.key,
|
||||
item_type=item.type,
|
||||
title=item.title,
|
||||
original_title=item.originalTitle,
|
||||
year=item.year,
|
||||
tmdbid=ids['tmdb_id'],
|
||||
imdbid=ids['imdb_id'],
|
||||
tvdbid=ids['tvdb_id'],
|
||||
path=path,
|
||||
)
|
||||
)
|
||||
return ret_movies
|
||||
|
||||
def get_tv_episodes(self,
|
||||
@ -178,7 +157,7 @@ class Plex(metaclass=Singleton):
|
||||
original_title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None,
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
season: int = None) -> Tuple[Optional[str], Optional[Dict[int, list]]]:
|
||||
"""
|
||||
根据标题、年份、季查询电视剧所有集信息
|
||||
:param item_id: 媒体ID
|
||||
@ -190,7 +169,7 @@ class Plex(metaclass=Singleton):
|
||||
:return: 所有集的列表
|
||||
"""
|
||||
if not self._plex:
|
||||
return {}
|
||||
return None, {}
|
||||
if item_id:
|
||||
videos = self._plex.fetchItem(item_id)
|
||||
else:
|
||||
@ -199,13 +178,13 @@ class Plex(metaclass=Singleton):
|
||||
if not videos and original_title and str(original_title) != str(title):
|
||||
videos = self._plex.library.search(title=original_title, year=year, libtype="show")
|
||||
if not videos:
|
||||
return {}
|
||||
return None, {}
|
||||
if isinstance(videos, list):
|
||||
videos = videos[0]
|
||||
video_tmdbid = self.__get_ids(videos.guids).get('tmdb_id')
|
||||
if tmdb_id and video_tmdbid:
|
||||
if str(video_tmdbid) != str(tmdb_id):
|
||||
return {}
|
||||
return None, {}
|
||||
episodes = videos.episodes()
|
||||
season_episodes = {}
|
||||
for episode in episodes:
|
||||
@ -214,7 +193,7 @@ class Plex(metaclass=Singleton):
|
||||
if episode.seasonNumber not in season_episodes:
|
||||
season_episodes[episode.seasonNumber] = []
|
||||
season_episodes[episode.seasonNumber].append(episode.index)
|
||||
return season_episodes
|
||||
return videos.key, season_episodes
|
||||
|
||||
def get_remote_image_by_id(self, item_id: str, image_type: str) -> Optional[str]:
|
||||
"""
|
||||
@ -245,7 +224,7 @@ class Plex(metaclass=Singleton):
|
||||
return False
|
||||
return self._plex.library.update()
|
||||
|
||||
def refresh_library_by_items(self, items: List[RefreshMediaItem]) -> bool:
|
||||
def refresh_library_by_items(self, items: List[schemas.RefreshMediaItem]) -> bool:
|
||||
"""
|
||||
按路径刷新媒体库 item: target_path
|
||||
"""
|
||||
@ -294,19 +273,34 @@ class Plex(metaclass=Singleton):
|
||||
logger.error(f"查找媒体库出错:{err}")
|
||||
return "", ""
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> dict:
|
||||
def get_iteminfo(self, itemid: str) -> Optional[schemas.MediaServerItem]:
|
||||
"""
|
||||
获取单个项目详情
|
||||
"""
|
||||
if not self._plex:
|
||||
return {}
|
||||
return None
|
||||
try:
|
||||
item = self._plex.fetchItem(itemid)
|
||||
ids = self.__get_ids(item.guids)
|
||||
return {'ProviderIds': {'Tmdb': ids['tmdb_id'], 'Imdb': ids['imdb_id']}}
|
||||
path = None
|
||||
if item.locations:
|
||||
path = item.locations[0]
|
||||
return schemas.MediaServerItem(
|
||||
server="plex",
|
||||
library=item.librarySectionID,
|
||||
item_id=item.key,
|
||||
item_type=item.type,
|
||||
title=item.title,
|
||||
original_title=item.originalTitle,
|
||||
year=item.year,
|
||||
tmdbid=ids['tmdb_id'],
|
||||
imdbid=ids['imdb_id'],
|
||||
tvdbid=ids['tvdb_id'],
|
||||
path=path,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"获取项目详情出错:{err}")
|
||||
return {}
|
||||
return None
|
||||
|
||||
@staticmethod
|
||||
def __get_ids(guids: List[Any]) -> dict:
|
||||
@ -332,14 +326,14 @@ class Plex(metaclass=Singleton):
|
||||
break
|
||||
return ids
|
||||
|
||||
def get_items(self, parent: str) -> Generator:
|
||||
def get_items(self, parent: str) -> Generator[Optional[schemas.MediaServerItem]]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not parent:
|
||||
yield {}
|
||||
yield None
|
||||
if not self._plex:
|
||||
yield {}
|
||||
yield None
|
||||
try:
|
||||
section = self._plex.library.sectionByID(int(parent))
|
||||
if section:
|
||||
@ -350,21 +344,24 @@ class Plex(metaclass=Singleton):
|
||||
path = None
|
||||
if item.locations:
|
||||
path = item.locations[0]
|
||||
yield {"id": item.key,
|
||||
"library": item.librarySectionID,
|
||||
"type": item.type,
|
||||
"title": item.title,
|
||||
"original_title": item.originalTitle,
|
||||
"year": item.year,
|
||||
"tmdbid": ids['tmdb_id'],
|
||||
"imdbid": ids['imdb_id'],
|
||||
"tvdbid": ids['tvdb_id'],
|
||||
"path": path}
|
||||
yield schemas.MediaServerItem(
|
||||
server="plex",
|
||||
library=item.librarySectionID,
|
||||
item_id=item.key,
|
||||
item_type=item.type,
|
||||
title=item.title,
|
||||
original_title=item.originalTitle,
|
||||
year=item.year,
|
||||
tmdbid=ids['tmdb_id'],
|
||||
imdbid=ids['imdb_id'],
|
||||
tvdbid=ids['tvdb_id'],
|
||||
path=path,
|
||||
)
|
||||
except Exception as err:
|
||||
logger.error(f"获取媒体库列表出错:{err}")
|
||||
yield {}
|
||||
yield None
|
||||
|
||||
def get_webhook_message(self, form: any) -> Optional[WebhookEventInfo]:
|
||||
def get_webhook_message(self, form: any) -> Optional[schemas.WebhookEventInfo]:
|
||||
"""
|
||||
解析Plex报文
|
||||
eventItem 字段的含义
|
||||
@ -482,7 +479,7 @@ class Plex(metaclass=Singleton):
|
||||
if not eventType:
|
||||
return None
|
||||
logger.info(f"接收到plex webhook:{message}")
|
||||
eventItem = WebhookEventInfo(event=eventType, channel="plex")
|
||||
eventItem = schemas.WebhookEventInfo(event=eventType, channel="plex")
|
||||
if message.get('Metadata'):
|
||||
if message.get('Metadata', {}).get('type') == 'episode':
|
||||
eventItem.item_type = "TV"
|
||||
|
@ -420,8 +420,7 @@ class BestFilmVersion(_PluginBase):
|
||||
item_info_resp = Emby().get_iteminfo(itemid=data.get('Id'))
|
||||
else:
|
||||
item_info_resp = self.plex_get_iteminfo(itemid=data.get('Id'))
|
||||
|
||||
logger.info(f'BestFilmVersion插件 item打印 {item_info_resp}')
|
||||
logger.debug(f'BestFilmVersion插件 item打印 {item_info_resp}')
|
||||
if not item_info_resp:
|
||||
continue
|
||||
|
||||
@ -430,19 +429,13 @@ class BestFilmVersion(_PluginBase):
|
||||
continue
|
||||
|
||||
# 获取tmdb_id
|
||||
media_info_ids = item_info_resp.get('ExternalUrls')
|
||||
if not media_info_ids:
|
||||
tmdb_id = item_info_resp.tmdbid
|
||||
if not tmdb_id:
|
||||
continue
|
||||
for media_info_id in media_info_ids:
|
||||
if 'TheMovieDb' != media_info_id.get('Name'):
|
||||
continue
|
||||
tmdb_find_id = str(media_info_id.get('Url')).split('/')
|
||||
tmdb_find_id.reverse()
|
||||
tmdb_id = tmdb_find_id[0]
|
||||
# 识别媒体信息
|
||||
mediainfo: MediaInfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbID:{tmdb_id}')
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.get("Name")},tmdbid:{tmdb_id}')
|
||||
continue
|
||||
# 添加订阅
|
||||
self.subscribechain.add(mtype=MediaType.MOVIE,
|
||||
@ -634,52 +627,34 @@ class BestFilmVersion(_PluginBase):
|
||||
if not _is_lock:
|
||||
return
|
||||
try:
|
||||
|
||||
mediainfo: Optional[MediaInfo] = None
|
||||
if not data.tmdb_id:
|
||||
info = None
|
||||
if data.channel == 'jellyfin' and data.save_reason == 'UpdateUserRating' and data.item_favorite:
|
||||
if (data.channel == 'jellyfin'
|
||||
and data.save_reason == 'UpdateUserRating'
|
||||
and data.item_favorite):
|
||||
info = Jellyfin().get_iteminfo(itemid=data.item_id)
|
||||
elif data.channel == 'emby' and data.event == 'item.rate':
|
||||
info = Emby().get_iteminfo(itemid=data.item_id)
|
||||
elif data.channel == 'plex' and data.event == 'item.rate':
|
||||
info = Plex().get_iteminfo(itemid=data.item_id)
|
||||
logger.info(f'BestFilmVersion/webhook_message_action item打印:{info}')
|
||||
|
||||
logger.debug(f'BestFilmVersion/webhook_message_action item打印:{info}')
|
||||
if not info:
|
||||
return
|
||||
if info['Type'] not in ['Movie', 'MOV', 'movie']:
|
||||
if info.item_type not in ['Movie', 'MOV', 'movie']:
|
||||
return
|
||||
|
||||
# 获取tmdb_id
|
||||
media_info_ids = info.get('ExternalUrls')
|
||||
if not media_info_ids:
|
||||
return
|
||||
for media_info_id in media_info_ids:
|
||||
|
||||
if 'TheMovieDb' != media_info_id.get('Name'):
|
||||
continue
|
||||
|
||||
tmdb_find_id = str(media_info_id.get('Url')).split('/')
|
||||
tmdb_find_id.reverse()
|
||||
tmdb_id = tmdb_find_id[0]
|
||||
|
||||
mediainfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.item_name},tmdbID:{tmdb_id}')
|
||||
return
|
||||
tmdb_id = info.tmdbid
|
||||
else:
|
||||
if data.channel == 'jellyfin' and (data.save_reason != 'UpdateUserRating' or not data.item_favorite):
|
||||
tmdb_id = data.tmdb_id
|
||||
if (data.channel == 'jellyfin'
|
||||
and (data.save_reason != 'UpdateUserRating' or not data.item_favorite)):
|
||||
return
|
||||
if data.item_type not in ['Movie', 'MOV', 'movie']:
|
||||
return
|
||||
|
||||
mediainfo = self.chain.recognize_media(tmdbid=data.tmdb_id, mtype=MediaType.MOVIE)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.item_name},tmdbID:{data.tmdb_id}')
|
||||
return
|
||||
|
||||
# 识别媒体信息
|
||||
mediainfo = self.chain.recognize_media(tmdbid=tmdb_id, mtype=MediaType.MOVIE)
|
||||
if not mediainfo:
|
||||
logger.warn(f'未识别到媒体信息,标题:{data.item_name},tmdbID:{tmdb_id}')
|
||||
return
|
||||
# 读取缓存
|
||||
caches = self._cache_path.read_text().split("\n") if self._cache_path.exists() else []
|
||||
|
@ -1,19 +1,18 @@
|
||||
import os
|
||||
from pathlib import Path
|
||||
import threading
|
||||
import time
|
||||
from typing import Any, List, Dict, Tuple
|
||||
|
||||
from requests import RequestException
|
||||
from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from apscheduler.triggers.cron import CronTrigger
|
||||
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.tmdb import TmdbChain
|
||||
from app.core.config import settings
|
||||
from app.core.event import eventmanager, Event
|
||||
from app.helper.nfo import NfoReader
|
||||
from app.log import logger
|
||||
from app.plugins import _PluginBase
|
||||
from app.schemas import TransferInfo, MediaInfo
|
||||
from app.schemas import MediaInfo, MediaServerItem
|
||||
from app.schemas.types import EventType, MediaType
|
||||
from app.utils.common import retry
|
||||
from app.utils.http import RequestUtils
|
||||
|
||||
|
||||
class PersonMeta(_PluginBase):
|
||||
@ -38,16 +37,64 @@ class PersonMeta(_PluginBase):
|
||||
# 可使用的用户级别
|
||||
auth_level = 1
|
||||
|
||||
# 退出事件
|
||||
_event = threading.Event()
|
||||
|
||||
# 私有属性
|
||||
_scheduler = None
|
||||
tmdbchain = None
|
||||
mschain = None
|
||||
_enabled = False
|
||||
_metadir = ""
|
||||
_onlyonce = False
|
||||
_cron = None
|
||||
_delay = 0
|
||||
|
||||
def init_plugin(self, config: dict = None):
|
||||
self.tmdbchain = TmdbChain(self.db)
|
||||
self.mschain = MediaServerChain(self.db)
|
||||
if config:
|
||||
self._enabled = config.get("enabled")
|
||||
self._metadir = config.get("metadir")
|
||||
self._onlyonce = config.get("onlyonce")
|
||||
self._cron = config.get("cron")
|
||||
self._delay = config.get("delay") or 0
|
||||
|
||||
# 停止现有任务
|
||||
self.stop_service()
|
||||
|
||||
# 启动服务
|
||||
if self._enabled or self._onlyonce:
|
||||
self._scheduler = BackgroundScheduler(timezone=settings.TZ)
|
||||
if self._cron:
|
||||
logger.info(f"演职人员刮削服务启动,周期:{self._cron}")
|
||||
try:
|
||||
self._scheduler.add_job(func=self.scrap_library,
|
||||
trigger=CronTrigger.from_crontab(self._cron),
|
||||
name="演职人员刮削")
|
||||
except Exception as e:
|
||||
logger.error(f"演职人员刮削服务启动失败,错误信息:{str(e)}")
|
||||
self.systemmessage.put(f"演职人员刮削服务启动失败,错误信息:{str(e)}")
|
||||
|
||||
if self._onlyonce:
|
||||
# 关闭一次性开关
|
||||
self._onlyonce = False
|
||||
# 保存配置
|
||||
self.__update_config()
|
||||
|
||||
if self._scheduler.get_jobs():
|
||||
# 启动服务
|
||||
self._scheduler.print_jobs()
|
||||
self._scheduler.start()
|
||||
|
||||
def __update_config(self):
|
||||
"""
|
||||
更新配置
|
||||
"""
|
||||
self.update_config({
|
||||
"enabled": self._enabled,
|
||||
"onlyonce": self._onlyonce,
|
||||
"cron": self._cron,
|
||||
"delay": self._delay
|
||||
})
|
||||
|
||||
def get_state(self) -> bool:
|
||||
return self._enabled
|
||||
@ -85,6 +132,22 @@ class PersonMeta(_PluginBase):
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VSwitch',
|
||||
'props': {
|
||||
'model': 'onlyonce',
|
||||
'label': '立即运行一次',
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -95,14 +158,32 @@ class PersonMeta(_PluginBase):
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'metadir',
|
||||
'label': '人物元数据目录',
|
||||
'placeholder': '/metadata/people'
|
||||
'model': 'cron',
|
||||
'label': '媒体库扫描周期',
|
||||
'placeholder': '5位cron表达式'
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
'component': 'VCol',
|
||||
'props': {
|
||||
'cols': 12,
|
||||
'md': 6
|
||||
},
|
||||
'content': [
|
||||
{
|
||||
'component': 'VTextField',
|
||||
'props': {
|
||||
'model': 'delay',
|
||||
'label': '入库延迟时间(秒)',
|
||||
'placeholder': '30'
|
||||
}
|
||||
}
|
||||
]
|
||||
@ -113,7 +194,9 @@ class PersonMeta(_PluginBase):
|
||||
}
|
||||
], {
|
||||
"enabled": False,
|
||||
"metadir": ""
|
||||
"onlyonce": False,
|
||||
"cron": "",
|
||||
"delay": 30
|
||||
}
|
||||
|
||||
def get_page(self) -> List[dict]:
|
||||
@ -126,103 +209,98 @@ class PersonMeta(_PluginBase):
|
||||
"""
|
||||
if not self._enabled:
|
||||
return
|
||||
# 下载人物头像
|
||||
if not self._metadir:
|
||||
logger.warning("人物元数据目录未配置,无法下载人物头像")
|
||||
return
|
||||
# 事件数据
|
||||
mediainfo: MediaInfo = event.event_data.get("mediainfo")
|
||||
transferinfo: TransferInfo = event.event_data.get("transferinfo")
|
||||
if not mediainfo or not transferinfo:
|
||||
if not mediainfo:
|
||||
return
|
||||
# 文件路径
|
||||
if not transferinfo.file_list_new:
|
||||
# 延迟
|
||||
if self._delay:
|
||||
time.sleep(int(self._delay))
|
||||
# 查询媒体服务器中的条目
|
||||
existsinfo = self.chain.media_exists(mediainfo=mediainfo)
|
||||
if not existsinfo or not existsinfo.itemid:
|
||||
logger.warn(f"演职人员刮削 {mediainfo.title_year} 在媒体库中不存在")
|
||||
return
|
||||
filepath = Path(transferinfo.file_list_new[0])
|
||||
# 电影
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
# nfo文件
|
||||
nfofile = filepath.with_name("movie.nfo")
|
||||
if not nfofile.exists():
|
||||
nfofile = filepath.with_name(f"{filepath.stem}.nfo")
|
||||
if not nfofile.exists():
|
||||
logger.warning(f"演职人员刮削 电影nfo文件不存在:{nfofile}")
|
||||
# 初始化媒体服务器
|
||||
if existsinfo.server == "plex":
|
||||
logger.warn(f"演职人员刮削 不支持{existsinfo.server}媒体服务器")
|
||||
return
|
||||
else:
|
||||
# nfo文件
|
||||
nfofile = filepath.parent.with_name("tvshow.nfo")
|
||||
if not nfofile.exists():
|
||||
logger.warning(f"演职人员刮削 剧集nfo文件不存在:{nfofile}")
|
||||
# 查询条目详情
|
||||
iteminfo = self.mschain.iteminfo(server=existsinfo.server, item_id=existsinfo.itemid)
|
||||
if not iteminfo:
|
||||
logger.warn(f"演职人员刮削 {mediainfo.title_year} 条目详情获取失败")
|
||||
return
|
||||
logger.info(f"演职人员刮削 开始刮削:{filepath}")
|
||||
# 主要媒体服务器
|
||||
mediaserver = str(settings.MEDIASERVER).split(",")[0]
|
||||
# 读取nfo文件
|
||||
nfo = NfoReader(nfofile)
|
||||
# 读取演员信息
|
||||
actors = nfo.get_elements("actor") or []
|
||||
for actor in actors:
|
||||
# 演员ID
|
||||
actor_id = actor.find("tmdbid").text
|
||||
if not actor_id:
|
||||
continue
|
||||
# 演员名称
|
||||
actor_name = actor.find("name").text
|
||||
if not actor_name:
|
||||
continue
|
||||
# 查询演员详情
|
||||
actor_info = self.tmdbchain.person_detail(int(actor_id))
|
||||
if not actor_info:
|
||||
continue
|
||||
# 演员头像
|
||||
actor_image = actor_info.get("profile_path")
|
||||
if not actor_image:
|
||||
continue
|
||||
# 计算保存目录
|
||||
if mediaserver == 'jellyfin':
|
||||
pers_path = Path(self._metadir) / f"{actor_name[0]}" / f"{actor_name}"
|
||||
else:
|
||||
pers_path = Path(self._metadir) / f"{actor_name}-tmdb-{actor_id}"
|
||||
# 创建目录
|
||||
if not pers_path.exists():
|
||||
os.makedirs(pers_path, exist_ok=True)
|
||||
# 文件路径
|
||||
image_path = pers_path / f"folder{Path(actor_image).suffix}"
|
||||
if image_path.exists():
|
||||
continue
|
||||
# 下载图片
|
||||
self.download_image(
|
||||
image_url=f"https://image.tmdb.org/t/p/original{actor_image}",
|
||||
path=image_path
|
||||
)
|
||||
# 刷新媒体库
|
||||
self.chain.refresh_mediaserver(
|
||||
mediainfo=mediainfo,
|
||||
file_path=filepath
|
||||
)
|
||||
logger.info(f"演职人员刮削 刮削完成:{filepath}")
|
||||
# 刮削演职人员信息
|
||||
self.__update_item(server=existsinfo.server, item=iteminfo, mediainfo=mediainfo)
|
||||
|
||||
@staticmethod
|
||||
@retry(RequestException, logger=logger)
|
||||
def download_image(image_url: str, path: Path):
|
||||
def scrap_library(self):
|
||||
"""
|
||||
下载图片,保存到指定路径
|
||||
扫描整个媒体库,刮削演员信息
|
||||
"""
|
||||
try:
|
||||
logger.info(f"正在下载演职人员图片:{image_url} ...")
|
||||
r = RequestUtils().get_res(url=image_url, raise_exception=True)
|
||||
if r:
|
||||
path.write_bytes(r.content)
|
||||
logger.info(f"图片已保存:{path}")
|
||||
else:
|
||||
logger.info(f"图片下载失败,请检查网络连通性:{image_url}")
|
||||
except RequestException as err:
|
||||
raise err
|
||||
except Exception as err:
|
||||
logger.error(f"图片下载失败:{err}")
|
||||
# 所有媒体服务器
|
||||
if not settings.MEDIASERVER:
|
||||
return
|
||||
for server in settings.MEDIASERVER.split(","):
|
||||
if server == "plex":
|
||||
logger.warn(f"演职人员刮削 不支持{server}媒体服务器")
|
||||
continue
|
||||
# 扫描所有媒体库
|
||||
logger.info(f"开始刮削服务器 {server} 的演员信息 ...")
|
||||
for library in self.mschain.librarys(server):
|
||||
logger.info(f"开始刮削媒体库 {library.name} 的演员信息 ...")
|
||||
for item in self.mschain.items(server, library.id):
|
||||
if not item:
|
||||
continue
|
||||
if not item.item_id:
|
||||
continue
|
||||
if self._event.is_set():
|
||||
logger.info(f"演职人员刮削服务停止")
|
||||
return
|
||||
# 处理条目
|
||||
logger.info(f"开始刮削 {item.title} 的演员信息 ...")
|
||||
self.__update_item(server=server, item=item)
|
||||
logger.info(f"{item.title} 的演员信息刮削完成")
|
||||
logger.info(f"媒体库 {library.name} 的演员信息刮削完成")
|
||||
logger.info(f"服务器 {server} 的演员信息刮削完成")
|
||||
|
||||
def __update_item(self, server: str, item: MediaServerItem, mediainfo: MediaInfo = None):
|
||||
"""
|
||||
更新媒体服务器中的条目
|
||||
"""
|
||||
# 识别媒体信息
|
||||
if not mediainfo:
|
||||
if not item.tmdbid:
|
||||
logger.warn(f"{item.title} 未找到tmdbid,无法识别媒体信息")
|
||||
return
|
||||
mtype = MediaType.TV if item.item_type in ['Series', 'show'] else MediaType.MOVIE
|
||||
mediainfo = self.chain.recognize_media(mtype=mtype, tmdbid=item.tmdbid)
|
||||
if not mediainfo:
|
||||
logger.warn(f"{item.title} 未识别到媒体信息")
|
||||
return
|
||||
# 搜索豆瓣词条
|
||||
|
||||
# 搜索豆瓣人物信息
|
||||
|
||||
# 匹配非中文人名
|
||||
|
||||
# 更新中文人名
|
||||
|
||||
# 下载图片
|
||||
|
||||
# 更新演员图片
|
||||
pass
|
||||
|
||||
def stop_service(self):
|
||||
"""
|
||||
退出插件
|
||||
停止服务
|
||||
"""
|
||||
pass
|
||||
try:
|
||||
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:
|
||||
print(str(e))
|
||||
|
@ -14,6 +14,10 @@ class ExistMediaInfo(BaseModel):
|
||||
type: Optional[MediaType]
|
||||
# 季
|
||||
seasons: Optional[Dict[int, list]] = {}
|
||||
# 媒体服务器
|
||||
server: Optional[str] = None
|
||||
# 媒体ID
|
||||
itemid: Optional[Union[str, int]] = None
|
||||
|
||||
|
||||
class NotExistMediaInfo(BaseModel):
|
||||
|
Loading…
x
Reference in New Issue
Block a user