feat:播放跳转api

This commit is contained in:
jxxghp 2024-01-03 12:02:08 +08:00
parent 322c72ab54
commit 0095e0f4dd
15 changed files with 260 additions and 80 deletions

View File

@ -195,16 +195,19 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
- `emby`设置项: - `emby`设置项:
- **EMBY_HOST** Emby服务器地址格式`ip:port`https需要添加`https://`前缀 - **EMBY_HOST** Emby服务器地址格式`ip:port`https需要添加`https://`前缀
- **EMBY_PLAY_HOST** EMBY外网地址格式`http(s)://DOMAIN:PORT`,未设置时使用`EMBY_HOST`
- **EMBY_API_KEY** Emby Api Key`设置->高级->API密钥`处生成 - **EMBY_API_KEY** Emby Api Key`设置->高级->API密钥`处生成
- `jellyfin`设置项: - `jellyfin`设置项:
- **JELLYFIN_HOST** Jellyfin服务器地址格式`ip:port`https需要添加`https://`前缀 - **JELLYFIN_HOST** Jellyfin服务器地址格式`ip:port`https需要添加`https://`前缀
- **JELLYFIN_PLAY_HOST** Jellyfin外网地址格式`http(s)://DOMAIN:PORT`,未设置时使用`JELLYFIN_HOST`
- **JELLYFIN_API_KEY** Jellyfin Api Key`设置->高级->API密钥`处生成 - **JELLYFIN_API_KEY** Jellyfin Api Key`设置->高级->API密钥`处生成
- `plex`设置项: - `plex`设置项:
- **PLEX_HOST** Plex服务器地址格式`ip:port`https需要添加`https://`前缀 - **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中获取 - **PLEX_TOKEN** Plex网页Url中的`X-Plex-Token`通过浏览器F12->网络从请求URL中获取
- **MEDIASERVER_SYNC_INTERVAL:** 媒体服务器同步间隔(小时),默认`6`,留空则不同步 - **MEDIASERVER_SYNC_INTERVAL:** 媒体服务器同步间隔(小时),默认`6`,留空则不同步
- **MEDIASERVER_SYNC_BLACKLIST:** 媒体服务器同步黑名单,多个媒体库名称使用,分割 - **MEDIASERVER_SYNC_BLACKLIST:** 媒体服务器同步黑名单,多个媒体库名称使用,分割

View File

@ -1,7 +1,7 @@
from fastapi import APIRouter from fastapi import APIRouter
from app.api.endpoints import login, user, site, message, webhook, subscribe, \ 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 = APIRouter()
api_router.include_router(login.router, prefix="/login", tags=["login"]) 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(dashboard.router, prefix="/dashboard", tags=["dashboard"])
api_router.include_router(filebrowser.router, prefix="/filebrowser", tags=["filebrowser"]) api_router.include_router(filebrowser.router, prefix="/filebrowser", tags=["filebrowser"])
api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"]) api_router.include_router(transfer.router, prefix="/transfer", tags=["transfer"])
api_router.include_router(mediaserver.router, prefix="/mediaserver", tags=["mediaserver"])

View File

