From 0095e0f4dddbac08793f8157f658a725c3cc0811 Mon Sep 17 00:00:00 2001 From: jxxghp Date: Wed, 3 Jan 2024 12:02:08 +0800 Subject: [PATCH] =?UTF-8?q?feat=EF=BC=9A=E6=92=AD=E6=94=BE=E8=B7=B3?= =?UTF-8?q?=E8=BD=ACapi?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 3 + app/api/apiv1.py | 3 +- app/api/endpoints/download.py | 39 +--------- app/api/endpoints/media.py | 25 ------- app/api/endpoints/mediaserver.py | 118 +++++++++++++++++++++++++++++++ app/chain/mediaserver.py | 8 ++- app/core/config.py | 6 ++ app/modules/emby/__init__.py | 8 +++ app/modules/emby/emby.py | 21 ++++-- app/modules/jellyfin/__init__.py | 8 +++ app/modules/jellyfin/jellyfin.py | 25 +++++-- app/modules/plex/__init__.py | 8 +++ app/modules/plex/plex.py | 58 +++++++++++++-- app/schemas/mediaserver.py | 4 ++ config/app.env | 6 ++ 15 files changed, 260 insertions(+), 80 deletions(-) create mode 100644 app/api/endpoints/mediaserver.py diff --git a/README.md b/README.md index 38c79d65..55b09ada 100644 --- a/README.md +++ b/README.md @@ -195,16 +195,19 @@ MoviePilot需要配套下载器和媒体服务器配合使用。 - `emby`设置项: - **EMBY_HOST:** Emby服务器地址,格式:`ip:port`,https需要添加`https://`前缀 + - **EMBY_PLAY_HOST:** EMBY外网地址,格式:`http(s)://DOMAIN:PORT`,未设置时使用`EMBY_HOST` - **EMBY_API_KEY:** Emby Api Key,在`设置->高级->API密钥`处生成 - `jellyfin`设置项: - **JELLYFIN_HOST:** Jellyfin服务器地址,格式:`ip:port`,https需要添加`https://`前缀 + - **JELLYFIN_PLAY_HOST:** Jellyfin外网地址,格式:`http(s)://DOMAIN:PORT`,未设置时使用`JELLYFIN_HOST` - **JELLYFIN_API_KEY:** Jellyfin Api Key,在`设置->高级->API密钥`处生成 - `plex`设置项: - **PLEX_HOST:** Plex服务器地址,格式:`ip:port`,https需要添加`https://`前缀 + - **PLEX_PLAY_HOST:** Plex外网地址,格式:`http(s)://DOMAIN:PORT`,未设置时使用`PLEX_HOST` - **PLEX_TOKEN:** Plex网页Url中的`X-Plex-Token`,通过浏览器F12->网络从请求URL中获取 - **MEDIASERVER_SYNC_INTERVAL:** 媒体服务器同步间隔(小时),默认`6`,留空则不同步 - **MEDIASERVER_SYNC_BLACKLIST:** 媒体服务器同步黑名单,多个媒体库名称使用,分割 diff --git a/app/api/apiv1.py b/app/api/apiv1.py index f0bcbd74..019975ce 100644 --- a/app/api/apiv1.py +++ b/app/api/apiv1.py @@ -1,7 +1,7 @@ from fastapi import APIRouter from app.api.endpoints import login, user, site, message, webhook, subscribe, \ - media, douban, search, plugin, tmdb, history, system, download, dashboard, filebrowser, transfer + media, douban, search, plugin, tmdb, history, system, download, dashboard, filebrowser, transfer, mediaserver api_router = APIRouter() api_router.include_router(login.router, prefix="/login", tags=["login"]) @@ -21,3 +21,4 @@ api_router.include_router(download.router, prefix="/download", tags=["download"] api_router.include_router(dashboard.router, prefix="/dashboard", tags=["dashboard"]) api_router.include_router(filebrowser.router, prefix="/filebrowser", tags=["filebrowser"]) api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"]) +api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"]) diff --git a/app/api/endpoints/download.py b/app/api/endpoints/download.py index 8ce6ace6..ee130595 100644 --- a/app/api/endpoints/download.py +++ b/app/api/endpoints/download.py @@ -1,16 +1,14 @@ from typing import Any, List -from fastapi import APIRouter, Depends, HTTPException +from fastapi import APIRouter, Depends from app import schemas from app.chain.download import DownloadChain -from app.chain.media import MediaChain from app.core.context import MediaInfo, Context, TorrentInfo from app.core.metainfo import MetaInfo from app.core.security import verify_token from app.db.models.user import User from app.db.userauth import get_current_active_user -from app.schemas import NotExistMediaInfo, MediaType router = APIRouter() @@ -53,41 +51,6 @@ def add_downloading( }) -@router.post("/notexists", summary="查询缺失媒体信息", response_model=List[NotExistMediaInfo]) -def exists(media_in: schemas.MediaInfo, - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 查询缺失媒体信息 - """ - # 媒体信息 - meta = MetaInfo(title=media_in.title) - mtype = MediaType(media_in.type) if media_in.type else None - if mtype: - meta.type = mtype - if media_in.season: - meta.begin_season = media_in.season - meta.type = MediaType.TV - if media_in.year: - meta.year = media_in.year - if media_in.tmdb_id or media_in.douban_id: - mediainfo = MediaChain().recognize_media(meta=meta, mtype=mtype, - tmdbid=media_in.tmdb_id, doubanid=media_in.douban_id) - else: - mediainfo = MediaChain().recognize_by_meta(metainfo=meta) - # 查询缺失信息 - if not mediainfo: - raise HTTPException(status_code=404, detail="媒体信息不存在") - mediakey = mediainfo.tmdb_id or mediainfo.douban_id - exist_flag, no_exists = DownloadChain().get_no_exists_info(meta=meta, mediainfo=mediainfo) - if mediainfo.type == MediaType.MOVIE: - # 电影已存在时返回空列表,存在时返回空对像列表 - return [] if exist_flag else [NotExistMediaInfo()] - elif no_exists and no_exists.get(mediakey): - # 电视剧返回缺失的剧集 - return list(no_exists.get(mediakey).values()) - return [] - - @router.get("/start/{hashString}", summary="开始任务", response_model=schemas.Response) def start_downloading( hashString: str, diff --git a/app/api/endpoints/media.py b/app/api/endpoints/media.py index c990fc34..44ce26a7 100644 --- a/app/api/endpoints/media.py +++ b/app/api/endpoints/media.py @@ -1,7 +1,6 @@ from typing import List, Any from fastapi import APIRouter, Depends -from sqlalchemy.orm import Session from app import schemas from app.chain.media import MediaChain @@ -9,8 +8,6 @@ from app.core.config import settings from app.core.context import Context from app.core.metainfo import MetaInfo from app.core.security import verify_token, verify_uri_token -from app.db import get_db -from app.db.mediaserver_oper import MediaServerOper from app.schemas import MediaType router = APIRouter() @@ -79,28 +76,6 @@ def search_by_title(title: str, return [] -@router.get("/exists", summary="本地是否存在", response_model=schemas.Response) -def exists(title: str = None, - year: int = None, - mtype: str = None, - tmdbid: int = None, - season: int = None, - db: Session = Depends(get_db), - _: schemas.TokenPayload = Depends(verify_token)) -> Any: - """ - 判断本地是否存在 - """ - meta = MetaInfo(title) - if not season: - season = meta.begin_season - exist = MediaServerOper(db).exists( - title=meta.name, year=year, mtype=mtype, tmdbid=tmdbid, season=season - ) - return schemas.Response(success=True if exist else False, data={ - "item": exist or {} - }) - - @router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo) def media_info(mediaid: str, type_name: str, _: schemas.TokenPayload = Depends(verify_token)) -> Any: diff --git a/app/api/endpoints/mediaserver.py b/app/api/endpoints/mediaserver.py new file mode 100644 index 00000000..16b1b3e5 --- /dev/null +++ b/app/api/endpoints/mediaserver.py @@ -0,0 +1,118 @@ +from typing import Any, List + +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.orm import Session +from starlette.responses import RedirectResponse + +from app import schemas +from app.chain.download import DownloadChain +from app.chain.media import MediaChain +from app.chain.mediaserver import MediaServerChain +from app.core.config import settings +from app.core.context import MediaInfo +from app.core.metainfo import MetaInfo +from app.core.security import verify_token +from app.db import get_db +from app.db.mediaserver_oper import MediaServerOper +from app.db.models import MediaServerItem +from app.schemas import MediaType, NotExistMediaInfo + +router = APIRouter() + + +@router.get("/play/{itemid}", summary="在线播放") +def play_item(itemid: str) -> Any: + """ + 跳转媒体服务器播放页面 + """ + if not itemid: + return + if not settings.MEDIASERVER: + return + mediaserver = settings.MEDIASERVER.split(",")[0] + play_url = MediaServerChain().get_play_url(server=mediaserver, item_id=itemid) + # 重定向到play_url + if not play_url: + return + return RedirectResponse(url=play_url) + + +@router.get("/exists", summary="本地是否存在", response_model=schemas.Response) +def exists(title: str = None, + year: int = None, + mtype: str = None, + tmdbid: int = None, + season: int = None, + db: Session = Depends(get_db), + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 判断本地是否存在 + """ + meta = MetaInfo(title) + if not season: + season = meta.begin_season + # 返回对象 + ret_info = {} + # 本地数据库是否存在 + exist: MediaServerItem = MediaServerOper(db).exists( + title=meta.name, year=year, mtype=mtype, tmdbid=tmdbid, season=season + ) + if not exist: + # 服务器是否存在 + mediainfo = MediaInfo() + mediainfo.from_dict({ + "title": meta.name, + "year": year or meta.year, + "type": mtype or meta.type, + "tmdb_id": tmdbid, + "season": season + }) + exist: schemas.ExistMediaInfo = MediaServerChain().media_exists( + mediainfo=mediainfo + ) + if exist: + ret_info = { + "id": exist.itemid + } + else: + ret_info = { + "id": exist.item_id + } + return schemas.Response(success=True if exist else False, data={ + "item": ret_info + }) + + +@router.post("/notexists", summary="查询缺失媒体信息", response_model=List[schemas.NotExistMediaInfo]) +def not_exists(media_in: schemas.MediaInfo, + _: schemas.TokenPayload = Depends(verify_token)) -> Any: + """ + 查询缺失媒体信息 + """ + # 媒体信息 + meta = MetaInfo(title=media_in.title) + mtype = MediaType(media_in.type) if media_in.type else None + if mtype: + meta.type = mtype + if media_in.season: + meta.begin_season = media_in.season + meta.type = MediaType.TV + if media_in.year: + meta.year = media_in.year + if media_in.tmdb_id or media_in.douban_id: + mediainfo = MediaChain().recognize_media(meta=meta, mtype=mtype, + tmdbid=media_in.tmdb_id, doubanid=media_in.douban_id) + else: + mediainfo = MediaChain().recognize_by_meta(metainfo=meta) + # 查询缺失信息 + if not mediainfo: + raise HTTPException(status_code=404, detail="媒体信息不存在") + mediakey = mediainfo.tmdb_id or mediainfo.douban_id + exist_flag, no_exists = DownloadChain().get_no_exists_info(meta=meta, mediainfo=mediainfo) + if mediainfo.type == MediaType.MOVIE: + # 电影已存在时返回空列表,存在时返回空对像列表 + return [] if exist_flag else [NotExistMediaInfo()] + elif no_exists and no_exists.get(mediakey): + # 电视剧返回缺失的剧集 + return list(no_exists.get(mediakey).values()) + return [] diff --git a/app/chain/mediaserver.py b/app/chain/mediaserver.py index 6e61fb86..f70949d1 100644 --- a/app/chain/mediaserver.py +++ b/app/chain/mediaserver.py @@ -1,6 +1,6 @@ import json import threading -from typing import List, Union +from typing import List, Union, Optional from app import schemas from app.chain import ChainBase @@ -56,6 +56,12 @@ class MediaServerChain(ChainBase): """ return self.run_module("mediaserver_latest", server=server, count=count) + def get_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: + """ + 获取播放地址 + """ + return self.run_module("mediaserver_play_url", server=server, item_id=item_id) + def sync(self): """ 同步媒体库所有数据到本地数据库 diff --git a/app/core/config.py b/app/core/config.py index 9c001efa..f76f4613 100644 --- a/app/core/config.py +++ b/app/core/config.py @@ -161,14 +161,20 @@ class Settings(BaseSettings): MEDIASERVER_SYNC_BLACKLIST: str = None # EMBY服务器地址,IP:PORT EMBY_HOST: str = None + # EMBY外网地址,http(s)://DOMAIN:PORT,未设置时使用EMBY_HOST + EMBY_PLAY_HOST: str = None # EMBY Api Key EMBY_API_KEY: str = None # Jellyfin服务器地址,IP:PORT JELLYFIN_HOST: str = None + # Jellyfin外网地址,http(s)://DOMAIN:PORT,未设置时使用JELLYFIN_HOST + JELLYFIN_PLAY_HOST: str = None # Jellyfin Api Key JELLYFIN_API_KEY: str = None # Plex服务器地址,IP:PORT PLEX_HOST: str = None + # Plex外网地址,http(s)://DOMAIN:PORT,未设置时使用PLEX_HOST + PLEX_PLAY_HOST: str = None # Plex Token PLEX_TOKEN: str = None # 转移方式 link/copy/move/softlink diff --git a/app/modules/emby/__init__.py b/app/modules/emby/__init__.py index 59824cb1..51256119 100644 --- a/app/modules/emby/__init__.py +++ b/app/modules/emby/__init__.py @@ -150,6 +150,14 @@ class EmbyModule(_ModuleBase): return [] return self.emby.get_resume(count) + def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: + """ + 获取媒体库播放地址 + """ + if server != "emby": + return None + return self.emby.get_play_url(item_id) + def mediaserver_latest(self, server: str, count: int = 20) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器最新入库条目 diff --git a/app/modules/emby/emby.py b/app/modules/emby/emby.py index 9a61f0c7..59616b9b 100644 --- a/app/modules/emby/emby.py +++ b/app/modules/emby/emby.py @@ -22,6 +22,12 @@ class Emby(metaclass=Singleton): self._host += "/" if not self._host.startswith("http"): self._host = "http://" + self._host + self._playhost = settings.EMBY_PLAY_HOST + if self._playhost: + if not self._playhost.endswith("/"): + self._playhost += "/" + if not self._playhost.startswith("http"): + self._playhost = "http://" + self._playhost self._apikey = settings.EMBY_API_KEY self.user = self.get_user(settings.SUPERUSER) self.folders = self.get_emby_folders() @@ -93,13 +99,17 @@ class Emby(metaclass=Singleton): library_type = MediaType.TV.value case _: continue + image = self.__get_local_image_by_id(library.get("Id")) libraries.append( schemas.MediaServerLibrary( server="emby", id=library.get("Id"), name=library.get("Name"), path=library.get("Path"), - type=library_type + type=library_type, + image=image, + link=f'{self._playhost or self._host}web/index.html' + f'#!/videos?serverId={self.serverid}&parentId={library.get("Id")}' ) ) return libraries @@ -909,12 +919,13 @@ class Emby(metaclass=Singleton): logger.error(f"连接Emby出错:" + str(e)) return None - def __get_play_url(self, item_id: str) -> str: + def get_play_url(self, item_id: str) -> str: """ 拼装媒体播放链接 :param item_id: 媒体的的ID """ - return f"{self._host}web/index.html#!/item?id={item_id}&context=home&serverId={self.serverid}" + return f"{self._playhost or self._host}web/index.html#!" \ + f"/item?id={item_id}&context=home&serverId={self.serverid}" def __get_backdrop_url(self, item_id: str, image_tag: str) -> str: """ @@ -958,7 +969,7 @@ class Emby(metaclass=Singleton): if item.get("Type") not in ["Movie", "Episode"]: continue item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value - link = self.__get_play_url(item.get("Id")) + link = self.get_play_url(item.get("Id")) if item_type == MediaType.MOVIE.value: title = item.get("Name") else: @@ -1008,7 +1019,7 @@ class Emby(metaclass=Singleton): if item.get("Type") not in ["Movie", "Series"]: continue item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value - link = self.__get_play_url(item.get("Id")) + link = self.get_play_url(item.get("Id")) image = self.__get_local_image_by_id(item_id=item.get("Id")) ret_latest.append(schemas.MediaServerPlayItem( id=item.get("Id"), diff --git a/app/modules/jellyfin/__init__.py b/app/modules/jellyfin/__init__.py index 7d5e600e..d80d608b 100644 --- a/app/modules/jellyfin/__init__.py +++ b/app/modules/jellyfin/__init__.py @@ -148,6 +148,14 @@ class JellyfinModule(_ModuleBase): return [] return self.jellyfin.get_resume(count) + def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: + """ + 获取媒体库播放地址 + """ + if server != "jellyfin": + return None + return self.jellyfin.get_play_url(item_id) + def mediaserver_latest(self, server: str, count: int = 20) -> List[schemas.MediaServerPlayItem]: """ 获取媒体服务器最新入库条目 diff --git a/app/modules/jellyfin/jellyfin.py b/app/modules/jellyfin/jellyfin.py index 5f3874bc..f6d85c26 100644 --- a/app/modules/jellyfin/jellyfin.py +++ b/app/modules/jellyfin/jellyfin.py @@ -20,6 +20,12 @@ class Jellyfin(metaclass=Singleton): self._host += "/" if not self._host.startswith("http"): self._host = "http://" + self._host + self._playhost = settings.JELLYFIN_PLAY_HOST + if self._playhost: + if not self._playhost.endswith("/"): + self._playhost += "/" + if not self._playhost.startswith("http"): + self._playhost = "http://" + self._playhost self._apikey = settings.JELLYFIN_API_KEY self.user = self.get_user(settings.SUPERUSER) self.serverid = self.get_server_id() @@ -72,13 +78,21 @@ class Jellyfin(metaclass=Singleton): library_type = MediaType.TV.value case _: continue + image = self.__get_local_image_by_id(library.get("Id")) + link = f"{self._playhost or self._host}web/index.html#!" \ + f"/movies.html?topParentId={library.get('Id')}" \ + if library_type == MediaType.MOVIE.value \ + else f"{self._playhost or self._host}web/index.html#!" \ + f"/tv.html?topParentId={library.get('Id')}" libraries.append( schemas.MediaServerLibrary( server="jellyfin", id=library.get("Id"), name=library.get("Name"), path=library.get("Path"), - type=library_type + type=library_type, + image=image, + link=link )) return libraries @@ -588,12 +602,13 @@ class Jellyfin(metaclass=Singleton): logger.error(f"连接Jellyfin出错:" + str(e)) return None - def __get_play_url(self, item_id: str) -> str: + def get_play_url(self, item_id: str) -> str: """ 拼装媒体播放链接 :param item_id: 媒体的的ID """ - return f"{self._host}web/index.html#!/details?id={item_id}&serverId={self.serverid}" + return f"{self._playhost or self._host}web/index.html#!" \ + f"/details?id={item_id}&serverId={self.serverid}" def __get_local_image_by_id(self, item_id: str) -> str: """ @@ -637,7 +652,7 @@ class Jellyfin(metaclass=Singleton): if item.get("Type") not in ["Movie", "Episode"]: continue item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value - link = self.__get_play_url(item.get("Id")) + link = self.get_play_url(item.get("Id")) if item.get("BackdropImageTags"): image = self.__get_backdrop_url(item_id=item.get("Id"), image_tag=item.get("BackdropImageTags")[0]) @@ -681,7 +696,7 @@ class Jellyfin(metaclass=Singleton): if item.get("Type") not in ["Movie", "Series"]: continue item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value - link = self.__get_play_url(item.get("Id")) + link = self.get_play_url(item.get("Id")) image = self.__get_local_image_by_id(item_id=item.get("Id")) ret_latest.append(schemas.MediaServerPlayItem( id=item.get("Id"), diff --git a/app/modules/plex/__init__.py b/app/modules/plex/__init__.py index 039c6d6f..ce193030 100644 --- a/app/modules/plex/__init__.py +++ b/app/modules/plex/__init__.py @@ -149,3 +149,11 @@ class PlexModule(_ModuleBase): if server != "plex": return [] return self.plex.get_latest(count) + + def mediaserver_play_url(self, server: str, item_id: Union[str, int]) -> Optional[str]: + """ + 获取媒体库播放地址 + """ + if server != "plex": + return None + return self.plex.get_play_url(item_id) diff --git a/app/modules/plex/plex.py b/app/modules/plex/plex.py index d0400dc0..5b15da5f 100644 --- a/app/modules/plex/plex.py +++ b/app/modules/plex/plex.py @@ -1,4 +1,5 @@ import json +from functools import lru_cache from pathlib import Path from typing import List, Optional, Dict, Tuple, Generator, Any from urllib.parse import quote_plus @@ -22,6 +23,12 @@ class Plex(metaclass=Singleton): self._host += "/" if not self._host.startswith("http"): self._host = "http://" + self._host + self._playhost = settings.PLEX_PLAY_HOST + if self._playhost: + if not self._playhost.endswith("/"): + self._playhost += "/" + if not self._playhost.startswith("http"): + self._playhost = "http://" + self._playhost self._token = settings.PLEX_TOKEN if self._host and self._token: try: @@ -50,6 +57,43 @@ class Plex(metaclass=Singleton): self._plex = None logger.error(f"Plex服务器连接失败:{str(e)}") + @lru_cache(maxsize=10) + def __get_library_images(self, library_key: str) -> Optional[List[str]]: + """ + 获取媒体服务器最近添加的媒体的图片列表 + param: library_key + param: type type的含义: 1 电影 2 剧集 详见 plexapi/utils.py中SEARCHTYPES的定义 + """ + if not self._plex: + return None + # 返回结果 + poster_urls = {} + # 页码计数 + container_start = 0 + # 需要的总条数/每页的条数 + total_size = 4 + # 如果总数不足,接续获取下一页 + while len(poster_urls) < total_size: + items = self._plex.fetchItems(f"/hubs/home/recentlyAdded?type={type}§ionID={library_key}", + container_size=total_size, + container_start=container_start) + for item in items: + if item.type == 'episode': + # 如果是剧集的单集,则去找上级的图片 + if item.parentThumb is not None: + poster_urls[item.parentThumb] = None + else: + # 否则就用自己的图片 + if item.thumb is not None: + poster_urls[item.thumb] = None + if len(poster_urls) == total_size: + break + if len(items) < total_size: + break + container_start += total_size + return [f"{self._host.rstrip('/') + url}?X-Plex-Token={self._token}" for url in + list(poster_urls.keys())[:total_size]] + def get_librarys(self) -> List[schemas.MediaServerLibrary]: """ 获取媒体服务器所有媒体库列表 @@ -70,12 +114,16 @@ class Plex(metaclass=Singleton): library_type = MediaType.TV.value case _: continue + image_list = self.__get_library_images(library.key) libraries.append( schemas.MediaServerLibrary( id=library.key, name=library.title, path=library.locations, - type=library_type + type=library_type, + image_list=image_list, + link=f"{self._playhost or self._host}#!/media/{self._plex.machineIdentifier}" + f"/com.plexapp.plugins.library?source={library.key}" ) ) return libraries @@ -544,12 +592,12 @@ class Plex(metaclass=Singleton): """ return self._plex - def __get_play_url(self, item_id: str) -> str: + def get_play_url(self, item_id: str) -> str: """ 拼装媒体播放链接 :param item_id: 媒体的的ID """ - return f'{self._host}#!/server/{self._plex.machineIdentifier}/details?key={item_id}' + return f'{self._playhost or self._host}#!/server/{self._plex.machineIdentifier}/details?key={item_id}' def get_resume(self, num: int = 12) -> Optional[List[schemas.MediaServerPlayItem]]: """ @@ -568,7 +616,7 @@ class Plex(metaclass=Singleton): name = "%s 第%s集" % (item.grandparentTitle, item.index) else: name = "%s 第%s季第%s集" % (item.grandparentTitle, item.parentIndex, item.index) - link = self.__get_play_url(item.key) + link = self.get_play_url(item.key) image = item.artUrl ret_resume.append(schemas.MediaServerPlayItem( id=item.key, @@ -590,7 +638,7 @@ class Plex(metaclass=Singleton): ret_resume = [] for item in items: item_type = MediaType.MOVIE.value if item.TYPE == "movie" else MediaType.TV.value - link = self.__get_play_url(item.key) + link = self.get_play_url(item.key) title = item.title if item_type == MediaType.MOVIE.value else \ "%s 第%s季" % (item.parentTitle, item.index) image = item.posterUrl diff --git a/app/schemas/mediaserver.py b/app/schemas/mediaserver.py index 970a4482..2bf2d832 100644 --- a/app/schemas/mediaserver.py +++ b/app/schemas/mediaserver.py @@ -66,6 +66,10 @@ class MediaServerLibrary(BaseModel): type: Optional[str] = None # 封面图 image: Optional[str] = None + # 封面图列表 + image_list: Optional[List[str]] = None + # 跳转链接 + link: Optional[str] = None class MediaServerItem(BaseModel): diff --git a/config/app.env b/config/app.env index 153ae9e6..ededf79b 100644 --- a/config/app.env +++ b/config/app.env @@ -87,16 +87,22 @@ TR_PASSWORD= #################################### # EMBY服务器地址,IP:PORT EMBY_HOST= +# EMBY外网地址,http(s)://DOMAIN:PORT,未设置时使用EMBY_HOST +EMBY_PLAY_HOST= # EMBY Api Key EMBY_API_KEY= # Jellyfin服务器地址,IP:PORT JELLYFIN_HOST= +# Jellyfin外网地址,http(s)://DOMAIN:PORT,未设置时使用JELLYFIN_HOST +JELLYFIN_PLAY_HOST= # Jellyfin Api Key JELLYFIN_API_KEY= # Plex服务器地址,IP:PORT PLEX_HOST= +# Plex外网地址,http(s)://DOMAIN:PORT,未设置时使用PLEX_HOST +PLEX_PLAY_HOST= # Plex Token PLEX_TOKEN=