feat:播放跳转api
This commit is contained in:
parent
322c72ab54
commit
0095e0f4dd
@ -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:** 媒体服务器同步黑名单,多个媒体库名称使用,分割
|
||||
|
@ -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"])
|
||||
|
@ -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,
|
||||
|
@ -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:
|
||||
|
118
app/api/endpoints/mediaserver.py
Normal file
118
app/api/endpoints/mediaserver.py
Normal 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 []
|
@ -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):
|
||||
"""
|
||||
同步媒体库所有数据到本地数据库
|
||||
|
@ -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
|
||||
|
@ -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]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
|
@ -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"),
|
||||
|
@ -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]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
|
@ -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"),
|
||||
|
@ -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)
|
||||
|
@ -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
|
||||
|
@ -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):
|
||||
|
@ -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=
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user