@ -1,16 +1,14 @@
from typing import Any, List from typing import Any, List
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends
from app import schemas from app import schemas
from app.chain.download import DownloadChain from app.chain.download import DownloadChain
from app.chain.media import MediaChain
from app.core.context import MediaInfo, Context, TorrentInfo from app.core.context import MediaInfo, Context, TorrentInfo
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.core.security import verify_token from app.core.security import verify_token
from app.db.models.user import User from app.db.models.user import User
from app.db.userauth import get_current_active_user from app.db.userauth import get_current_active_user
from app.schemas import NotExistMediaInfo, MediaType
router = APIRouter() 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) @router.get("/start/{hashString}", summary="开始任务", response_model=schemas.Response)
def start_downloading( def start_downloading(
hashString: str, hashString: str,

View File

@ -1,7 +1,6 @@
from typing import List, Any from typing import List, Any
from fastapi import APIRouter, Depends from fastapi import APIRouter, Depends
from sqlalchemy.orm import Session
from app import schemas from app import schemas
from app.chain.media import MediaChain 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.context import Context
from app.core.metainfo import MetaInfo from app.core.metainfo import MetaInfo
from app.core.security import verify_token, verify_uri_token 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 from app.schemas import MediaType
router = APIRouter() router = APIRouter()
@ -79,28 +76,6 @@ def search_by_title(title: str,
return [] 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) @router.get("/{mediaid}", summary="查询媒体详情", response_model=schemas.MediaInfo)
def media_info(mediaid: str, type_name: str, def media_info(mediaid: str, type_name: str,
_: schemas.TokenPayload = Depends(verify_token)) -> Any: _: schemas.TokenPayload = Depends(verify_token)) -> Any:

View File

@ -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 []

View File

@ -1,6 +1,6 @@
import json import json
import threading import threading
from typing import List, Union from typing import List, Union, Optional
from app import schemas from app import schemas
from app.chain import ChainBase from app.chain import ChainBase
@ -56,6 +56,12 @@ class MediaServerChain(ChainBase):
""" """
return self.run_module("mediaserver_latest", server=server, count=count) 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): def sync(self):
""" """
同步媒体库所有数据到本地数据库 同步媒体库所有数据到本地数据库

View File

@ -161,14 +161,20 @@ class Settings(BaseSettings):
MEDIASERVER_SYNC_BLACKLIST: str = None MEDIASERVER_SYNC_BLACKLIST: str = None
# EMBY服务器地址IP:PORT # EMBY服务器地址IP:PORT
EMBY_HOST: str = None EMBY_HOST: str = None
# EMBY外网地址http(s)://DOMAIN:PORT未设置时使用EMBY_HOST
EMBY_PLAY_HOST: str = None
# EMBY Api Key # EMBY Api Key
EMBY_API_KEY: str = None EMBY_API_KEY: str = None
# Jellyfin服务器地址IP:PORT # Jellyfin服务器地址IP:PORT
JELLYFIN_HOST: str = None JELLYFIN_HOST: str = None
# Jellyfin外网地址http(s)://DOMAIN:PORT未设置时使用JELLYFIN_HOST
JELLYFIN_PLAY_HOST: str = None
# Jellyfin Api Key # Jellyfin Api Key
JELLYFIN_API_KEY: str = None JELLYFIN_API_KEY: str = None
# Plex服务器地址IP:PORT # Plex服务器地址IP:PORT
PLEX_HOST: str = None PLEX_HOST: str = None
# Plex外网地址http(s)://DOMAIN:PORT未设置时使用PLEX_HOST
PLEX_PLAY_HOST: str = None
# Plex Token # Plex Token
PLEX_TOKEN: str = None PLEX_TOKEN: str = None
# 转移方式 link/copy/move/softlink # 转移方式 link/copy/move/softlink

View File

@ -150,6 +150,14 @@ class EmbyModule(_ModuleBase):
return [] return []
return self.emby.get_resume(count) 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]: def mediaserver_latest(self, server: str, count: int = 20) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目

View File

