mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-05-10 17:42:45 +08:00
Compare commits
60 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6a2cde0664 | ||
|
|
c86cc2cb51 | ||
|
|
6d7a63ff61 | ||
|
|
c044e59481 | ||
|
|
3c31bf24e5 | ||
|
|
d89c80ac89 | ||
|
|
8236d6c8d7 | ||
|
|
3646540a7f | ||
|
|
c1ecdfc61d | ||
|
|
7587946d51 | ||
|
|
3ad64baaeb | ||
|
|
24c43b53a2 | ||
|
|
53a6a1c691 | ||
|
|
c3ba83c7ca | ||
|
|
d9b349873e | ||
|
|
4dcefb141a | ||
|
|
c674e32046 | ||
|
|
8aa1027aae | ||
|
|
b4cb9c3fb3 | ||
|
|
d82ab5d60d | ||
|
|
979b636eec | ||
|
|
bf8a75b201 | ||
|
|
87111c8736 | ||
|
|
9b97e478aa | ||
|
|
2af7abee3c | ||
|
|
2c8a41ebad | ||
|
|
c632cfd6b9 | ||
|
|
7f05df2fb3 | ||
|
|
ff33432809 | ||
|
|
0a57e69bcf | ||
|
|
7af8b15dbb | ||
|
|
bc4931d971 | ||
|
|
cfb029b6b4 | ||
|
|
6fa50101a6 | ||
|
|
843fbc83f4 | ||
|
|
55f8fb3b66 | ||
|
|
a47774472d | ||
|
|
713f4ca356 | ||
|
|
b06795510a | ||
|
|
0f57ec099a | ||
|
|
8325caabdc | ||
|
|
44d276d7e7 | ||
|
|
935340561b | ||
|
|
a60fde3b91 | ||
|
|
163a855d5c | ||
|
|
c9b1e75361 | ||
|
|
a9932d0866 | ||
|
|
11d29919bf | ||
|
|
4fe755332d | ||
|
|
0095e0f4dd | ||
|
|
322c72ab54 | ||
|
|
4d51459a47 | ||
|
|
d51de30898 | ||
|
|
90f9edbf24 | ||
|
|
8aa10457a7 | ||
|
|
ab584720c6 | ||
|
|
56ad281cb6 | ||
|
|
61281cca02 | ||
|
|
b53dbbc38e | ||
|
|
3f88cfba28 |
@@ -108,6 +108,7 @@ MoviePilot需要配套下载器和媒体服务器配合使用。
|
||||
- **TMDB_IMAGE_DOMAIN:** TMDB图片地址,默认`image.tmdb.org`,可配置为其它中转代理以加速TMDB图片显示,如:`static-mdb.v.geilijiasu.com`
|
||||
- **WALLPAPER:** 登录首页电影海报,`tmdb`/`bing`,默认`tmdb`
|
||||
- **RECOGNIZE_SOURCE:** 媒体信息识别来源,`themoviedb`/`douban`,默认`themoviedb`,使用`douban`时不支持二级分类
|
||||
- **FANART_ENABLE:** Fanart开关,`true`/`false`,默认`true`,关闭后刮削的图片类型会大幅减少
|
||||
---
|
||||
- **SCRAP_METADATA:** 刮削入库的媒体文件,`true`/`false`,默认`true`
|
||||
- **SCRAP_SOURCE:** 刮削元数据及图片使用的数据源,`themoviedb`/`douban`,默认`themoviedb`
|
||||
@@ -195,16 +196,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,
|
||||
|
||||
@@ -56,7 +56,9 @@ async def login_access_token(
|
||||
logger.info(f"用户 {user.name} 登录成功!")
|
||||
return schemas.Token(
|
||||
access_token=security.create_access_token(
|
||||
user.id,
|
||||
userid=user.id,
|
||||
username=user.name,
|
||||
super_user=user.is_superuser,
|
||||
expires_delta=timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES)
|
||||
),
|
||||
token_type="bearer",
|
||||
|
||||
@@ -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:
|
||||
|
||||
146
app/api/endpoints/mediaserver.py
Normal file
146
app/api/endpoints/mediaserver.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from typing import Any, List
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
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.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) -> schemas.Response:
|
||||
"""
|
||||
获取媒体服务器播放页面地址
|
||||
"""
|
||||
if not itemid:
|
||||
return schemas.Response(success=False, msg="参数错误")
|
||||
if not settings.MEDIASERVER:
|
||||
return schemas.Response(success=False, msg="未配置媒体服务器")
|
||||
mediaserver = settings.MEDIASERVER.split(",")[0]
|
||||
play_url = MediaServerChain().get_play_url(server=mediaserver, item_id=itemid)
|
||||
# 重定向到play_url
|
||||
if not play_url:
|
||||
return schemas.Response(success=False, msg="未找到播放地址")
|
||||
return schemas.Response(success=True, data={
|
||||
"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 exist:
|
||||
ret_info = {
|
||||
"id": exist.item_id
|
||||
}
|
||||
"""
|
||||
else:
|
||||
# 服务器是否存在
|
||||
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
|
||||
}
|
||||
"""
|
||||
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 []
|
||||
|
||||
|
||||
@router.get("/latest", summary="最新入库条目", response_model=List[schemas.MediaServerPlayItem])
|
||||
def latest(count: int = 18,
|
||||
userinfo: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
return MediaServerChain().latest(count=count, username=userinfo.username) or []
|
||||
|
||||
|
||||
@router.get("/playing", summary="正在播放条目", response_model=List[schemas.MediaServerPlayItem])
|
||||
def playing(count: int = 12,
|
||||
userinfo: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
获取媒体服务器正在播放条目
|
||||
"""
|
||||
return MediaServerChain().playing(count=count, username=userinfo.username) or []
|
||||
|
||||
|
||||
@router.get("/library", summary="媒体库列表", response_model=List[schemas.MediaServerLibrary])
|
||||
def library(userinfo: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
"""
|
||||
获取媒体服务器媒体库列表
|
||||
"""
|
||||
return MediaServerChain().librarys(username=userinfo.username) or []
|
||||
@@ -35,14 +35,12 @@ def all_plugins(_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||
elif plugin.get("has_update"):
|
||||
plugin["installed"] = False
|
||||
plugins.append(plugin)
|
||||
# 本地未安装的插件
|
||||
# 本地插件存在但未安装,且本地插件不在online插件中
|
||||
plugin_ids = [plugin["id"] for plugin in plugins]
|
||||
for plugin in local_plugins:
|
||||
if plugin["id"] not in installed_ids:
|
||||
if plugin["id"] not in installed_ids \
|
||||
and plugin["id"] not in plugin_ids:
|
||||
plugins.append(plugin)
|
||||
elif plugin.get("has_update"):
|
||||
plugin["installed"] = False
|
||||
plugins.append(plugin)
|
||||
|
||||
return plugins
|
||||
|
||||
|
||||
|
||||
@@ -24,14 +24,17 @@ from version import APP_VERSION
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/img/{imgurl:path}", summary="图片代理")
|
||||
def get_img(imgurl: str) -> Any:
|
||||
@router.get("/img/{imgurl:path}/{proxy}", summary="图片代理")
|
||||
def get_img(imgurl: str, proxy: bool = False) -> Any:
|
||||
"""
|
||||
通过图片代理(使用代理服务器)
|
||||
"""
|
||||
if not imgurl:
|
||||
return None
|
||||
response = RequestUtils(ua=settings.USER_AGENT, proxies=settings.PROXY).get_res(url=imgurl)
|
||||
if proxy:
|
||||
response = RequestUtils(ua=settings.USER_AGENT, proxies=settings.PROXY).get_res(url=imgurl)
|
||||
else:
|
||||
response = RequestUtils(ua=settings.USER_AGENT).get_res(url=imgurl)
|
||||
if response:
|
||||
return Response(content=response.content, media_type="image/jpeg")
|
||||
return None
|
||||
|
||||
@@ -55,6 +55,8 @@ class DownloadChain(ChainBase):
|
||||
msg_text = f"{msg_text}\n种子:{torrent.title}"
|
||||
if torrent.pubdate:
|
||||
msg_text = f"{msg_text}\n发布时间:{torrent.pubdate}"
|
||||
if torrent.freedate:
|
||||
msg_text = f"{msg_text}\n免费时间:{StringUtils.diff_time_str(torrent.freedate)}"
|
||||
if torrent.seeders:
|
||||
msg_text = f"{msg_text}\n做种数:{torrent.seeders}"
|
||||
if torrent.uploadvolumefactor and torrent.downloadvolumefactor:
|
||||
@@ -355,12 +357,13 @@ class DownloadChain(ChainBase):
|
||||
need = list(set(_need).difference(set(_current)))
|
||||
# 清除已下载的季信息
|
||||
seas = copy.deepcopy(no_exists.get(_mid))
|
||||
for _sea in list(seas):
|
||||
if _sea not in need:
|
||||
no_exists[_mid].pop(_sea)
|
||||
if not no_exists.get(_mid) and no_exists.get(_mid) is not None:
|
||||
no_exists.pop(_mid)
|
||||
break
|
||||
if seas:
|
||||
for _sea in list(seas):
|
||||
if _sea not in need:
|
||||
no_exists[_mid].pop(_sea)
|
||||
if not no_exists.get(_mid) and no_exists.get(_mid) is not None:
|
||||
no_exists.pop(_mid)
|
||||
break
|
||||
return need
|
||||
|
||||
def __update_episodes(_mid: Union[int, str], _sea: int, _need: list, _current: set) -> list:
|
||||
@@ -488,6 +491,9 @@ class DownloadChain(ChainBase):
|
||||
need_season = __update_seasons(_mid=need_mid,
|
||||
_need=need_season,
|
||||
_current=torrent_season)
|
||||
if not need_season:
|
||||
# 全部下载完成
|
||||
break
|
||||
# 电视剧季内的集匹配
|
||||
if no_exists:
|
||||
# TMDBID列表
|
||||
|
||||
@@ -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
|
||||
@@ -20,11 +20,11 @@ class MediaServerChain(ChainBase):
|
||||
super().__init__()
|
||||
self.dboper = MediaServerOper()
|
||||
|
||||
def librarys(self, server: str) -> List[schemas.MediaServerLibrary]:
|
||||
def librarys(self, server: str = None, username: str = None) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库
|
||||
"""
|
||||
return self.run_module("mediaserver_librarys", server=server)
|
||||
return self.run_module("mediaserver_librarys", server=server, username=username)
|
||||
|
||||
def items(self, server: str, library_id: Union[str, int]) -> List[schemas.MediaServerItem]:
|
||||
"""
|
||||
@@ -44,6 +44,24 @@ class MediaServerChain(ChainBase):
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
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]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
return self.run_module("mediaserver_latest", count=count, server=server, username=username)
|
||||
|
||||
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):
|
||||
"""
|
||||
同步媒体库所有数据到本地数据库
|
||||
|
||||
@@ -2,7 +2,7 @@ import pickle
|
||||
import re
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from datetime import datetime
|
||||
from typing import Dict
|
||||
from typing import Dict, Tuple
|
||||
from typing import List, Optional
|
||||
|
||||
from app.chain import ChainBase
|
||||
@@ -153,6 +153,7 @@ class SearchChain(ChainBase):
|
||||
return []
|
||||
# 使用过滤规则再次过滤
|
||||
torrents = self.filter_torrents_by_rule(torrents=torrents,
|
||||
mediainfo=mediainfo,
|
||||
filter_rule=filter_rule)
|
||||
if not torrents:
|
||||
logger.warn(f'{keyword or mediainfo.title} 没有符合过滤规则的资源')
|
||||
@@ -336,12 +337,14 @@ class SearchChain(ChainBase):
|
||||
|
||||
def filter_torrents_by_rule(self,
|
||||
torrents: List[TorrentInfo],
|
||||
filter_rule: Dict[str, str] = None
|
||||
mediainfo: MediaInfo,
|
||||
filter_rule: Dict[str, str] = None,
|
||||
) -> List[TorrentInfo]:
|
||||
"""
|
||||
使用过滤规则过滤种子
|
||||
:param torrents: 种子列表
|
||||
:param filter_rule: 过滤规则
|
||||
:param mediainfo: 媒体信息
|
||||
"""
|
||||
|
||||
if not filter_rule:
|
||||
@@ -359,6 +362,26 @@ class SearchChain(ChainBase):
|
||||
resolution = filter_rule.get("resolution")
|
||||
# 特效
|
||||
effect = filter_rule.get("effect")
|
||||
# 电影大小
|
||||
movie_size = filter_rule.get("movie_size")
|
||||
# 剧集单集大小
|
||||
tv_size = filter_rule.get("tv_size")
|
||||
|
||||
def __get_size_range(size_str: str) -> Tuple[float, float]:
|
||||
"""
|
||||
获取大小范围
|
||||
"""
|
||||
if not size_str:
|
||||
return 0, 0
|
||||
try:
|
||||
size_range = size_str.split("-")
|
||||
if len(size_range) == 1:
|
||||
return 0, float(size_range[0])
|
||||
elif len(size_range) == 2:
|
||||
return float(size_range[0]), float(size_range[1])
|
||||
except Exception as e:
|
||||
print(str(e))
|
||||
return 0, 0
|
||||
|
||||
def __filter_torrent(t: TorrentInfo) -> bool:
|
||||
"""
|
||||
@@ -394,6 +417,36 @@ class SearchChain(ChainBase):
|
||||
logger.info(f"{t.title} 不匹配特效规则 {effect}")
|
||||
return False
|
||||
|
||||
# 大小
|
||||
if movie_size or tv_size:
|
||||
if mediainfo.type == MediaType.TV:
|
||||
size = tv_size
|
||||
else:
|
||||
size = movie_size
|
||||
# 大小范围
|
||||
begin_size, end_size = __get_size_range(size)
|
||||
if begin_size and end_size:
|
||||
meta = MetaInfo(title=t.title, subtitle=t.description)
|
||||
# 集数
|
||||
if mediainfo.type == MediaType.TV:
|
||||
# 电视剧
|
||||
season = meta.begin_season or 1
|
||||
if meta.total_episode:
|
||||
# 识别的总集数
|
||||
episodes_num = meta.total_episode
|
||||
else:
|
||||
# 整季集数
|
||||
episodes_num = len(mediainfo.seasons.get(season) or [1])
|
||||
# 比较大小
|
||||
if not (begin_size * 1024 ** 3 <= (t.size / episodes_num) <= end_size * 1024 ** 3):
|
||||
logger.info(f"{t.title} {StringUtils.str_filesize(t.size)} "
|
||||
f"共{episodes_num}集,不匹配大小规则 {size}")
|
||||
return False
|
||||
else:
|
||||
# 电影比较大小
|
||||
if not (begin_size * 1024 ** 3 <= t.size <= end_size * 1024 ** 3):
|
||||
logger.info(f"{t.title} {StringUtils.str_filesize(t.size)} 不匹配大小规则 {size}")
|
||||
return False
|
||||
return True
|
||||
|
||||
# 使用默认过滤规则再次过滤
|
||||
|
||||
@@ -59,6 +59,8 @@ class Settings(BaseSettings):
|
||||
TMDB_API_KEY: str = "db55323b8d3e4154498498a75642b381"
|
||||
# TVDB API Key
|
||||
TVDB_API_KEY: str = "6b481081-10aa-440c-99f2-21d17717ee02"
|
||||
# Fanart开关
|
||||
FANART_ENABLE: bool = True
|
||||
# Fanart API Key
|
||||
FANART_API_KEY: str = "d2d31f9ecabea050fc7d68aa3146015f"
|
||||
# 支持的后缀格式
|
||||
@@ -161,14 +163,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
|
||||
|
||||
@@ -6,6 +6,7 @@ from app.core.config import settings
|
||||
from app.core.meta import MetaBase
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.string import StringUtils
|
||||
|
||||
|
||||
@dataclass
|
||||
@@ -44,6 +45,8 @@ class TorrentInfo:
|
||||
pubdate: str = None
|
||||
# 已过时间
|
||||
date_elapsed: str = None
|
||||
# 免费截止时间
|
||||
freedate: str = None
|
||||
# 上传因子
|
||||
uploadvolumefactor: float = None
|
||||
# 下载因子
|
||||
@@ -90,7 +93,9 @@ class TorrentInfo:
|
||||
"1.0 1.0": "普通",
|
||||
"1.0 0.0": "免费",
|
||||
"2.0 1.0": "2X",
|
||||
"4.0 1.0": "4X",
|
||||
"2.0 0.0": "2X免费",
|
||||
"4.0 0.0": "4X免费",
|
||||
"1.0 0.5": "50%",
|
||||
"2.0 0.5": "2X 50%",
|
||||
"1.0 0.7": "70%",
|
||||
@@ -105,12 +110,22 @@ class TorrentInfo:
|
||||
"""
|
||||
return self.get_free_string(self.uploadvolumefactor, self.downloadvolumefactor)
|
||||
|
||||
@property
|
||||
def freedate_diff(self):
|
||||
"""
|
||||
返回免费剩余时间
|
||||
"""
|
||||
if not self.freedate:
|
||||
return ""
|
||||
return StringUtils.diff_time_str(self.freedate)
|
||||
|
||||
def to_dict(self):
|
||||
"""
|
||||
返回字典
|
||||
"""
|
||||
dicts = asdict(self)
|
||||
dicts["volume_factor"] = self.volume_factor
|
||||
dicts["freedate_diff"] = self.freedate_diff
|
||||
return dicts
|
||||
|
||||
|
||||
|
||||
@@ -270,7 +270,7 @@ class MetaVideo(MetaBase):
|
||||
self.tokens.get_next()
|
||||
self._last_token_type = "part"
|
||||
self._continue_flag = False
|
||||
self._stop_name_flag = False
|
||||
# self._stop_name_flag = False
|
||||
|
||||
def __init_year(self, token: str):
|
||||
if not self.name:
|
||||
|
||||
@@ -213,6 +213,26 @@ class PluginManager(metaclass=Singleton):
|
||||
ret_apis.extend(apis)
|
||||
return ret_apis
|
||||
|
||||
def get_plugin_services(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件服务
|
||||
[{
|
||||
"id": "服务ID",
|
||||
"name": "服务名称",
|
||||
"trigger": "触发器:cron、interval、date、CronTrigger.from_crontab()",
|
||||
"func": self.xxx,
|
||||
"kwagrs": {} # 定时器参数
|
||||
}]
|
||||
"""
|
||||
ret_services = []
|
||||
for pid, plugin in self._running_plugins.items():
|
||||
if hasattr(plugin, "get_service") \
|
||||
and ObjectUtils.check_method(plugin.get_service):
|
||||
services = plugin.get_service()
|
||||
if services:
|
||||
ret_services.extend(services)
|
||||
return ret_services
|
||||
|
||||
def run_plugin_method(self, pid: str, method: str, *args, **kwargs) -> Any:
|
||||
"""
|
||||
运行插件方法
|
||||
@@ -243,6 +263,8 @@ class PluginManager(metaclass=Singleton):
|
||||
markets = settings.PLUGIN_MARKET.split(",")
|
||||
for market in markets:
|
||||
online_plugins = self.pluginhelper.get_plugins(market) or {}
|
||||
if not online_plugins:
|
||||
logger.warn(f"获取插件库失败 {market}")
|
||||
for pid, plugin in online_plugins.items():
|
||||
# 运行状插件
|
||||
plugin_obj = self._running_plugins.get(pid)
|
||||
|
||||
@@ -26,7 +26,8 @@ reusable_oauth2 = OAuth2PasswordBearer(
|
||||
|
||||
|
||||
def create_access_token(
|
||||
subject: Union[str, Any], expires_delta: timedelta = None
|
||||
userid: Union[str, Any], username: str, super_user: bool = False,
|
||||
expires_delta: timedelta = None
|
||||
) -> str:
|
||||
if expires_delta:
|
||||
expire = datetime.utcnow() + expires_delta
|
||||
@@ -34,7 +35,12 @@ def create_access_token(
|
||||
expire = datetime.utcnow() + timedelta(
|
||||
minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES
|
||||
)
|
||||
to_encode = {"exp": expire, "sub": str(subject)}
|
||||
to_encode = {
|
||||
"exp": expire,
|
||||
"sub": str(userid),
|
||||
"username": username,
|
||||
"super_user": super_user
|
||||
}
|
||||
encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=ALGORITHM)
|
||||
return encoded_jwt
|
||||
|
||||
|
||||
23
app/main.py
23
app/main.py
@@ -24,9 +24,10 @@ from app.helper.thread import ThreadHelper
|
||||
from app.helper.display import DisplayHelper
|
||||
from app.helper.resource import ResourceHelper
|
||||
from app.helper.sites import SitesHelper
|
||||
from app.helper.message import MessageHelper
|
||||
from app.scheduler import Scheduler
|
||||
from app.command import Command
|
||||
|
||||
from app.command import Command, CommandChian
|
||||
from app.schemas import Notification, NotificationType
|
||||
|
||||
# App
|
||||
App = FastAPI(title=settings.PROJECT_NAME,
|
||||
@@ -139,6 +140,22 @@ def start_tray():
|
||||
threading.Thread(target=TrayIcon.run, daemon=True).start()
|
||||
|
||||
|
||||
def check_auth():
|
||||
"""
|
||||
检查认证状态
|
||||
"""
|
||||
if SitesHelper().auth_level < 2:
|
||||
err_msg = "用户认证失败,站点相关功能将无法使用!"
|
||||
MessageHelper().put(f"注意:{err_msg}")
|
||||
CommandChian().post_message(
|
||||
Notification(
|
||||
mtype=NotificationType.Manual,
|
||||
title="MoviePilot用户认证",
|
||||
text=err_msg
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@App.on_event("shutdown")
|
||||
def shutdown_server():
|
||||
"""
|
||||
@@ -183,6 +200,8 @@ def start_module():
|
||||
init_routers()
|
||||
# 启动前端服务
|
||||
start_frontend()
|
||||
# 检查认证状态
|
||||
check_auth()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
@@ -103,13 +103,13 @@ class EmbyModule(_ModuleBase):
|
||||
media_statistic.user_count = self.emby.get_user_count()
|
||||
return [media_statistic]
|
||||
|
||||
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
def mediaserver_librarys(self, server: str = None, username: str = None) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
if server != "emby":
|
||||
if server and server != "emby":
|
||||
return None
|
||||
return self.emby.get_librarys()
|
||||
return self.emby.get_librarys(username)
|
||||
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||
"""
|
||||
@@ -141,3 +141,29 @@ class EmbyModule(_ModuleBase):
|
||||
season=season,
|
||||
episodes=episodes
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, count: int = 20,
|
||||
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
if server and server != "emby":
|
||||
return []
|
||||
return self.emby.get_resume(num=count, username=username)
|
||||
|
||||
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, count: int = 20,
|
||||
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
if server and server != "emby":
|
||||
return []
|
||||
return self.emby.get_latest(num=count, username=username)
|
||||
|
||||
@@ -22,9 +22,16 @@ 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()
|
||||
self.serverid = self.get_server_id()
|
||||
|
||||
def is_inactive(self) -> bool:
|
||||
"""
|
||||
@@ -59,13 +66,17 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接Library/SelectableMediaFolders 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def __get_emby_librarys(self) -> List[dict]:
|
||||
def __get_emby_librarys(self, username: str = None) -> List[dict]:
|
||||
"""
|
||||
获取Emby媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
req_url = f"{self._host}emby/Users/{self.user}/Views?api_key={self._apikey}"
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
req_url = f"{self._host}emby/Users/{user}/Views?api_key={self._apikey}"
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
@@ -77,14 +88,17 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接User/Views 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
def get_librarys(self, username: str = None) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
libraries = []
|
||||
for library in self.__get_emby_librarys() or []:
|
||||
black_list = (settings.MEDIASERVER_SYNC_BLACKLIST or '').split(",")
|
||||
for library in self.__get_emby_librarys(username) or []:
|
||||
if library.get("Name") in black_list:
|
||||
continue
|
||||
match library.get("CollectionType"):
|
||||
case "movies":
|
||||
library_type = MediaType.MOVIE.value
|
||||
@@ -92,13 +106,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
|
||||
@@ -907,3 +925,160 @@ class Emby(metaclass=Singleton):
|
||||
except Exception as e:
|
||||
logger.error(f"连接Emby出错:" + str(e))
|
||||
return None
|
||||
|
||||
def get_play_url(self, item_id: str) -> str:
|
||||
"""
|
||||
拼装媒体播放链接
|
||||
:param item_id: 媒体的的ID
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
获取Emby的Backdrop图片地址
|
||||
:param: item_id: 在Emby中的ID
|
||||
:param: image_tag: 图片的tag
|
||||
:param: remote 是否远程使用,TG微信等客户端调用应为True
|
||||
:param: inner 是否NT内部调用,为True是会使用NT中转
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return ""
|
||||
if not image_tag or not item_id:
|
||||
return ""
|
||||
return f"{self._host}Items/{item_id}/" \
|
||||
f"Images/Backdrop?tag={image_tag}&fillWidth=666&api_key={self._apikey}"
|
||||
|
||||
def __get_local_image_by_id(self, item_id: str) -> str:
|
||||
"""
|
||||
根据ItemId从媒体服务器查询本地图片地址
|
||||
:param: item_id: 在Emby中的ID
|
||||
:param: remote 是否远程使用,TG微信等客户端调用应为True
|
||||
:param: inner 是否NT内部调用,为True是会使用NT中转
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return ""
|
||||
return "%sItems/%s/Images/Primary" % (self._host, item_id)
|
||||
|
||||
def get_resume(self, num: int = 12, username: str = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获得继续观看
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
req_url = (f"{self._host}Users/{user}/Items/Resume?"
|
||||
f"Limit=100&MediaTypes=Video&api_key={self._apikey}&Fields=ProductionYear,Path")
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
result = res.json().get("Items") or []
|
||||
ret_resume = []
|
||||
# 用户媒体库文件夹列表(排除黑名单)
|
||||
library_folders = self.get_user_library_folders()
|
||||
for item in result:
|
||||
if len(ret_resume) == num:
|
||||
break
|
||||
if item.get("Type") not in ["Movie", "Episode"]:
|
||||
continue
|
||||
item_path = item.get("Path")
|
||||
if item_path and library_folders and not any(
|
||||
str(item_path).startswith(folder) for folder in library_folders):
|
||||
continue
|
||||
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value
|
||||
link = self.get_play_url(item.get("Id"))
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
title = item.get("Name")
|
||||
subtitle = item.get("ProductionYear")
|
||||
else:
|
||||
title = f'{item.get("SeriesName")}'
|
||||
subtitle = f'S{item.get("ParentIndexNumber")}:{item.get("IndexNumber")} - {item.get("Name")}'
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
if item.get("BackdropImageTags"):
|
||||
image = self.__get_backdrop_url(item_id=item.get("Id"),
|
||||
image_tag=item.get("BackdropImageTags")[0])
|
||||
else:
|
||||
image = self.__get_local_image_by_id(item.get("Id"))
|
||||
else:
|
||||
image = self.__get_backdrop_url(item_id=item.get("SeriesId"),
|
||||
image_tag=item.get("SeriesPrimaryImageTag"))
|
||||
if not image:
|
||||
image = self.__get_local_image_by_id(item.get("SeriesId"))
|
||||
ret_resume.append(schemas.MediaServerPlayItem(
|
||||
id=item.get("Id"),
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
type=item_type,
|
||||
image=image,
|
||||
link=link,
|
||||
percent=item.get("UserData", {}).get("PlayedPercentage")
|
||||
))
|
||||
return ret_resume
|
||||
else:
|
||||
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Resume出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_latest(self, num: int = 20, username: str = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获得最近更新
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
req_url = (f"{self._host}Users/{user}/Items/Latest?"
|
||||
f"Limit=100&MediaTypes=Video&api_key={self._apikey}&Fields=ProductionYear,Path")
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
result = res.json() or []
|
||||
ret_latest = []
|
||||
# 用户媒体库文件夹列表(排除黑名单)
|
||||
library_folders = self.get_user_library_folders()
|
||||
for item in result:
|
||||
if len(ret_latest) == num:
|
||||
break
|
||||
if item.get("Type") not in ["Movie", "Series"]:
|
||||
continue
|
||||
item_path = item.get("Path")
|
||||
if item_path and library_folders and not any(
|
||||
str(item_path).startswith(folder) for folder in library_folders):
|
||||
continue
|
||||
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value
|
||||
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"),
|
||||
title=item.get("Name"),
|
||||
subtitle=item.get("ProductionYear"),
|
||||
type=item_type,
|
||||
image=image,
|
||||
link=link
|
||||
))
|
||||
return ret_latest
|
||||
else:
|
||||
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_user_library_folders(self):
|
||||
"""
|
||||
获取Emby媒体库文件夹列表(排除黑名单)
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
library_folders = []
|
||||
black_list = (settings.MEDIASERVER_SYNC_BLACKLIST or '').split(",")
|
||||
for library in self.get_emby_folders() or []:
|
||||
if library.get("Name") in black_list:
|
||||
continue
|
||||
library_folders += [folder.get("Path") for folder in library.get("SubFolders")]
|
||||
return library_folders
|
||||
|
||||
@@ -326,6 +326,8 @@ class FanartModule(_ModuleBase):
|
||||
:param mediainfo: 识别的媒体信息
|
||||
:return: 更新后的媒体信息
|
||||
"""
|
||||
if not settings.FANART_ENABLE:
|
||||
return None
|
||||
if not mediainfo.tmdb_id and not mediainfo.tvdb_id:
|
||||
return None
|
||||
if mediainfo.type == MediaType.MOVIE:
|
||||
|
||||
@@ -43,7 +43,7 @@ class IndexerModule(_ModuleBase):
|
||||
# 确认搜索的名字
|
||||
if not keywords:
|
||||
# 浏览种子页
|
||||
keywords = [None]
|
||||
keywords = ['']
|
||||
|
||||
# 开始索引
|
||||
result_array = []
|
||||
|
||||
@@ -12,9 +12,9 @@ from ruamel.yaml import CommentedMap
|
||||
from app.core.config import settings
|
||||
from app.helper.browser import PlaywrightHelper
|
||||
from app.log import logger
|
||||
from app.schemas.types import MediaType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.string import StringUtils
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
class TorrentSpider:
|
||||
@@ -547,6 +547,29 @@ class TorrentSpider:
|
||||
else:
|
||||
self.torrents_info['labels'] = []
|
||||
|
||||
def __get_free_date(self, torrent):
|
||||
# free date
|
||||
if 'freedate' not in self.fields:
|
||||
return
|
||||
selector = self.fields.get('freedate', {})
|
||||
freedate = torrent(selector.get('selector', '')).clone()
|
||||
self.__remove(freedate, selector)
|
||||
items = self.__attribute_or_text(freedate, selector)
|
||||
self.torrents_info['freedate'] = self.__index(items, selector)
|
||||
self.torrents_info['freedate'] = self.__filter_text(self.torrents_info.get('freedate'),
|
||||
selector.get('filters'))
|
||||
|
||||
def __get_hit_and_run(self, torrent):
|
||||
# hitandrun
|
||||
if 'hr' not in self.fields:
|
||||
return
|
||||
selector = self.fields.get('hr', {})
|
||||
hit_and_run = torrent(selector.get('selector', ''))
|
||||
if hit_and_run:
|
||||
self.torrents_info['hit_and_run'] = True
|
||||
else:
|
||||
self.torrents_info['hit_and_run'] = False
|
||||
|
||||
def get_info(self, torrent) -> dict:
|
||||
"""
|
||||
解析单条种子数据
|
||||
@@ -566,13 +589,15 @@ class TorrentSpider:
|
||||
self.__get_uploadvolumefactor(torrent)
|
||||
self.__get_pubdate(torrent)
|
||||
self.__get_date_elapsed(torrent)
|
||||
self.__get_free_date(torrent)
|
||||
self.__get_labels(torrent)
|
||||
self.__get_hit_and_run(torrent)
|
||||
except Exception as err:
|
||||
logger.error("%s 搜索出现错误:%s" % (self.indexername, str(err)))
|
||||
return self.torrents_info
|
||||
|
||||
@staticmethod
|
||||
def __filter_text(text, filters):
|
||||
def __filter_text(text: str, filters: list):
|
||||
"""
|
||||
对文件进行处理
|
||||
"""
|
||||
@@ -613,7 +638,7 @@ class TorrentSpider:
|
||||
item.remove(v)
|
||||
|
||||
@staticmethod
|
||||
def __attribute_or_text(item, selector):
|
||||
def __attribute_or_text(item, selector: dict):
|
||||
if not selector:
|
||||
return item
|
||||
if not item:
|
||||
@@ -625,7 +650,7 @@ class TorrentSpider:
|
||||
return items
|
||||
|
||||
@staticmethod
|
||||
def __index(items, selector):
|
||||
def __index(items: list, selector: dict):
|
||||
if not items:
|
||||
return None
|
||||
if selector:
|
||||
|
||||
@@ -101,13 +101,13 @@ class JellyfinModule(_ModuleBase):
|
||||
media_statistic.user_count = self.jellyfin.get_user_count()
|
||||
return [media_statistic]
|
||||
|
||||
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
def mediaserver_librarys(self, server: str = None, username: str = None) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
if server != "jellyfin":
|
||||
if server and server != "jellyfin":
|
||||
return None
|
||||
return self.jellyfin.get_librarys()
|
||||
return self.jellyfin.get_librarys(username)
|
||||
|
||||
def mediaserver_items(self, server: str, library_id: str) -> Optional[Generator]:
|
||||
"""
|
||||
@@ -139,3 +139,29 @@ class JellyfinModule(_ModuleBase):
|
||||
season=season,
|
||||
episodes=episodes
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, count: int = 20,
|
||||
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
if server and server != "jellyfin":
|
||||
return []
|
||||
return self.jellyfin.get_resume(num=count, username=username)
|
||||
|
||||
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, count: int = 20,
|
||||
server: str = None, username: str = None) -> List[schemas.MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
if server and server != "jellyfin":
|
||||
return []
|
||||
return self.jellyfin.get_latest(num=count, username=username)
|
||||
|
||||
@@ -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()
|
||||
@@ -39,13 +45,35 @@ class Jellyfin(metaclass=Singleton):
|
||||
self.user = self.get_user()
|
||||
self.serverid = self.get_server_id()
|
||||
|
||||
def __get_jellyfin_librarys(self) -> List[dict]:
|
||||
def get_jellyfin_folders(self) -> List[dict]:
|
||||
"""
|
||||
获取Jellyfin媒体库路径列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
req_url = "%Library/SelectableMediaFolders?api_key=%s" % (self._host, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
return res.json()
|
||||
else:
|
||||
logger.error(f"Library/SelectableMediaFolders 未获取到返回数据")
|
||||
return []
|
||||
except Exception as e:
|
||||
logger.error(f"连接Library/SelectableMediaFolders 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def __get_jellyfin_librarys(self, username: str = None) -> List[dict]:
|
||||
"""
|
||||
获取Jellyfin媒体库的信息
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
req_url = f"{self._host}Users/{self.user}/Views?api_key={self._apikey}"
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
req_url = f"{self._host}Users/{user}/Views?api_key={self._apikey}"
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
@@ -57,14 +85,17 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接Users/Views 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_librarys(self):
|
||||
def get_librarys(self, username: str = None) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
libraries = []
|
||||
for library in self.__get_jellyfin_librarys() or []:
|
||||
black_list = (settings.MEDIASERVER_SYNC_BLACKLIST or '').split(",")
|
||||
for library in self.__get_jellyfin_librarys(username) or []:
|
||||
if library.get("Name") in black_list:
|
||||
continue
|
||||
match library.get("CollectionType"):
|
||||
case "movies":
|
||||
library_type = MediaType.MOVIE.value
|
||||
@@ -72,13 +103,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
|
||||
|
||||
@@ -587,3 +626,154 @@ class Jellyfin(metaclass=Singleton):
|
||||
except Exception as e:
|
||||
logger.error(f"连接Jellyfin出错:" + str(e))
|
||||
return None
|
||||
|
||||
def get_play_url(self, item_id: str) -> str:
|
||||
"""
|
||||
拼装媒体播放链接
|
||||
:param item_id: 媒体的的ID
|
||||
"""
|
||||
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:
|
||||
"""
|
||||
根据ItemId从媒体服务器查询有声书图片地址
|
||||
:param: item_id: 在Emby中的ID
|
||||
:param: remote 是否远程使用,TG微信等客户端调用应为True
|
||||
:param: inner 是否NT内部调用,为True是会使用NT中转
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return ""
|
||||
return "%sItems/%s/Images/Primary" % (self._host, item_id)
|
||||
|
||||
def __get_backdrop_url(self, item_id: str, image_tag: str) -> str:
|
||||
"""
|
||||
获取Backdrop图片地址
|
||||
:param: item_id: 在Emby中的ID
|
||||
:param: image_tag: 图片的tag
|
||||
:param: remote 是否远程使用,TG微信等客户端调用应为True
|
||||
:param: inner 是否NT内部调用,为True是会使用NT中转
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return ""
|
||||
if not image_tag or not item_id:
|
||||
return ""
|
||||
return f"{self._host}Items/{item_id}/" \
|
||||
f"Images/Backdrop?tag={image_tag}&fillWidth=666&api_key={self._apikey}"
|
||||
|
||||
def get_resume(self, num: int = 12, username: str = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获得继续观看
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
req_url = (f"{self._host}Users/{user}/Items/Resume?"
|
||||
f"Limit=100&MediaTypes=Video&api_key={self._apikey}&Fields=ProductionYear,Path")
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
result = res.json().get("Items") or []
|
||||
ret_resume = []
|
||||
# 用户媒体库文件夹列表(排除黑名单)
|
||||
library_folders = self.get_user_library_folders()
|
||||
for item in result:
|
||||
if len(ret_resume) == num:
|
||||
break
|
||||
if item.get("Type") not in ["Movie", "Episode"]:
|
||||
continue
|
||||
item_path = item.get("Path")
|
||||
if item_path and library_folders and not any(
|
||||
str(item_path).startswith(folder) for folder in library_folders):
|
||||
continue
|
||||
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value
|
||||
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])
|
||||
else:
|
||||
image = self.__get_local_image_by_id(item.get("Id"))
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
title = item.get("Name")
|
||||
subtitle = item.get("ProductionYear")
|
||||
else:
|
||||
title = f'{item.get("SeriesName")}'
|
||||
subtitle = f'S{item.get("ParentIndexNumber")}:{item.get("IndexNumber")} - {item.get("Name")}'
|
||||
ret_resume.append(schemas.MediaServerPlayItem(
|
||||
id=item.get("Id"),
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
type=item_type,
|
||||
image=image,
|
||||
link=link,
|
||||
percent=item.get("UserData", {}).get("PlayedPercentage")
|
||||
))
|
||||
return ret_resume
|
||||
else:
|
||||
logger.error(f"Users/Items/Resume 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Resume出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_latest(self, num=20, username: str = None) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获得最近更新
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
if username:
|
||||
user = self.get_user(username)
|
||||
else:
|
||||
user = self.user
|
||||
req_url = (f"{self._host}Users/{user}/Items/Latest?"
|
||||
f"Limit=100&MediaTypes=Video&api_key={self._apikey}&Fields=ProductionYear,Path")
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res:
|
||||
result = res.json() or []
|
||||
ret_latest = []
|
||||
# 用户媒体库文件夹列表(排除黑名单)
|
||||
library_folders = self.get_user_library_folders()
|
||||
for item in result:
|
||||
if len(ret_latest) == num:
|
||||
break
|
||||
if item.get("Type") not in ["Movie", "Series"]:
|
||||
continue
|
||||
item_path = item.get("Path")
|
||||
if item_path and library_folders and not any(
|
||||
str(item_path).startswith(folder) for folder in library_folders):
|
||||
continue
|
||||
item_type = MediaType.MOVIE.value if item.get("Type") == "Movie" else MediaType.TV.value
|
||||
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"),
|
||||
title=item.get("Name"),
|
||||
subtitle=item.get("ProductionYear"),
|
||||
type=item_type,
|
||||
image=image,
|
||||
link=link
|
||||
))
|
||||
return ret_latest
|
||||
else:
|
||||
logger.error(f"Users/Items/Latest 未获取到返回数据")
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items/Latest出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_user_library_folders(self):
|
||||
"""
|
||||
获取Emby媒体库文件夹列表(排除黑名单)
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
library_folders = []
|
||||
black_list = (settings.MEDIASERVER_SYNC_BLACKLIST or '').split(",")
|
||||
for library in self.get_jellyfin_folders() or []:
|
||||
if library.get("Name") in black_list:
|
||||
continue
|
||||
library_folders += [folder.get("Path") for folder in library.get("SubFolders")]
|
||||
return library_folders
|
||||
|
||||
@@ -95,11 +95,11 @@ class PlexModule(_ModuleBase):
|
||||
media_statistic.user_count = 1
|
||||
return [media_statistic]
|
||||
|
||||
def mediaserver_librarys(self, server: str) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
def mediaserver_librarys(self, server: str = None, **kwargs) -> Optional[List[schemas.MediaServerLibrary]]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
if server != "plex":
|
||||
if server and server != "plex":
|
||||
return None
|
||||
return self.plex.get_librarys()
|
||||
|
||||
@@ -133,3 +133,27 @@ class PlexModule(_ModuleBase):
|
||||
season=season,
|
||||
episodes=episodes
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
||||
def mediaserver_playing(self, count: int = 20, server: str = None, **kwargs) -> List[schemas.MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器正在播放信息
|
||||
"""
|
||||
if server and server != "plex":
|
||||
return []
|
||||
return self.plex.get_resume(count)
|
||||
|
||||
def mediaserver_latest(self, count: int = 20, server: str = None, **kwargs) -> List[schemas.MediaServerPlayItem]:
|
||||
"""
|
||||
获取媒体服务器最新入库条目
|
||||
"""
|
||||
if server and 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
|
||||
@@ -15,6 +16,8 @@ from app.utils.singleton import Singleton
|
||||
|
||||
class Plex(metaclass=Singleton):
|
||||
|
||||
_plex = None
|
||||
|
||||
def __init__(self):
|
||||
self._host = settings.PLEX_HOST
|
||||
if self._host:
|
||||
@@ -22,6 +25,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 +59,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, mtype: int) -> 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={mtype}§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]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
@@ -62,12 +108,17 @@ class Plex(metaclass=Singleton):
|
||||
logger.error(f"获取媒体服务器所有媒体库列表出错:{str(err)}")
|
||||
return []
|
||||
libraries = []
|
||||
black_list = (settings.MEDIASERVER_SYNC_BLACKLIST or '').split(",")
|
||||
for library in self._libraries:
|
||||
if library.title in black_list:
|
||||
continue
|
||||
match library.type:
|
||||
case "movie":
|
||||
library_type = MediaType.MOVIE.value
|
||||
image_list = self.__get_library_images(library.key, 1)
|
||||
case "show":
|
||||
library_type = MediaType.TV.value
|
||||
image_list = self.__get_library_images(library.key, 2)
|
||||
case _:
|
||||
continue
|
||||
libraries.append(
|
||||
@@ -75,7 +126,10 @@ class Plex(metaclass=Singleton):
|
||||
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}web/index.html#!/media/{self._plex.machineIdentifier}"
|
||||
f"/com.plexapp.plugins.library?source={library.key}"
|
||||
)
|
||||
)
|
||||
return libraries
|
||||
@@ -543,3 +597,63 @@ class Plex(metaclass=Singleton):
|
||||
获取plex对象,以便直接操作
|
||||
"""
|
||||
return self._plex
|
||||
|
||||
def get_play_url(self, item_id: str) -> str:
|
||||
"""
|
||||
拼装媒体播放链接
|
||||
:param item_id: 媒体的的ID
|
||||
"""
|
||||
return f'{self._playhost or self._host}web/index.html#!/server/{self._plex.machineIdentifier}/details?key={item_id}'
|
||||
|
||||
def get_resume(self, num: int = 12) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取继续观看的媒体
|
||||
"""
|
||||
if not self._plex:
|
||||
return []
|
||||
items = self._plex.fetchItems('/hubs/continueWatching/items', container_start=0, container_size=num)
|
||||
ret_resume = []
|
||||
for item in items:
|
||||
item_type = MediaType.MOVIE.value if item.TYPE == "movie" else MediaType.TV.value
|
||||
if item_type == MediaType.MOVIE.value:
|
||||
title = item.title
|
||||
subtitle = item.year
|
||||
else:
|
||||
title = item.grandparentTitle
|
||||
subtitle = f"S{item.parentIndex}:E{item.index} - {item.title}"
|
||||
link = self.get_play_url(item.key)
|
||||
image = item.artUrl
|
||||
ret_resume.append(schemas.MediaServerPlayItem(
|
||||
id=item.key,
|
||||
title=title,
|
||||
subtitle=subtitle,
|
||||
type=item_type,
|
||||
image=image,
|
||||
link=link,
|
||||
percent=item.viewOffset / item.duration * 100 if item.viewOffset and item.duration else 0
|
||||
))
|
||||
return ret_resume[:num]
|
||||
|
||||
def get_latest(self, num: int = 20) -> Optional[List[schemas.MediaServerPlayItem]]:
|
||||
"""
|
||||
获取最近添加媒体
|
||||
"""
|
||||
if not self._plex:
|
||||
return None
|
||||
items = self._plex.fetchItems('/library/recentlyAdded', container_start=0, container_size=num)
|
||||
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)
|
||||
title = item.title if item_type == MediaType.MOVIE.value else \
|
||||
"%s 第%s季" % (item.parentTitle, item.index)
|
||||
image = item.posterUrl
|
||||
ret_resume.append(schemas.MediaServerPlayItem(
|
||||
id=item.key,
|
||||
title=title,
|
||||
subtitle=item.year,
|
||||
type=item_type,
|
||||
image=image,
|
||||
link=link
|
||||
))
|
||||
return ret_resume[:num]
|
||||
|
||||
@@ -81,7 +81,7 @@ class TmdbScraper:
|
||||
dir_path=file_path.parents[1])
|
||||
# 生成根目录图片
|
||||
for attr_name, attr_value in vars(mediainfo).items():
|
||||
if attr_value \
|
||||
if attr_name \
|
||||
and attr_name.endswith("_path") \
|
||||
and not attr_name.startswith("season") \
|
||||
and attr_value \
|
||||
@@ -135,7 +135,8 @@ class TmdbScraper:
|
||||
file_path=file_path)
|
||||
# 集的图片
|
||||
episode_image = episodeinfo.get("still_path")
|
||||
image_path = file_path.with_name(file_path.stem + "-thumb").with_suffix(Path(episode_image).suffix)
|
||||
image_path = file_path.with_name(file_path.stem + "-thumb.jpg").with_suffix(
|
||||
Path(episode_image).suffix)
|
||||
if episode_image and (self._force_img or not image_path.exists()):
|
||||
self.__save_image(
|
||||
f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/original{episode_image}",
|
||||
|
||||
@@ -57,7 +57,7 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def get_command() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件命令
|
||||
注册插件远程命令
|
||||
[{
|
||||
"cmd": "/xx",
|
||||
"event": EventType.xx,
|
||||
@@ -71,7 +71,7 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
@abstractmethod
|
||||
def get_api(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
获取插件API
|
||||
注册插件API
|
||||
[{
|
||||
"path": "/xx",
|
||||
"endpoint": self.xxx,
|
||||
@@ -82,6 +82,19 @@ class _PluginBase(metaclass=ABCMeta):
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_service(self) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
注册插件公共服务
|
||||
[{
|
||||
"id": "服务ID",
|
||||
"name": "服务名称",
|
||||
"trigger": "触发器:cron/interval/date/CronTrigger.from_crontab()",
|
||||
"func": self.xxx,
|
||||
"kwargs": {} # 定时器参数
|
||||
}]
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_form(self) -> Tuple[List[dict], Dict[str, Any]]:
|
||||
"""
|
||||
|
||||
@@ -16,6 +16,7 @@ from app.chain.tmdb import TmdbChain
|
||||
from app.chain.torrents import TorrentsChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
from app.core.plugin import PluginManager
|
||||
from app.log import logger
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.timer import TimerUtils
|
||||
@@ -227,6 +228,27 @@ class Scheduler(metaclass=Singleton):
|
||||
}
|
||||
)
|
||||
|
||||
# 注册插件公共服务
|
||||
plugin_services = PluginManager().get_plugin_services()
|
||||
for service in plugin_services:
|
||||
try:
|
||||
self._jobs[service["id"]] = {
|
||||
"func": service["func"],
|
||||
"running": False,
|
||||
}
|
||||
self._scheduler.add_job(
|
||||
self.start,
|
||||
service["trigger"],
|
||||
id=service["id"],
|
||||
name=service["name"],
|
||||
**service["kwargs"],
|
||||
kwargs={
|
||||
'job_id': service["id"]
|
||||
}
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"注册插件服务失败:{str(e)} - {service}")
|
||||
|
||||
# 打印服务
|
||||
logger.debug(self._scheduler.print_jobs())
|
||||
|
||||
|
||||
@@ -196,6 +196,8 @@ class TorrentInfo(BaseModel):
|
||||
pubdate: Optional[str] = None
|
||||
# 已过时间
|
||||
date_elapsed: Optional[str] = None
|
||||
# 免费截止时间
|
||||
freedate: Optional[str] = None
|
||||
# 上传因子
|
||||
uploadvolumefactor: Optional[float] = None
|
||||
# 下载因子
|
||||
@@ -208,6 +210,8 @@ class TorrentInfo(BaseModel):
|
||||
pri_order: Optional[int] = 0
|
||||
# 促销
|
||||
volume_factor: Optional[str] = None
|
||||
# 剩余免费时间
|
||||
freedate_diff: Optional[str] = None
|
||||
|
||||
|
||||
class Context(BaseModel):
|
||||
|
||||
@@ -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):
|
||||
@@ -139,3 +143,16 @@ class WebhookEventInfo(BaseModel):
|
||||
save_reason: Optional[str] = None
|
||||
item_isvirtual: Optional[bool] = None
|
||||
media_type: Optional[str] = None
|
||||
|
||||
|
||||
class MediaServerPlayItem(BaseModel):
|
||||
"""
|
||||
媒体服务器可播放项目信息
|
||||
"""
|
||||
id: Optional[Union[str, int]] = None
|
||||
title: Optional[str] = None
|
||||
subtitle: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
image: Optional[str] = None
|
||||
link: Optional[str] = None
|
||||
percent: Optional[float] = None
|
||||
|
||||
@@ -14,3 +14,7 @@ class Token(BaseModel):
|
||||
class TokenPayload(BaseModel):
|
||||
# 用户ID
|
||||
sub: Optional[int] = None
|
||||
# 用户名
|
||||
username: Optional[str] = None
|
||||
# 超级用户
|
||||
super_user: Optional[bool] = None
|
||||
|
||||
@@ -711,3 +711,29 @@ class StringUtils:
|
||||
return -1
|
||||
else:
|
||||
return 0
|
||||
|
||||
@staticmethod
|
||||
def diff_time_str(time_str: str):
|
||||
"""
|
||||
输入YYYY-MM-DD HH24:MI:SS 格式的时间字符串,返回距离现在的剩余时间:xx天xx小时xx分钟
|
||||
"""
|
||||
if not time_str:
|
||||
return ''
|
||||
try:
|
||||
time_obj = datetime.datetime.strptime(time_str, '%Y-%m-%d %H:%M:%S')
|
||||
except ValueError:
|
||||
return ''
|
||||
now = datetime.datetime.now()
|
||||
diff = time_obj - now
|
||||
diff_seconds = diff.seconds
|
||||
diff_days = diff.days
|
||||
diff_hours = diff_seconds // 3600
|
||||
diff_minutes = (diff_seconds % 3600) // 60
|
||||
if diff_days > 0:
|
||||
return f'{diff_days}天{diff_hours}小时{diff_minutes}分钟'
|
||||
elif diff_hours > 0:
|
||||
return f'{diff_hours}小时{diff_minutes}分钟'
|
||||
elif diff_minutes > 0:
|
||||
return f'{diff_minutes}分钟'
|
||||
else:
|
||||
return ''
|
||||
|
||||
@@ -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=
|
||||
|
||||
@@ -113,6 +119,8 @@ TMDB_IMAGE_DOMAIN=image.tmdb.org
|
||||
TMDB_API_DOMAIN=api.themoviedb.org
|
||||
# 媒体识别来源 themoviedb/douban,使用themoviedb时需要确保能正常连接api.themoviedb.org,使用douban时不支持二级分类
|
||||
RECOGNIZE_SOURCE=themoviedb
|
||||
# Fanart开关
|
||||
FANART_ENABLE=true
|
||||
|
||||
# 【*】消息通知渠道 telegram/wechat/slack,多个通知渠道用,分隔,需要在上面配置对应消息通知渠道的参数
|
||||
MESSAGER=telegram
|
||||
|
||||
@@ -1 +1 @@
|
||||
APP_VERSION = 'v1.5.5-1'
|
||||
APP_VERSION = 'v1.5.9-1'
|
||||
|
||||
Reference in New Issue
Block a user