fix mediaservers

This commit is contained in:
jxxghp
2024-07-02 10:03:56 +08:00
parent 9a07d88d41
commit dfc5872087
7 changed files with 408 additions and 267 deletions
+16 -16
View File
@@ -4,8 +4,8 @@ from typing import List, Union, Optional
from app import schemas from app import schemas
from app.chain import ChainBase from app.chain import ChainBase
from app.core.config import settings
from app.db.mediaserver_oper import MediaServerOper from app.db.mediaserver_oper import MediaServerOper
from app.helper.mediaserver import MediaServerHelper
from app.log import logger from app.log import logger
lock = threading.Lock() lock = threading.Lock()
@@ -19,8 +19,9 @@ class MediaServerChain(ChainBase):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self.dboper = MediaServerOper() self.dboper = MediaServerOper()
self.mediaserverhelper = MediaServerHelper()
def librarys(self, server: str = None, username: str = None) -> List[schemas.MediaServerLibrary]: def librarys(self, server: str, username: str = None) -> List[schemas.MediaServerLibrary]:
""" """
获取媒体服务器所有媒体库 获取媒体服务器所有媒体库
""" """
@@ -44,13 +45,13 @@ class MediaServerChain(ChainBase):
""" """
return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id) return self.run_module("mediaserver_tv_episodes", server=server, item_id=item_id)
def playing(self, count: int = 20, server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: def playing(self, server: str, count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器正在播放信息 获取媒体服务器正在播放信息
""" """
return self.run_module("mediaserver_playing", count=count, server=server, username=username) return self.run_module("mediaserver_playing", count=count, server=server, username=username)
def latest(self, count: int = 20, server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: def latest(self, server: str, count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目
""" """
@@ -67,12 +68,9 @@ class MediaServerChain(ChainBase):
同步媒体库所有数据到本地数据库 同步媒体库所有数据到本地数据库
""" """
# 设置的媒体服务器 # 设置的媒体服务器
if not settings.MEDIASERVER: mediaservers = self.mediaserverhelper.get_mediaservers()
if not mediaservers:
return return
# 同步黑名单
sync_blacklist = settings.MEDIASERVER_SYNC_BLACKLIST.split(
",") if settings.MEDIASERVER_SYNC_BLACKLIST else []
mediaservers = settings.MEDIASERVER.split(",")
with lock: with lock:
# 汇总统计 # 汇总统计
total_count = 0 total_count = 0
@@ -82,14 +80,16 @@ class MediaServerChain(ChainBase):
for mediaserver in mediaservers: for mediaserver in mediaservers:
if not mediaserver: if not mediaserver:
continue continue
logger.info(f"开始同步媒体库 {mediaserver} 的数据 ...") server_name = mediaserver.name
for library in self.librarys(mediaserver): sync_blacklist = mediaserver.config.get("sync_blacklist") or []
logger.info(f"开始同步媒体库 {server_name} 的数据 ...")
for library in self.librarys(server_name):
# 同步黑名单 跳过 # 同步黑名单 跳过
if library.name in sync_blacklist: if library.name in sync_blacklist:
continue continue
logger.info(f"正在同步 {mediaserver} 媒体库 {library.name} ...") logger.info(f"正在同步 {server_name} 媒体库 {library.name} ...")
library_count = 0 library_count = 0
for item in self.items(mediaserver, library.id): for item in self.items(server_name, library.id):
if not item: if not item:
continue continue
if not item.item_id: if not item.item_id:
@@ -102,7 +102,7 @@ class MediaServerChain(ChainBase):
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影" item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
if item_type == "电视剧": if item_type == "电视剧":
# 查询剧集信息 # 查询剧集信息
espisodes_info = self.episodes(mediaserver, item.item_id) or [] espisodes_info = self.episodes(server_name, item.item_id) or []
for episode in espisodes_info: for episode in espisodes_info:
seasoninfo[episode.season] = episode.episodes seasoninfo[episode.season] = episode.episodes
# 插入数据 # 插入数据
@@ -110,7 +110,7 @@ class MediaServerChain(ChainBase):
item_dict['seasoninfo'] = json.dumps(seasoninfo) item_dict['seasoninfo'] = json.dumps(seasoninfo)
item_dict['item_type'] = item_type item_dict['item_type'] = item_type
self.dboper.add(**item_dict) self.dboper.add(**item_dict)
logger.info(f"{mediaserver} 媒体库 {library.name} 同步完成,共同步数量:{library_count}") logger.info(f"{server_name} 媒体库 {library.name} 同步完成,共同步数量:{library_count}")
# 总数累加 # 总数累加
total_count += library_count total_count += library_count
logger.info("【MediaServer】媒体库数据同步完成,同步数量:%s" % total_count) logger.info(f"媒体库 {server_name} 数据同步完成,同步数量:%s" % total_count)
+30 -34
View File
@@ -39,18 +39,16 @@ class Settings(BaseSettings):
DEBUG: bool = False DEBUG: bool = False
# 是否开发模式 # 是否开发模式
DEV: bool = False DEV: bool = False
# 是否开启插件热加载
PLUGIN_AUTO_RELOAD: bool = False
# 配置文件目录 # 配置文件目录
CONFIG_DIR: Optional[str] = None CONFIG_DIR: Optional[str] = None
# 超级管理员 # 超级管理员
SUPERUSER: str = "admin" SUPERUSER: str = "admin"
# API密钥,需要更换 # API密钥,需要更换
API_TOKEN: str = "moviepilot" API_TOKEN: str = "moviepilot"
# 登录页面电影海报,tmdb/bing
WALLPAPER: str = "tmdb"
# 网络代理 IP:PORT # 网络代理 IP:PORT
PROXY_HOST: Optional[str] = None PROXY_HOST: Optional[str] = None
# 登录页面电影海报,tmdb/bing
WALLPAPER: str = "tmdb"
# 媒体搜索来源 themoviedb/douban/bangumi,多个用,分隔 # 媒体搜索来源 themoviedb/douban/bangumi,多个用,分隔
SEARCH_SOURCE: str = "themoviedb,douban,bangumi" SEARCH_SOURCE: str = "themoviedb,douban,bangumi"
# 媒体识别来源 themoviedb/douban # 媒体识别来源 themoviedb/douban
@@ -71,6 +69,16 @@ class Settings(BaseSettings):
FANART_ENABLE: bool = True FANART_ENABLE: bool = True
# Fanart API Key # Fanart API Key
FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f" FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f"
# 元数据识别缓存过期时间(小时)
META_CACHE_EXPIRE: int = 0
# 电视剧动漫的分类genre_ids
ANIME_GENREIDS = [16]
# 用户认证站点
AUTH_SITE: str = ""
# 自动检查和更新站点资源包(站点索引、认证等)
AUTO_UPDATE_RESOURCE: bool = True
# 是否启用DOH解析域名
DOH_ENABLE: bool = True
# 支持的后缀格式 # 支持的后缀格式
RMT_MEDIAEXT: list = ['.mp4', '.mkv', '.ts', '.iso', RMT_MEDIAEXT: list = ['.mp4', '.mkv', '.ts', '.iso',
'.rmvb', '.avi', '.mov', '.mpeg', '.rmvb', '.avi', '.mov', '.mpeg',
@@ -79,26 +87,26 @@ class Settings(BaseSettings):
'.tp', '.f4v'] '.tp', '.f4v']
# 支持的字幕文件后缀格式 # 支持的字幕文件后缀格式
RMT_SUBEXT: list = ['.srt', '.ass', '.ssa', '.sup'] RMT_SUBEXT: list = ['.srt', '.ass', '.ssa', '.sup']
# 下载器临时文件后缀
DOWNLOAD_TMPEXT: list = ['.!qB', '.part']
# 支持的音轨文件后缀格式 # 支持的音轨文件后缀格式
RMT_AUDIO_TRACK_EXT: list = ['.mka'] RMT_AUDIO_TRACK_EXT: list = ['.mka']
# 索引器 # 下载器临时文件后缀
INDEXER: str = "builtin" DOWNLOAD_TMPEXT: list = ['.!qB', '.part']
# 订阅模式 # 订阅模式
SUBSCRIBE_MODE: str = "spider" SUBSCRIBE_MODE: str = "spider"
# RSS订阅模式刷新时间间隔(分钟) # RSS订阅模式刷新时间间隔(分钟)
SUBSCRIBE_RSS_INTERVAL: int = 30 SUBSCRIBE_RSS_INTERVAL: int = 30
# 订阅数据共享
SUBSCRIBE_STATISTIC_SHARE: bool = True
# 订阅搜索开关 # 订阅搜索开关
SUBSCRIBE_SEARCH: bool = False SUBSCRIBE_SEARCH: bool = False
# 用户认证站点 # 搜索多个名称
AUTH_SITE: str = "" SEARCH_MULTIPLE_NAME: bool = False
# 交互搜索自动下载用户ID,使用,分割
AUTO_DOWNLOAD_USER: Optional[str] = None
# 种子标签 # 种子标签
TORRENT_TAG: str = "MOVIEPILOT" TORRENT_TAG: str = "MOVIEPILOT"
# 下载站点字幕 # 下载站点字幕
DOWNLOAD_SUBTITLE: bool = True DOWNLOAD_SUBTITLE: bool = True
# 交互搜索自动下载用户ID,使用,分割
AUTO_DOWNLOAD_USER: Optional[str] = None
# CookieCloud是否启动本地服务 # CookieCloud是否启动本地服务
COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False COOKIECLOUD_ENABLE_LOCAL: Optional[bool] = False
# CookieCloud服务器地址 # CookieCloud服务器地址
@@ -111,12 +119,8 @@ class Settings(BaseSettings):
COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24 COOKIECLOUD_INTERVAL: Optional[int] = 60 * 24
# CookieCloud同步黑名单,多个域名,分割 # CookieCloud同步黑名单,多个域名,分割
COOKIECLOUD_BLACKLIST: Optional[str] = None COOKIECLOUD_BLACKLIST: Optional[str] = None
# OCR服务器地址
OCR_HOST: str = "https://movie-pilot.org"
# CookieCloud对应的浏览器UA # CookieCloud对应的浏览器UA
USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57" USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/113.0.0.0 Safari/537.36 Edg/113.0.1774.57"
# 电视剧动漫的分类genre_ids
ANIME_GENREIDS = [16]
# 电影重命名格式 # 电影重命名格式
MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \ MOVIE_RENAME_FORMAT: str = "{{title}}{% if year %} ({{year}}){% endif %}" \
"/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}" \ "/{{title}}{% if year %} ({{year}}){% endif %}{% if part %}-{{part}}{% endif %}{% if videoFormat %} - {{videoFormat}}{% endif %}" \
@@ -126,30 +130,22 @@ class Settings(BaseSettings):
"/Season {{season}}" \ "/Season {{season}}" \
"/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}" \ "/{{title}} - {{season_episode}}{% if part %}-{{part}}{% endif %}{% if episode %} - 第 {{episode}} 集{% endif %}" \
"{{fileExt}}" "{{fileExt}}"
# 转移时覆盖模式 # OCR服务器地址
OVERWRITE_MODE: str = "size" OCR_HOST: str = "https://movie-pilot.org"
# 大内存模式 # 服务器地址,对应 https://github.com/jxxghp/MoviePilot-Server 项目
BIG_MEMORY_MODE: bool = False MP_SERVER_HOST: str = "https://movie-pilot.org"
# 插件市场仓库地址,多个地址使用,分隔,地址以/结尾 # 插件市场仓库地址,多个地址使用,分隔,地址以/结尾
PLUGIN_MARKET: str = "https://github.com/jxxghp/MoviePilot-Plugins,https://github.com/thsrite/MoviePilot-Plugins,https://github.com/honue/MoviePilot-Plugins,https://github.com/InfinityPacer/MoviePilot-Plugins" PLUGIN_MARKET: str = "https://github.com/jxxghp/MoviePilot-Plugins,https://github.com/thsrite/MoviePilot-Plugins,https://github.com/honue/MoviePilot-Plugins,https://github.com/InfinityPacer/MoviePilot-Plugins"
# 插件安装数据共享
PLUGIN_STATISTIC_SHARE: bool = True
# 是否开启插件热加载
PLUGIN_AUTO_RELOAD: bool = False
# Github token,提高请求api限流阈值 ghp_**** # Github token,提高请求api限流阈值 ghp_****
GITHUB_TOKEN: Optional[str] = None GITHUB_TOKEN: Optional[str] = None
# Github代理服务器,格式:https://mirror.ghproxy.com/ # Github代理服务器,格式:https://mirror.ghproxy.com/
GITHUB_PROXY: Optional[str] = '' GITHUB_PROXY: Optional[str] = ''
# 自动检查和更新站点资源包(站点索引、认证等) # 大内存模式
AUTO_UPDATE_RESOURCE: bool = True BIG_MEMORY_MODE: bool = False
# 元数据识别缓存过期时间(小时)
META_CACHE_EXPIRE: int = 0
# 是否启用DOH解析域名
DOH_ENABLE: bool = True
# 搜索多个名称
SEARCH_MULTIPLE_NAME: bool = False
# 订阅数据共享
SUBSCRIBE_STATISTIC_SHARE: bool = True
# 插件安装数据共享
PLUGIN_STATISTIC_SHARE: bool = True
# 服务器地址,对应 https://github.com/jxxghp/MoviePilot-Server 项目
MP_SERVER_HOST: str = "https://movie-pilot.org"
@validator("SUBSCRIBE_RSS_INTERVAL", @validator("SUBSCRIBE_RSS_INTERVAL",
"COOKIECLOUD_INTERVAL", "COOKIECLOUD_INTERVAL",
+8 -5
View File
@@ -1,4 +1,7 @@
from typing import List
from app.db.systemconfig_oper import SystemConfigOper from app.db.systemconfig_oper import SystemConfigOper
from app.schemas import MediaServerConf
from app.schemas.types import SystemConfigKey from app.schemas.types import SystemConfigKey
@@ -10,11 +13,11 @@ class MediaServerHelper:
def __init__(self): def __init__(self):
self.systemconfig = SystemConfigOper() self.systemconfig = SystemConfigOper()
def get_mediaservers(self) -> dict: def get_mediaservers(self) -> List[MediaServerConf]:
""" """
获取媒体服务器 获取媒体服务器
""" """
mediaserver_conf: dict = self.systemconfig.get(SystemConfigKey.MediaServers) mediaserver_confs: List[dict] = self.systemconfig.get(SystemConfigKey.MediaServers)
if not mediaserver_conf: if not mediaserver_confs:
return {} return []
return mediaserver_conf return [MediaServerConf(**conf) for conf in mediaserver_confs]
+117 -72
View File
@@ -1,7 +1,8 @@
from typing import Optional, Tuple, Union, Any, List, Generator from typing import Optional, Tuple, Union, Any, List, Generator, Dict
from app import schemas from app import schemas
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.helper.mediaserver import MediaServerHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.emby.emby import Emby from app.modules.emby.emby import Emby
@@ -9,10 +10,27 @@ from app.schemas.types import MediaType
class EmbyModule(_ModuleBase): class EmbyModule(_ModuleBase):
emby: Emby = None _servers: Dict[str, Emby] = {}
def init_module(self) -> None: def init_module(self) -> None:
self.emby = Emby() """
初始化模块
"""
# 读取媒体服务器配置
self._servers = {}
mediaservers = MediaServerHelper().get_mediaservers()
if not mediaservers:
return
# 读取Emby配置
for server in mediaservers:
if server.type == "emby":
self._servers[server.name] = Emby(**server.config)
def get_server(self, name: str) -> Optional[Emby]:
"""
获取Emby服务器
"""
return self._servers.get(name)
@staticmethod @staticmethod
def get_name() -> str: def get_name() -> str:
@@ -25,22 +43,27 @@ class EmbyModule(_ModuleBase):
""" """
测试模块连接性 测试模块连接性
""" """
if self.emby.is_inactive(): if not self._servers:
self.emby.reconnect() return False, "未配置Emby服务器"
if not self.emby.get_user(): for name, server in self._servers.items():
return False, "无法连接Emby,请检查参数配置" if server.is_inactive():
server.reconnect()
if not server.get_user():
return False, f"无法连接Emby服务器:{name}"
return True, "" return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]: def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "emby" pass
def scheduler_job(self) -> None: def scheduler_job(self) -> None:
""" """
定时任务,每10分钟调用一次 定时任务,每10分钟调用一次
""" """
# 定时重连 # 定时重连
if self.emby.is_inactive(): for name, server in self._servers.items():
self.emby.reconnect() if server.is_inactive():
logger.info(f"Emby服务器 {name} 连接断开,尝试重连 ...")
server.reconnect()
def user_authenticate(self, name: str, password: str) -> Optional[str]: def user_authenticate(self, name: str, password: str) -> Optional[str]:
""" """
@@ -50,7 +73,11 @@ class EmbyModule(_ModuleBase):
:return: token or None :return: token or None
""" """
# Emby认证 # Emby认证
return self.emby.authenticate(name, password) for server in self._servers.values():
result = server.authenticate(name, password)
if result:
return result
return None
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
""" """
@@ -60,7 +87,11 @@ class EmbyModule(_ModuleBase):
:param args: 请求参数 :param args: 请求参数
:return: 字典,解析为消息时需要包含:title、text、image :return: 字典,解析为消息时需要包含:title、text、image
""" """
return self.emby.get_webhook_message(form, args) for server in self._servers.values():
result = server.get_webhook_message(form, args)
if result:
return result
return None
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]:
""" """
@@ -69,86 +100,97 @@ class EmbyModule(_ModuleBase):
:param itemid: 媒体服务器ItemID :param itemid: 媒体服务器ItemID
:return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}}
""" """
if mediainfo.type == MediaType.MOVIE: for name, server in self._servers.values():
if itemid: if mediainfo.type == MediaType.MOVIE:
movie = self.emby.get_iteminfo(itemid) if itemid:
if movie: movie = server.get_iteminfo(itemid)
logger.info(f"媒体库中已存在:{movie}") if movie:
logger.info(f"媒体库 {name} 中找到了 {movie}")
return schemas.ExistMediaInfo(
type=MediaType.MOVIE,
server=name,
itemid=movie.item_id
)
movies = server.get_movies(title=mediainfo.title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id)
if not movies:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
else:
logger.info(f"媒体库 {name} 中找到了 {movies}")
return schemas.ExistMediaInfo( return schemas.ExistMediaInfo(
type=MediaType.MOVIE, type=MediaType.MOVIE,
server="emby", server=name,
itemid=movie.item_id itemid=movies[0].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: else:
logger.info(f"媒体库中已存在:{movies}") itemid, tvs = server.get_tv_episodes(title=mediainfo.title,
return schemas.ExistMediaInfo( year=mediainfo.year,
type=MediaType.MOVIE, tmdb_id=mediainfo.tmdb_id,
server="emby", item_id=itemid)
itemid=movies[0].item_id if not tvs:
) logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
else: continue
itemid, tvs = self.emby.get_tv_episodes(title=mediainfo.title, else:
year=mediainfo.year, logger.info(f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}")
tmdb_id=mediainfo.tmdb_id, return schemas.ExistMediaInfo(
item_id=itemid) type=MediaType.TV,
if not tvs: seasons=tvs,
logger.info(f"{mediainfo.title_year} 在媒体库中不存在") server=name,
return None itemid=itemid
else: )
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") return None
return schemas.ExistMediaInfo(
type=MediaType.TV,
seasons=tvs,
server="emby",
itemid=itemid
)
def media_statistic(self) -> List[schemas.Statistic]: def media_statistic(self) -> List[schemas.Statistic]:
""" """
媒体数量统计 媒体数量统计
""" """
media_statistic = self.emby.get_medias_count() media_statistics = []
media_statistic.user_count = self.emby.get_user_count() for server in self._servers.values():
return [media_statistic] media_statistic = server.get_medias_count()
media_statistic.user_count = server.get_user_count()
if media_statistic:
media_statistics.append(media_statistic)
return media_statistics
def mediaserver_librarys(self, server: str = None, username: str = None) -> Optional[List[schemas.MediaServerLibrary]]: def mediaserver_librarys(self, server: str,
username: str = None) -> Optional[List[schemas.MediaServerLibrary]]:
""" """
媒体库列表 媒体库列表
""" """
if server and server != "emby": server_obj = self.get_server(server)
return None if server_obj:
return self.emby.get_librarys(username) return server_obj.get_librarys(username)
return None
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
""" """
媒体库项目列表 媒体库项目列表
""" """
if server != "emby": server_obj = self.get_server(server)
return None if server_obj:
return self.emby.get_items(library_id) return server_obj.get_items(library_id)
return None
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
""" """
媒体库项目详情 媒体库项目详情
""" """
if server != "emby": server_obj = self.get_server(server)
return None if server_obj:
return self.emby.get_iteminfo(item_id) return server_obj.get_iteminfo(item_id)
return None
def mediaserver_tv_episodes(self, server: str, def mediaserver_tv_episodes(self, server: str,
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
""" """
获取剧集信息 获取剧集信息
""" """
if server != "emby": server_obj = self.get_server(server)
if not server_obj:
return None return None
_, seasoninfo = self.emby.get_tv_episodes(item_id=item_id) _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id)
if not seasoninfo: if not seasoninfo:
return [] return []
return [schemas.MediaServerSeasonInfo( return [schemas.MediaServerSeasonInfo(
@@ -156,28 +198,31 @@ class EmbyModule(_ModuleBase):
episodes=episodes episodes=episodes
) for season, episodes in seasoninfo.items()] ) for season, episodes in seasoninfo.items()]
def mediaserver_playing(self, count: int = 20, def mediaserver_playing(self, server: str,
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器正在播放信息 获取媒体服务器正在播放信息
""" """
if server and server != "emby": server_obj = self.get_server(server)
if not server_obj:
return [] return []
return self.emby.get_resume(num=count, username=username) return server_obj.get_resume(num=count, username=username)
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
""" """
获取媒体库播放地址 获取媒体库播放地址
""" """
if server != "emby": server_obj = self.get_server(server)
if not server_obj:
return None return None
return self.emby.get_play_url(item_id) return server_obj.get_play_url(item_id)
def mediaserver_latest(self, count: int = 20, def mediaserver_latest(self, server: str,
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目
""" """
if server and server != "emby": server_obj = self.get_server(server)
if not server_obj:
return [] return []
return self.emby.get_latest(num=count, username=username) return server_obj.get_latest(num=count, username=username)
+115 -70
View File
@@ -1,7 +1,8 @@
from typing import Optional, Tuple, Union, Any, List, Generator from typing import Optional, Tuple, Union, Any, List, Generator, Dict
from app import schemas from app import schemas
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.helper.mediaserver import MediaServerHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.jellyfin.jellyfin import Jellyfin from app.modules.jellyfin.jellyfin import Jellyfin
@@ -9,25 +10,44 @@ from app.schemas.types import MediaType
class JellyfinModule(_ModuleBase): class JellyfinModule(_ModuleBase):
jellyfin: Jellyfin = None _servers: Dict[str, Jellyfin] = {}
def init_module(self) -> None: def init_module(self) -> None:
self.jellyfin = Jellyfin() """
初始化模块
"""
# 读取媒体服务器配置
self._servers = {}
mediaservers = MediaServerHelper().get_mediaservers()
if not mediaservers:
return
# 读取Jelly配置
for server in mediaservers:
if server.type == "jellyfin":
self._servers[server.name] = Jellyfin(**server.config)
def get_server(self, name: str) -> Optional[Jellyfin]:
"""
获取Jellyfin服务器
"""
return self._servers.get(name)
@staticmethod @staticmethod
def get_name() -> str: def get_name() -> str:
return "Jellyfin" return "Jellyfin"
def init_setting(self) -> Tuple[str, Union[str, bool]]: def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "jellyfin" pass
def scheduler_job(self) -> None: def scheduler_job(self) -> None:
""" """
定时任务,每10分钟调用一次 定时任务,每10分钟调用一次
""" """
# 定时重连 # 定时重连
if self.jellyfin.is_inactive(): for name, server in self._servers.items():
self.jellyfin.reconnect() if server.is_inactive():
logger.info(f"Jellyfin {name} 服务器连接断开,尝试重连 ...")
server.reconnect()
def stop(self): def stop(self):
pass pass
@@ -36,10 +56,13 @@ class JellyfinModule(_ModuleBase):
""" """
测试模块连接性 测试模块连接性
""" """
if self.jellyfin.is_inactive(): if not self._servers:
self.jellyfin.reconnect() return False, "未配置Jellyfin服务器"
if not self.jellyfin.get_user(): for name, server in self._servers.items():
return False, "无法连接Jellyfin,请检查参数配置" if server.is_inactive():
server.reconnect()
if not server.get_user():
return False, f"无法连接Jellyfin服务器:{name}"
return True, "" return True, ""
def user_authenticate(self, name: str, password: str) -> Optional[str]: def user_authenticate(self, name: str, password: str) -> Optional[str]:
@@ -50,7 +73,11 @@ class JellyfinModule(_ModuleBase):
:return: Token or None :return: Token or None
""" """
# Jellyfin认证 # Jellyfin认证
return self.jellyfin.authenticate(name, password) for server in self._servers.values():
result = server.authenticate(name, password)
if result:
return result
return None
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
""" """
@@ -60,7 +87,11 @@ class JellyfinModule(_ModuleBase):
:param args: 请求参数 :param args: 请求参数
:return: 字典,解析为消息时需要包含:title、text、image :return: 字典,解析为消息时需要包含:title、text、image
""" """
return self.jellyfin.get_webhook_message(body) for server in self._servers.values():
result = server.get_webhook_message(body)
if result:
return result
return None
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]:
""" """
@@ -69,84 +100,95 @@ class JellyfinModule(_ModuleBase):
:param itemid: 媒体服务器ItemID :param itemid: 媒体服务器ItemID
:return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}}
""" """
if mediainfo.type == MediaType.MOVIE: for name, server in self._servers.items():
if itemid: if mediainfo.type == MediaType.MOVIE:
movie = self.jellyfin.get_iteminfo(itemid) if itemid:
if movie: movie = server.get_iteminfo(itemid)
logger.info(f"媒体库中已存在:{movie}") if movie:
logger.info(f"媒体库 {name} 中找到了 {movie}")
return schemas.ExistMediaInfo(
type=MediaType.MOVIE,
server=name,
itemid=movie.item_id
)
movies = server.get_movies(title=mediainfo.title, year=mediainfo.year, tmdb_id=mediainfo.tmdb_id)
if not movies:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
else:
logger.info(f"媒体库 {name} 中找到了 {movies}")
return schemas.ExistMediaInfo( return schemas.ExistMediaInfo(
type=MediaType.MOVIE, type=MediaType.MOVIE,
server="jellyfin", server=name,
itemid=movie.item_id itemid=movies[0].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: else:
logger.info(f"媒体库中已存在:{movies}") itemid, tvs = server.get_tv_episodes(title=mediainfo.title,
return schemas.ExistMediaInfo( year=mediainfo.year,
type=MediaType.MOVIE, tmdb_id=mediainfo.tmdb_id,
server="jellyfin", item_id=itemid)
itemid=movies[0].item_id if not tvs:
) logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
else: continue
itemid, tvs = self.jellyfin.get_tv_episodes(title=mediainfo.title, else:
year=mediainfo.year, logger.info(f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}")
tmdb_id=mediainfo.tmdb_id, return schemas.ExistMediaInfo(
item_id=itemid) type=MediaType.TV,
if not tvs: seasons=tvs,
logger.info(f"{mediainfo.title_year} 在媒体库中不存在") server=name,
return None itemid=itemid
else: )
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") return None
return schemas.ExistMediaInfo(
type=MediaType.TV,
seasons=tvs,
server="jellyfin",
itemid=itemid
)
def media_statistic(self) -> List[schemas.Statistic]: def media_statistic(self) -> List[schemas.Statistic]:
""" """
媒体数量统计 媒体数量统计
""" """
media_statistic = self.jellyfin.get_medias_count() media_statistics = []
media_statistic.user_count = self.jellyfin.get_user_count() for server in self._servers.values():
return [media_statistic] media_statistic = server.get_medias_count()
media_statistic.user_count = server.get_user_count()
if media_statistic:
media_statistics.append(media_statistic)
return media_statistics
def mediaserver_librarys(self, server: str = None, username: str = None) -> Optional[List[schemas.MediaServerLibrary]]: def mediaserver_librarys(self, server: str = None,
username: str = None) -> Optional[List[schemas.MediaServerLibrary]]:
""" """
媒体库列表 媒体库列表
""" """
if server and server != "jellyfin": server_obj = self.get_server(server)
return None if server_obj:
return self.jellyfin.get_librarys(username) return server_obj.get_librarys(username)
return None
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
""" """
媒体库项目列表 媒体库项目列表
""" """
if server != "jellyfin": server_obj = self.get_server(server)
return None if server_obj:
return self.jellyfin.get_items(library_id) return server_obj.get_items(library_id)
return None
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
""" """
媒体库项目详情 媒体库项目详情
""" """
if server != "jellyfin": server_obj = self.get_server(server)
return None if server_obj:
return self.jellyfin.get_iteminfo(item_id) return server_obj.get_iteminfo(item_id)
return None
def mediaserver_tv_episodes(self, server: str, def mediaserver_tv_episodes(self, server: str,
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
""" """
获取剧集信息 获取剧集信息
""" """
if server != "jellyfin": server_obj = self.get_server(server)
if not server_obj:
return None return None
_, seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id) _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id)
if not seasoninfo: if not seasoninfo:
return [] return []
return [schemas.MediaServerSeasonInfo( return [schemas.MediaServerSeasonInfo(
@@ -154,28 +196,31 @@ class JellyfinModule(_ModuleBase):
episodes=episodes episodes=episodes
) for season, episodes in seasoninfo.items()] ) for season, episodes in seasoninfo.items()]
def mediaserver_playing(self, count: int = 20, def mediaserver_playing(self, server: str,
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器正在播放信息 获取媒体服务器正在播放信息
""" """
if server and server != "jellyfin": server_obj = self.get_server(server)
if not server_obj:
return [] return []
return self.jellyfin.get_resume(num=count, username=username) return server_obj.get_resume(num=count, username=username)
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
""" """
获取媒体库播放地址 获取媒体库播放地址
""" """
if server != "jellyfin": server_obj = self.get_server(server)
if not server_obj:
return None return None
return self.jellyfin.get_play_url(item_id) return server_obj.get_play_url(item_id)
def mediaserver_latest(self, count: int = 20, def mediaserver_latest(self, server: str,
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]: count: int = 20, username: str = None) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目
""" """
if server and server != "jellyfin": server_obj = self.get_server(server)
if not server_obj:
return [] return []
return self.jellyfin.get_latest(num=count, username=username) return server_obj.get_latest(num=count, username=username)
+110 -70
View File
@@ -1,7 +1,8 @@
from typing import Optional, Tuple, Union, Any, List, Generator from typing import Optional, Tuple, Union, Any, List, Generator, Dict
from app import schemas from app import schemas
from app.core.context import MediaInfo from app.core.context import MediaInfo
from app.helper.mediaserver import MediaServerHelper
from app.log import logger from app.log import logger
from app.modules import _ModuleBase from app.modules import _ModuleBase
from app.modules.plex.plex import Plex from app.modules.plex.plex import Plex
@@ -9,15 +10,32 @@ from app.schemas.types import MediaType
class PlexModule(_ModuleBase): class PlexModule(_ModuleBase):
plex: Plex = None _servers: Dict[str, Plex] = {}
def init_module(self) -> None: def init_module(self) -> None:
self.plex = Plex() """
初始化模块
"""
# 读取媒体服务器配置
self._servers = {}
mediaservers = MediaServerHelper().get_mediaservers()
if not mediaservers:
return
# 读取Emby配置
for server in mediaservers:
if server.type == "plex":
self._servers[server.name] = Plex(**server.config)
@staticmethod @staticmethod
def get_name() -> str: def get_name() -> str:
return "Plex" return "Plex"
def get_server(self, name: str) -> Optional[Plex]:
"""
获取Plex服务器
"""
return self._servers.get(name)
def stop(self): def stop(self):
pass pass
@@ -25,22 +43,27 @@ class PlexModule(_ModuleBase):
""" """
测试模块连接性 测试模块连接性
""" """
if self.plex.is_inactive(): if not self._servers:
self.plex.reconnect() return False, "未配置Plex服务器"
if not self.plex.get_librarys(): for name, server in self._servers.items():
return False, "无法连接Plex,请检查参数配置" if server.is_inactive():
server.reconnect()
if not server.get_librarys():
return False, f"无法连接Plex服务器:{name}"
return True, "" return True, ""
def init_setting(self) -> Tuple[str, Union[str, bool]]: def init_setting(self) -> Tuple[str, Union[str, bool]]:
return "MEDIASERVER", "plex" pass
def scheduler_job(self) -> None: def scheduler_job(self) -> None:
""" """
定时任务,每10分钟调用一次 定时任务,每10分钟调用一次
""" """
# 定时重连 # 定时重连
if self.plex.is_inactive(): for name, server in self._servers.items():
self.plex.reconnect() if server.is_inactive():
logger.info(f"Plex {name} 服务器连接断开,尝试重连 ...")
server.reconnect()
def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]: def webhook_parser(self, body: Any, form: Any, args: Any) -> Optional[schemas.WebhookEventInfo]:
""" """
@@ -50,7 +73,11 @@ class PlexModule(_ModuleBase):
:param args: 请求参数 :param args: 请求参数
:return: 字典,解析为消息时需要包含:title、text、image :return: 字典,解析为消息时需要包含:title、text、image
""" """
return self.plex.get_webhook_message(form) for server in self._servers.values():
result = server.get_webhook_message(body)
if result:
return result
return None
def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]: def media_exists(self, mediainfo: MediaInfo, itemid: str = None) -> Optional[schemas.ExistMediaInfo]:
""" """
@@ -59,88 +86,98 @@ class PlexModule(_ModuleBase):
:param itemid: 媒体服务器ItemID :param itemid: 媒体服务器ItemID
:return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}} :return: 如不存在返回None,存在时返回信息,包括每季已存在所有集{type: movie/tv, seasons: {season: [episodes]}}
""" """
if mediainfo.type == MediaType.MOVIE: for name, server in self._servers.items():
if itemid: if mediainfo.type == MediaType.MOVIE:
movie = self.plex.get_iteminfo(itemid) if itemid:
if movie: movie = server.get_iteminfo(itemid)
logger.info(f"媒体库中已存在:{movie}") if movie:
logger.info(f"媒体库 {name} 中找到了 {movie}")
return schemas.ExistMediaInfo(
type=MediaType.MOVIE,
server=name,
itemid=movie.item_id
)
movies = server.get_movies(title=mediainfo.title,
original_title=mediainfo.original_title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id)
if not movies:
logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
continue
else:
logger.info(f"媒体库 {name} 中找到了 {movies}")
return schemas.ExistMediaInfo( return schemas.ExistMediaInfo(
type=MediaType.MOVIE, type=MediaType.MOVIE,
server="plex", server=name,
itemid=movie.item_id itemid=movies[0].item_id
) )
movies = self.plex.get_movies(title=mediainfo.title,
original_title=mediainfo.original_title,
year=mediainfo.year,
tmdb_id=mediainfo.tmdb_id)
if not movies:
logger.info(f"{mediainfo.title_year} 在媒体库中不存在")
return None
else: else:
logger.info(f"媒体库中已存在:{movies}") item_id, tvs = server.get_tv_episodes(title=mediainfo.title,
return schemas.ExistMediaInfo( original_title=mediainfo.original_title,
type=MediaType.MOVIE, year=mediainfo.year,
server="plex", tmdb_id=mediainfo.tmdb_id,
itemid=movies[0].item_id item_id=itemid)
) if not tvs:
else: logger.info(f"{mediainfo.title_year} 没有在媒体库 {name}")
item_id, tvs = self.plex.get_tv_episodes(title=mediainfo.title, continue
original_title=mediainfo.original_title, else:
year=mediainfo.year, logger.info(f"{mediainfo.title_year} 在媒体库 {name} 中找到了这些季集:{tvs}")
tmdb_id=mediainfo.tmdb_id, return schemas.ExistMediaInfo(
item_id=itemid) type=MediaType.TV,
if not tvs: seasons=tvs,
logger.info(f"{mediainfo.title_year} 在媒体库中不存在") server=name,
return None itemid=item_id
else: )
logger.info(f"{mediainfo.title_year} 媒体库中已存在:{tvs}") return None
return schemas.ExistMediaInfo(
type=MediaType.TV,
seasons=tvs,
server="plex",
itemid=item_id
)
def media_statistic(self) -> List[schemas.Statistic]: def media_statistic(self) -> List[schemas.Statistic]:
""" """
媒体数量统计 媒体数量统计
""" """
media_statistic = self.plex.get_medias_count() media_statistics = []
media_statistic.user_count = 1 for server in self._servers.values():
return [media_statistic] media_statistic = server.get_medias_count()
media_statistic.user_count = 1
if media_statistic:
media_statistics.append(media_statistic)
return media_statistics
def mediaserver_librarys(self, server: str = None, **kwargs) -> Optional[List[schemas.MediaServerLibrary]]: def mediaserver_librarys(self, server: str = None, **kwargs) -> Optional[List[schemas.MediaServerLibrary]]:
""" """
媒体库列表 媒体库列表
""" """
if server and server != "plex": server_obj = self.get_server(server)
return None if server_obj:
return self.plex.get_librarys() return server_obj.get_librarys()
return None
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]: def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
""" """
媒体库项目列表 媒体库项目列表
""" """
if server != "plex": server_obj = self.get_server(server)
return None if server_obj:
return self.plex.get_items(library_id) return server_obj.get_items(library_id)
return None
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]: def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
""" """
媒体库项目详情 媒体库项目详情
""" """
if server != "plex": server_obj = self.get_server(server)
return None if server_obj:
return self.plex.get_iteminfo(item_id) return server_obj.get_iteminfo(item_id)
return None
def mediaserver_tv_episodes(self, server: str, def mediaserver_tv_episodes(self, server: str,
item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]: item_id: Union[str, int]) -> Optional[List[schemas.MediaServerSeasonInfo]]:
""" """
获取剧集信息 获取剧集信息
""" """
if server != "plex": server_obj = self.get_server(server)
if not server_obj:
return None return None
_, seasoninfo = self.plex.get_tv_episodes(item_id=item_id) _, seasoninfo = server_obj.get_tv_episodes(item_id=item_id)
if not seasoninfo: if not seasoninfo:
return [] return []
return [schemas.MediaServerSeasonInfo( return [schemas.MediaServerSeasonInfo(
@@ -148,26 +185,29 @@ class PlexModule(_ModuleBase):
episodes=episodes episodes=episodes
) for season, episodes in seasoninfo.items()] ) for season, episodes in seasoninfo.items()]
def mediaserver_playing(self, count: int = 20, server: str = None, **kwargs) -> List[schemas.MediaServerPlayItem]: def mediaserver_playing(self, server: str, count: int = 20, **kwargs) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器正在播放信息 获取媒体服务器正在播放信息
""" """
if server and server != "plex": server_obj = self.get_server(server)
if not server_obj:
return [] return []
return self.plex.get_resume(count) return server_obj.get_resume(num=count)
def mediaserver_latest(self, count: int = 20, server: str = None, **kwargs) -> List[schemas.MediaServerPlayItem]: def mediaserver_latest(self, server: str, count: int = 20, **kwargs) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目
""" """
if server and server != "plex": server_obj = self.get_server(server)
if not server_obj:
return [] return []
return self.plex.get_latest(count) return server_obj.get_latest(num=count)
def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]:
""" """
获取媒体库播放地址 获取媒体库播放地址
""" """
if server != "plex": server_obj = self.get_server(server)
if not server_obj:
return None return None
return self.plex.get_play_url(item_id) return server_obj.get_play_url(item_id)
+12
View File
@@ -156,3 +156,15 @@ class MediaServerPlayItem(BaseModel):
image: Optional[str] = None image: Optional[str] = None
link: Optional[str] = None link: Optional[str] = None
percent: Optional[float] = None percent: Optional[float] = None
class MediaServerConf(BaseModel):
"""
媒体服务器配置
"""
# 名称
name: Optional[str] = None
# 类型 emby/jellyfin/plex
type: Optional[str] = None
# 配置
config: Optional[dict] = {}