@ -22,6 +22,12 @@ class Emby(metaclass=Singleton):
self._host += "/" self._host += "/"
if not self._host.startswith("http"): if not self._host.startswith("http"):
self._host = "http://" + self._host 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._apikey = settings.EMBY_API_KEY
self.user = self.get_user(settings.SUPERUSER) self.user = self.get_user(settings.SUPERUSER)
self.folders = self.get_emby_folders() self.folders = self.get_emby_folders()
@ -93,13 +99,17 @@ class Emby(metaclass=Singleton):
library_type = MediaType.TV.value library_type = MediaType.TV.value
case _: case _:
continue continue
image = self.__get_local_image_by_id(library.get("Id"))
libraries.append( libraries.append(
schemas.MediaServerLibrary( schemas.MediaServerLibrary(
server="emby", server="emby",
id=library.get("Id"), id=library.get("Id"),
name=library.get("Name"), name=library.get("Name"),
path=library.get("Path"), 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 return libraries
@ -909,12 +919,13 @@ class Emby(metaclass=Singleton):
logger.error(f"连接Emby出错" + str(e)) logger.error(f"连接Emby出错" + str(e))
return None return None
def __get_play_url(self, item_id: str) -> str: def get_play_url(self, item_id: str) -> str:
""" """
拼装媒体播放链接 拼装媒体播放链接
:param item_id: 媒体的的ID :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: 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"]: if item.get("Type") not in ["Movie", "Episode"]:
continue continue
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value 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: if item_type == MediaType.MOVIE.value:
title = item.get("Name") title = item.get("Name")
else: else:
@ -1008,7 +1019,7 @@ class Emby(metaclass=Singleton):
if item.get("Type") not in ["Movie", "Series"]: if item.get("Type") not in ["Movie", "Series"]:
continue continue
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value 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")) image = self.__get_local_image_by_id(item_id=item.get("Id"))
ret_latest.append(schemas.MediaServerPlayItem( ret_latest.append(schemas.MediaServerPlayItem(
id=item.get("Id"), id=item.get("Id"),

View File

@ -148,6 +148,14 @@ class JellyfinModule(_ModuleBase):
return [] return []
return self.jellyfin.get_resume(count) 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]: def mediaserver_latest(self, server: str, count: int = 20) -> List[schemas.MediaServerPlayItem]:
""" """
获取媒体服务器最新入库条目 获取媒体服务器最新入库条目

View File

@ -20,6 +20,12 @@ class Jellyfin(metaclass=Singleton):
self._host += "/" self._host += "/"
if not self._host.startswith("http"): if not self._host.startswith("http"):
self._host = "http://" + self._host 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._apikey = settings.JELLYFIN_API_KEY
self.user = self.get_user(settings.SUPERUSER) self.user = self.get_user(settings.SUPERUSER)
self.serverid = self.get_server_id() self.serverid = self.get_server_id()
@ -72,13 +78,21 @@ class Jellyfin(metaclass=Singleton):
library_type = MediaType.TV.value library_type = MediaType.TV.value
case _: case _:
continue 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( libraries.append(
schemas.MediaServerLibrary( schemas.MediaServerLibrary(
server="jellyfin", server="jellyfin",
id=library.get("Id"), id=library.get("Id"),
name=library.get("Name"), name=library.get("Name"),
path=library.get("Path"), path=library.get("Path"),
type=library_type type=library_type,
image=image,
link=link
)) ))
return libraries return libraries
@ -588,12 +602,13 @@ class Jellyfin(metaclass=Singleton):
logger.error(f"连接Jellyfin出错" + str(e)) logger.error(f"连接Jellyfin出错" + str(e))
return None return None
def __get_play_url(self, item_id: str) -> str: def get_play_url(self, item_id: str) -> str:
""" """
拼装媒体播放链接 拼装媒体播放链接
:param item_id: 媒体的的ID :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: 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"]: if item.get("Type") not in ["Movie", "Episode"]:
continue continue
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value 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"): if item.get("BackdropImageTags"):
image = self.__get_backdrop_url(item_id=item.get("Id"), image = self.__get_backdrop_url(item_id=item.get("Id"),
image_tag=item.get("BackdropImageTags")[0]) image_tag=item.get("BackdropImageTags")[0])
@ -681,7 +696,7 @@ class Jellyfin(metaclass=Singleton):
if item.get("Type") not in ["Movie", "Series"]: if item.get("Type") not in ["Movie", "Series"]:
continue continue
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value 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")) image = self.__get_local_image_by_id(item_id=item.get("Id"))
ret_latest.append(schemas.MediaServerPlayItem( ret_latest.append(schemas.MediaServerPlayItem(
id=item.get("Id"), id=item.get("Id"),

View File

@ -149,3 +149,11 @@ class PlexModule(_ModuleBase):
if server != "plex": if server != "plex":
return [] return []
return self.plex.get_latest(count) 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)

View File

@ -1,4 +1,5 @@
import json import json
from functools import lru_cache
from pathlib import Path from pathlib import Path
from typing import List, Optional, Dict, Tuple, Generator, Any from typing import List, Optional, Dict, Tuple, Generator, Any
from urllib.parse import quote_plus from urllib.parse import quote_plus
@ -22,6 +23,12 @@ class Plex(metaclass=Singleton):
self._host += "/" self._host += "/"
if not self._host.startswith("http"): if not self._host.startswith("http"):
self._host = "http://" + self._host 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 self._token = settings.PLEX_TOKEN
if self._host and self._token: if self._host and self._token:
try: try:
@ -50,6 +57,43 @@ class Plex(metaclass=Singleton):
self._plex = None self._plex = None
logger.error(f"Plex服务器连接失败{str(e)}") 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}&sectionID={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]: def get_librarys(self) -> List[schemas.MediaServerLibrary]:
""" """
获取媒体服务器所有媒体库列表 获取媒体服务器所有媒体库列表
@ -70,12 +114,16 @@ class Plex(metaclass=Singleton):
library_type = MediaType.TV.value library_type = MediaType.TV.value
case _: case _:
continue continue
image_list = self.__get_library_images(library.key)
libraries.append( libraries.append(
schemas.MediaServerLibrary( schemas.MediaServerLibrary(
id=library.key, id=library.key,
name=library.title, name=library.title,
path=library.locations, 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 return libraries
@ -544,12 +592,12 @@ class Plex(metaclass=Singleton):
""" """
return self._plex 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 :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]]: 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) name = "%s%s" % (item.grandparentTitle, item.index)
else: else:
name = "%s%s季第%s" % (item.grandparentTitle, item.parentIndex, item.index) 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 image = item.artUrl
ret_resume.append(schemas.MediaServerPlayItem( ret_resume.append(schemas.MediaServerPlayItem(
id=item.key, id=item.key,
@ -590,7 +638,7 @@ class Plex(metaclass=Singleton):
ret_resume = [] ret_resume = []
for item in items: for item in items:
item_type = MediaType.MOVIE.value if item.TYPE == "movie" else MediaType.TV.value 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 \ title = item.title if item_type == MediaType.MOVIE.value else \
"%s%s" % (item.parentTitle, item.index) "%s%s" % (item.parentTitle, item.index)
image = item.posterUrl image = item.posterUrl

View File

@ -66,6 +66,10 @@ class MediaServerLibrary(BaseModel):
type: Optional[str] = None type: Optional[str] = None
# 封面图 # 封面图
image: Optional[str] = None image: Optional[str] = None
# 封面图列表
image_list: Optional[List[str]] = None
# 跳转链接
link: Optional[str] = None
class MediaServerItem(BaseModel): class MediaServerItem(BaseModel):

View File

@ -87,16 +87,22 @@ TR_PASSWORD=
#################################### ####################################
# EMBY服务器地址IP:PORT # EMBY服务器地址IP:PORT
EMBY_HOST= EMBY_HOST=
# EMBY外网地址http(s)://DOMAIN:PORT未设置时使用EMBY_HOST
EMBY_PLAY_HOST=
# EMBY Api Key # EMBY Api Key
EMBY_API_KEY= EMBY_API_KEY=
# Jellyfin服务器地址IP:PORT # Jellyfin服务器地址IP:PORT
JELLYFIN_HOST= JELLYFIN_HOST=
# Jellyfin外网地址http(s)://DOMAIN:PORT未设置时使用JELLYFIN_HOST
JELLYFIN_PLAY_HOST=
# Jellyfin Api Key # Jellyfin Api Key
JELLYFIN_API_KEY= JELLYFIN_API_KEY=
# Plex服务器地址IP:PORT # Plex服务器地址IP:PORT
PLEX_HOST= PLEX_HOST=
# Plex外网地址http(s)://DOMAIN:PORT未设置时使用PLEX_HOST
PLEX_PLAY_HOST=
# Plex Token # Plex Token
PLEX_TOKEN= PLEX_TOKEN=