feat 本地存在标识&媒体库同步
This commit is contained in:
parent
ce05b25f87
commit
d93bc31092
@ -106,6 +106,8 @@ docker pull jxxghp/moviepilot:latest
|
||||
|
||||
**MEDIASERVER:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||
|
||||
**MEDIASERVER_SYNC_INTERVAL:** 媒体服务器同步间隔(小时),默认`6`,留空则不同步
|
||||
|
||||
`emby`设置项:
|
||||
|
||||
- **EMBY_HOST:** Emby服务器地址,格式:`ip:port`,https需要添加`https://`前缀
|
||||
@ -218,10 +220,10 @@ docker pull jxxghp/moviepilot:latest
|
||||
- [x] 自定义识别词
|
||||
- [x] 便捷工具
|
||||
- [x] 过滤规则维护
|
||||
- [x] 本地存在标识
|
||||
- [ ] 插件管理
|
||||
- [ ] 手动整理功能增强
|
||||
- [ ] 本地存在标识
|
||||
- [ ] 媒体详情页面
|
||||
- [ ] 洗版支持
|
||||
- [ ] 插件管理
|
||||
|
||||
|
||||
|
@ -1,10 +1,13 @@
|
||||
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
|
||||
from app.core.security import verify_token
|
||||
from app.db import get_db
|
||||
from app.db.mediaserver_oper import MediaServerOper
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
@ -35,3 +38,22 @@ def search_by_title(title: str,
|
||||
if medias:
|
||||
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
||||
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:
|
||||
"""
|
||||
判断本地是否存在
|
||||
"""
|
||||
exist = MediaServerOper(db).exists(
|
||||
title=title, year=year, mtype=mtype, tmdbid=tmdbid, season=season
|
||||
)
|
||||
return schemas.Response(success=True if exist else False, data={
|
||||
"item": exist or {}
|
||||
})
|
||||
|
@ -1,5 +1,4 @@
|
||||
import json
|
||||
import json
|
||||
import time
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
@ -12,7 +12,7 @@ from app.core.context import MediaInfo
|
||||
from app.core.metainfo import MetaInfo
|
||||
from app.helper.rss import RssHelper
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType, Notification, MessageChannel, NotificationType
|
||||
from app.schemas import MediaType, Notification, MessageChannel
|
||||
|
||||
|
||||
class DoubanChain(ChainBase):
|
||||
@ -95,10 +95,10 @@ class DoubanChain(ChainBase):
|
||||
"""
|
||||
同步豆瓣想看数据,发送消息
|
||||
"""
|
||||
self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe,
|
||||
self.post_message(Notification(channel=channel,
|
||||
title="开始同步豆瓣想看 ...", userid=userid))
|
||||
self.sync()
|
||||
self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe,
|
||||
self.post_message(Notification(channel=channel,
|
||||
title="同步豆瓣想看数据完成!", userid=userid))
|
||||
|
||||
def sync(self):
|
||||
|
88
app/chain/mediaserver.py
Normal file
88
app/chain/mediaserver.py
Normal file
@ -0,0 +1,88 @@
|
||||
import json
|
||||
import threading
|
||||
from typing import List, Union, Generator
|
||||
|
||||
from app import schemas
|
||||
from app.chain import ChainBase
|
||||
from app.core.config import settings
|
||||
from app.db.mediaserver_oper import MediaServerOper
|
||||
from app.log import logger
|
||||
from app.schemas import MessageChannel, Notification
|
||||
|
||||
lock = threading.Lock()
|
||||
|
||||
|
||||
class MediaServerChain(ChainBase):
|
||||
"""
|
||||
媒体服务器处理链
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.mediaserverdb = MediaServerOper()
|
||||
|
||||
def librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
获取媒体服务器所有媒体库
|
||||
"""
|
||||
return self.run_module("mediaserver_librarys")
|
||||
|
||||
def items(self, library_id: Union[str, int]) -> Generator:
|
||||
"""
|
||||
获取媒体服务器所有项目
|
||||
"""
|
||||
return self.run_module("mediaserver_items", library_id=library_id)
|
||||
|
||||
def episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||
"""
|
||||
获取媒体服务器剧集信息
|
||||
"""
|
||||
return self.run_module("mediaserver_tv_episodes", item_id=item_id)
|
||||
|
||||
def remote_sync(self, channel: MessageChannel, userid: Union[int, str]):
|
||||
"""
|
||||
同步豆瓣想看数据,发送消息
|
||||
"""
|
||||
self.post_message(Notification(channel=channel,
|
||||
title="开始媒体服务器 ...", userid=userid))
|
||||
self.sync()
|
||||
self.post_message(Notification(channel=channel,
|
||||
title="同步媒体服务器完成!", userid=userid))
|
||||
|
||||
def sync(self):
|
||||
"""
|
||||
同步媒体库所有数据到本地数据库
|
||||
"""
|
||||
with lock:
|
||||
logger.info("开始同步媒体库数据 ...")
|
||||
# 汇总统计
|
||||
total_count = 0
|
||||
# 清空登记薄
|
||||
self.mediaserverdb.empty(server=settings.MEDIASERVER)
|
||||
for library in self.librarys():
|
||||
logger.info(f"正在同步媒体库 {library.name} ...")
|
||||
library_count = 0
|
||||
for item in self.items(library.id):
|
||||
if not item:
|
||||
continue
|
||||
if not item.item_id:
|
||||
continue
|
||||
# 计数
|
||||
library_count += 1
|
||||
seasoninfo = {}
|
||||
# 类型
|
||||
item_type = "电视剧" if item.item_type in ['Series', 'show'] else "电影"
|
||||
if item_type == "电视剧":
|
||||
# 查询剧集信息
|
||||
espisodes_info = self.episodes(item.item_id)
|
||||
for episode in espisodes_info:
|
||||
seasoninfo[episode.season] = episode.episodes
|
||||
# 插入数据
|
||||
item_dict = item.dict()
|
||||
item_dict['seasoninfo'] = json.dumps(seasoninfo)
|
||||
item_dict['item_type'] = item_type
|
||||
self.mediaserverdb.add(**item_dict)
|
||||
logger.info(f"媒体库 {library.name} 同步完成,共同步数量:{library_count}")
|
||||
# 总数累加
|
||||
total_count += library_count
|
||||
logger.info("【MediaServer】媒体库数据同步完成,同步数量:%s" % total_count)
|
@ -123,7 +123,8 @@ class SiteChain(ChainBase):
|
||||
if not site:
|
||||
self.post_message(Notification(
|
||||
channel=channel,
|
||||
title=f"站点编号 {site_id} 不存在!", userid=userid))
|
||||
title=f"站点编号 {site_id} 不存在!",
|
||||
userid=userid))
|
||||
return
|
||||
# 禁用站点
|
||||
self.siteoper.update(site_id, {
|
||||
|
@ -6,6 +6,7 @@ from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.download import DownloadChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.site import SiteChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.transfer import TransferChain
|
||||
@ -74,6 +75,11 @@ class Command(metaclass=Singleton):
|
||||
"description": "同步豆瓣想看",
|
||||
"data": {}
|
||||
},
|
||||
"/mediaserver_sync": {
|
||||
"func": MediaServerChain().remote_sync,
|
||||
"description": "同步媒体服务器",
|
||||
"data": {}
|
||||
},
|
||||
"/subscribes": {
|
||||
"func": SubscribeChain().remote_list,
|
||||
"description": "查询订阅",
|
||||
|
@ -113,6 +113,8 @@ class Settings(BaseSettings):
|
||||
DOWNLOAD_CATEGORY: bool = False
|
||||
# 媒体服务器 emby/jellyfin/plex
|
||||
MEDIASERVER: str = "emby"
|
||||
# 媒体服务器同步间隔(小时)
|
||||
MEDIASERVER_SYNC_INTERVAL: int = 6
|
||||
# EMBY服务器地址,IP:PORT
|
||||
EMBY_HOST: str = None
|
||||
# EMBY Api Key
|
||||
|
54
app/db/mediaserver_oper.py
Normal file
54
app/db/mediaserver_oper.py
Normal file
@ -0,0 +1,54 @@
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from app.db import DbOper, SessionLocal
|
||||
from app.db.models.mediaserver import MediaServerItem
|
||||
|
||||
|
||||
class MediaServerOper(DbOper):
|
||||
"""
|
||||
媒体服务器数据管理
|
||||
"""
|
||||
|
||||
def __init__(self, db=SessionLocal()):
|
||||
super().__init__(db)
|
||||
|
||||
def add(self, **kwargs) -> bool:
|
||||
"""
|
||||
新增媒体服务器数据
|
||||
"""
|
||||
item = MediaServerItem(**kwargs)
|
||||
if not item.get_by_itemid(self._db, kwargs.get("item_id")):
|
||||
item.create(self._db)
|
||||
return True
|
||||
return False
|
||||
|
||||
def empty(self, server: str):
|
||||
"""
|
||||
清空媒体服务器数据
|
||||
"""
|
||||
MediaServerItem.empty(self._db, server)
|
||||
|
||||
def exists(self, **kwargs) -> Optional[MediaServerItem]:
|
||||
"""
|
||||
判断媒体服务器数据是否存在
|
||||
"""
|
||||
if kwargs.get("tmdbid"):
|
||||
# 优先按TMDBID查
|
||||
item = MediaServerItem.exist_by_tmdbid(self._db, tmdbid=kwargs.get("tmdbid"),
|
||||
mtype=kwargs.get("mtype"))
|
||||
else:
|
||||
# 按标题、类型、年份查
|
||||
item = MediaServerItem.exists_by_title(self._db, title=kwargs.get("title"),
|
||||
mtype=kwargs.get("mtype"), year=kwargs.get("year"))
|
||||
if not item:
|
||||
return None
|
||||
|
||||
if kwargs.get("season"):
|
||||
# 判断季是否存在
|
||||
if not item.seasoninfo:
|
||||
return None
|
||||
seasoninfo = json.loads(item.seasoninfo) or {}
|
||||
if kwargs.get("season") not in seasoninfo.keys():
|
||||
return None
|
||||
return item
|
61
app/db/models/mediaserver.py
Normal file
61
app/db/models/mediaserver.py
Normal file
@ -0,0 +1,61 @@
|
||||
from datetime import datetime
|
||||
|
||||
from sqlalchemy import Column, Integer, String, Sequence
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.db.models import Base
|
||||
|
||||
|
||||
class MediaServerItem(Base):
|
||||
"""
|
||||
站点表
|
||||
"""
|
||||
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
|
||||
# 服务器类型
|
||||
server = Column(String)
|
||||
# 媒体库ID
|
||||
library = Column(String)
|
||||
# ID
|
||||
item_id = Column(String, index=True)
|
||||
# 类型
|
||||
item_type = Column(String)
|
||||
# 标题
|
||||
title = Column(String, index=True)
|
||||
# 原标题
|
||||
original_title = Column(String)
|
||||
# 年份
|
||||
year = Column(String)
|
||||
# TMDBID
|
||||
tmdbid = Column(Integer, index=True)
|
||||
# IMDBID
|
||||
imdbid = Column(String, index=True)
|
||||
# TVDBID
|
||||
tvdbid = Column(String, index=True)
|
||||
# 路径
|
||||
path = Column(String)
|
||||
# 季集
|
||||
seasoninfo = Column(String)
|
||||
# 备注
|
||||
note = Column(String)
|
||||
# 同步时间
|
||||
lst_mod_date = Column(String, default=datetime.now().strftime("%Y-%m-%d %H:%M:%S"))
|
||||
|
||||
@staticmethod
|
||||
def get_by_itemid(db: Session, item_id: str):
|
||||
return db.query(MediaServerItem).filter(MediaServerItem.item_id == item_id).first()
|
||||
|
||||
@staticmethod
|
||||
def empty(db: Session, server: str):
|
||||
db.query(MediaServerItem).filter(MediaServerItem.server == server).delete()
|
||||
db.commit()
|
||||
|
||||
@staticmethod
|
||||
def exist_by_tmdbid(db: Session, tmdbid: int, mtype: str):
|
||||
return db.query(MediaServerItem).filter(MediaServerItem.tmdbid == tmdbid,
|
||||
MediaServerItem.item_type == mtype).first()
|
||||
|
||||
@staticmethod
|
||||
def exists_by_title(db: Session, title: str, mtype: str, year: str):
|
||||
return db.query(MediaServerItem).filter(MediaServerItem.title == title,
|
||||
MediaServerItem.item_type == mtype,
|
||||
MediaServerItem.year == str(year)).first()
|
@ -181,4 +181,4 @@ class DoubanScraper:
|
||||
return
|
||||
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
|
||||
file_path.write_bytes(xml_str)
|
||||
logger.info(f"NFO文件已保存:{file_path}")
|
||||
logger.info(f"NFO文件已保存:{file_path}")
|
||||
|
@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union, Any
|
||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
@ -97,3 +97,50 @@ class EmbyModule(_ModuleBase):
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
user_count=user_count or 0
|
||||
)
|
||||
|
||||
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
librarys = self.emby.get_librarys()
|
||||
if not librarys:
|
||||
return []
|
||||
return [schemas.MediaServerLibrary(
|
||||
server="emby",
|
||||
id=library.get("id"),
|
||||
name=library.get("name"),
|
||||
type=library.get("type"),
|
||||
path=library.get("path")
|
||||
) for library in librarys]
|
||||
|
||||
def mediaserver_items(self, library_id: str) -> Generator:
|
||||
"""
|
||||
媒体库项目列表
|
||||
"""
|
||||
items = self.emby.get_items(library_id)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
server="emby",
|
||||
library=item.get("library"),
|
||||
item_id=item.get("id"),
|
||||
item_type=item.get("type"),
|
||||
title=item.get("title"),
|
||||
original_title=item.get("original_title"),
|
||||
year=item.get("year"),
|
||||
tmdbid=item.get("tmdbid"),
|
||||
imdbid=item.get("imdbid"),
|
||||
tvdbid=item.get("tvdbid"),
|
||||
path=item.get("path"),
|
||||
)
|
||||
|
||||
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||
"""
|
||||
获取剧集信息
|
||||
"""
|
||||
seasoninfo = self.emby.get_tv_episodes(item_id=item_id)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
return [schemas.MediaServerSeasonInfo(
|
||||
season=season,
|
||||
episodes=episodes
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
@ -1,7 +1,7 @@
|
||||
import json
|
||||
import re
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Union, Dict
|
||||
from typing import List, Optional, Union, Dict, Generator
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
@ -43,7 +43,7 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接Library/SelectableMediaFolders 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_emby_librarys(self) -> List[dict]:
|
||||
def __get_emby_librarys(self) -> List[dict]:
|
||||
"""
|
||||
获取Emby媒体库列表
|
||||
"""
|
||||
@ -61,6 +61,29 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接User/Views 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_librarys(self):
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
libraries = []
|
||||
for library in self.__get_emby_librarys() or []:
|
||||
match library.get("CollectionType"):
|
||||
case "movies":
|
||||
library_type = MediaType.MOVIE.value
|
||||
case "tvshows":
|
||||
library_type = MediaType.TV.value
|
||||
case _:
|
||||
continue
|
||||
libraries.append({
|
||||
"id": library.get("Id"),
|
||||
"name": library.get("Name"),
|
||||
"path": library.get("Path"),
|
||||
"type": library_type
|
||||
})
|
||||
return libraries
|
||||
|
||||
def get_user(self, user_name: str = None) -> Optional[Union[str, int]]:
|
||||
"""
|
||||
获得管理员用户
|
||||
@ -269,12 +292,14 @@ class Emby(metaclass=Singleton):
|
||||
return []
|
||||
|
||||
def get_tv_episodes(self,
|
||||
item_id: str = None,
|
||||
title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None,
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
"""
|
||||
根据标题和年份和季,返回Emby中的剧集列表
|
||||
:param item_id: Emby中的ID
|
||||
:param title: 标题
|
||||
:param year: 年份
|
||||
:param tmdb_id: TMDBID
|
||||
@ -284,16 +309,17 @@ class Emby(metaclass=Singleton):
|
||||
if not self._host or not self._apikey:
|
||||
return None
|
||||
# 电视剧
|
||||
item_id = self.__get_emby_series_id_by_name(title, year)
|
||||
if item_id is None:
|
||||
return None
|
||||
if not item_id:
|
||||
return {}
|
||||
# 验证tmdbid是否相同
|
||||
item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
item_id = self.__get_emby_series_id_by_name(title, year)
|
||||
if item_id is None:
|
||||
return None
|
||||
if not item_id:
|
||||
return {}
|
||||
# 验证tmdbid是否相同
|
||||
item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
return {}
|
||||
# /Shows/Id/Episodes 查集的信息
|
||||
if not season:
|
||||
season = ""
|
||||
@ -475,6 +501,42 @@ class Emby(metaclass=Singleton):
|
||||
logger.error(f"连接Items/Id出错:" + str(e))
|
||||
return {}
|
||||
|
||||
def get_items(self, parent: str) -> Generator:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not parent:
|
||||
yield {}
|
||||
if not self._host or not self._apikey:
|
||||
yield {}
|
||||
req_url = "%semby/Users/%s/Items?ParentId=%s&api_key=%s" % (self._host, self._user, parent, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res and res.status_code == 200:
|
||||
results = res.json().get("Items") or []
|
||||
for result in results:
|
||||
if not result:
|
||||
continue
|
||||
if result.get("Type") in ["Movie", "Series"]:
|
||||
item_info = self.get_iteminfo(result.get("Id"))
|
||||
yield {"id": result.get("Id"),
|
||||
"library": item_info.get("ParentId"),
|
||||
"type": item_info.get("Type"),
|
||||
"title": item_info.get("Name"),
|
||||
"original_title": item_info.get("OriginalTitle"),
|
||||
"year": item_info.get("ProductionYear"),
|
||||
"tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"),
|
||||
"imdbid": item_info.get("ProviderIds", {}).get("Imdb"),
|
||||
"tvdbid": item_info.get("ProviderIds", {}).get("Tvdb"),
|
||||
"path": item_info.get("Path"),
|
||||
"json": str(item_info)}
|
||||
elif "Folder" in result.get("Type"):
|
||||
for item in self.get_items(parent=result.get('Id')):
|
||||
yield item
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
yield {}
|
||||
|
||||
def get_webhook_message(self, message_str: str) -> dict:
|
||||
"""
|
||||
解析Emby Webhook报文
|
||||
|
@ -1,6 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union, Any
|
||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
@ -89,3 +89,50 @@ class JellyfinModule(_ModuleBase):
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
user_count=user_count or 0
|
||||
)
|
||||
|
||||
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
librarys = self.jellyfin.get_librarys()
|
||||
if not librarys:
|
||||
return []
|
||||
return [schemas.MediaServerLibrary(
|
||||
server="jellyfin",
|
||||
id=library.get("id"),
|
||||
name=library.get("name"),
|
||||
type=library.get("type"),
|
||||
path=library.get("path")
|
||||
) for library in librarys]
|
||||
|
||||
def mediaserver_items(self, library_id: str) -> Generator:
|
||||
"""
|
||||
媒体库项目列表
|
||||
"""
|
||||
items = self.jellyfin.get_items(library_id)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
server="jellyfin",
|
||||
library=item.get("library"),
|
||||
item_id=item.get("id"),
|
||||
item_type=item.get("type"),
|
||||
title=item.get("title"),
|
||||
original_title=item.get("original_title"),
|
||||
year=item.get("year"),
|
||||
tmdbid=item.get("tmdbid"),
|
||||
imdbid=item.get("imdbid"),
|
||||
tvdbid=item.get("tvdbid"),
|
||||
path=item.get("path"),
|
||||
)
|
||||
|
||||
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||
"""
|
||||
获取剧集信息
|
||||
"""
|
||||
seasoninfo = self.jellyfin.get_tv_episodes(item_id=item_id)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
return [schemas.MediaServerSeasonInfo(
|
||||
season=season,
|
||||
episodes=episodes
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
@ -1,9 +1,10 @@
|
||||
import json
|
||||
import re
|
||||
from typing import List, Union, Optional, Dict
|
||||
from typing import List, Union, Optional, Dict, Generator
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas import MediaType
|
||||
from app.utils.http import RequestUtils
|
||||
from app.utils.singleton import Singleton
|
||||
from app.utils.string import StringUtils
|
||||
@ -22,7 +23,7 @@ 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_librarys(self) -> List[dict]:
|
||||
"""
|
||||
获取Jellyfin媒体库的信息
|
||||
"""
|
||||
@ -40,6 +41,29 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接Users/Views 出错:" + str(e))
|
||||
return []
|
||||
|
||||
def get_librarys(self):
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._host or not self._apikey:
|
||||
return []
|
||||
libraries = []
|
||||
for library in self.__get_jellyfin_librarys() or []:
|
||||
match library.get("CollectionType"):
|
||||
case "movies":
|
||||
library_type = MediaType.MOVIE.value
|
||||
case "tvshows":
|
||||
library_type = MediaType.TV.value
|
||||
case _:
|
||||
continue
|
||||
libraries.append({
|
||||
"id": library.get("Id"),
|
||||
"name": library.get("Name"),
|
||||
"path": library.get("Path"),
|
||||
"type": library_type
|
||||
})
|
||||
return libraries
|
||||
|
||||
def get_user_count(self) -> int:
|
||||
"""
|
||||
获得用户数量
|
||||
@ -243,12 +267,14 @@ class Jellyfin(metaclass=Singleton):
|
||||
return []
|
||||
|
||||
def get_tv_episodes(self,
|
||||
item_id: str = None,
|
||||
title: str = None,
|
||||
year: str = None,
|
||||
tmdb_id: int = None,
|
||||
season: int = None) -> Optional[Dict[str, list]]:
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
"""
|
||||
根据标题和年份和季,返回Jellyfin中的剧集列表
|
||||
:param item_id: Jellyfin中的Id
|
||||
:param title: 标题
|
||||
:param year: 年份
|
||||
:param tmdb_id: TMDBID
|
||||
@ -258,16 +284,17 @@ class Jellyfin(metaclass=Singleton):
|
||||
if not self._host or not self._apikey or not self._user:
|
||||
return None
|
||||
# 查TVID
|
||||
item_id = self.__get_jellyfin_series_id_by_name(title, year)
|
||||
if item_id is None:
|
||||
return None
|
||||
if not item_id:
|
||||
return {}
|
||||
# 验证tmdbid是否相同
|
||||
item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
item_id = self.__get_jellyfin_series_id_by_name(title, year)
|
||||
if item_id is None:
|
||||
return None
|
||||
if not item_id:
|
||||
return {}
|
||||
# 验证tmdbid是否相同
|
||||
item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb")
|
||||
if tmdb_id and item_tmdbid:
|
||||
if str(tmdb_id) != str(item_tmdbid):
|
||||
return {}
|
||||
if not season:
|
||||
season = ""
|
||||
try:
|
||||
@ -338,6 +365,24 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接Library/Refresh出错:" + str(e))
|
||||
return False
|
||||
|
||||
def get_webhook_message(self, message: dict) -> dict:
|
||||
"""
|
||||
解析Jellyfin报文
|
||||
"""
|
||||
eventItem = {'event': message.get('NotificationType', ''),
|
||||
'item_name': message.get('Name'),
|
||||
'user_name': message.get('NotificationUsername'),
|
||||
"channel": "jellyfin"
|
||||
}
|
||||
|
||||
# 获取消息图片
|
||||
if eventItem.get("item_id"):
|
||||
# 根据返回的item_id去调用媒体服务器获取
|
||||
eventItem['image_url'] = self.get_remote_image_by_id(item_id=eventItem.get('item_id'),
|
||||
image_type="Backdrop")
|
||||
|
||||
return eventItem
|
||||
|
||||
def get_iteminfo(self, itemid: str) -> dict:
|
||||
"""
|
||||
获取单个项目详情
|
||||
@ -356,20 +401,38 @@ class Jellyfin(metaclass=Singleton):
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
return {}
|
||||
|
||||
def get_webhook_message(self, message: dict) -> dict:
|
||||
def get_items(self, parent: str) -> Generator:
|
||||
"""
|
||||
解析Jellyfin报文
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
eventItem = {'event': message.get('NotificationType', ''),
|
||||
'item_name': message.get('Name'),
|
||||
'user_name': message.get('NotificationUsername'),
|
||||
"channel": "jellyfin"
|
||||
}
|
||||
|
||||
# 获取消息图片
|
||||
if eventItem.get("item_id"):
|
||||
# 根据返回的item_id去调用媒体服务器获取
|
||||
eventItem['image_url'] = self.get_remote_image_by_id(item_id=eventItem.get('item_id'),
|
||||
image_type="Backdrop")
|
||||
|
||||
return eventItem
|
||||
if not parent:
|
||||
yield {}
|
||||
if not self._host or not self._apikey:
|
||||
yield {}
|
||||
req_url = "%sUsers/%s/Items?parentId=%s&api_key=%s" % (self._host, self._user, parent, self._apikey)
|
||||
try:
|
||||
res = RequestUtils().get_res(req_url)
|
||||
if res and res.status_code == 200:
|
||||
results = res.json().get("Items") or []
|
||||
for result in results:
|
||||
if not result:
|
||||
continue
|
||||
if result.get("Type") in ["Movie", "Series"]:
|
||||
item_info = self.get_iteminfo(result.get("Id"))
|
||||
yield {"id": result.get("Id"),
|
||||
"library": item_info.get("ParentId"),
|
||||
"type": item_info.get("Type"),
|
||||
"title": item_info.get("Name"),
|
||||
"original_title": item_info.get("OriginalTitle"),
|
||||
"year": item_info.get("ProductionYear"),
|
||||
"tmdbid": item_info.get("ProviderIds", {}).get("Tmdb"),
|
||||
"imdbid": item_info.get("ProviderIds", {}).get("Imdb"),
|
||||
"tvdbid": item_info.get("ProviderIds", {}).get("Tvdb"),
|
||||
"path": item_info.get("Path"),
|
||||
"json": str(item_info)}
|
||||
elif "Folder" in result.get("Type"):
|
||||
for item in self.get_items(result.get("Id")):
|
||||
yield item
|
||||
except Exception as e:
|
||||
logger.error(f"连接Users/Items出错:" + str(e))
|
||||
yield {}
|
||||
|
@ -1,5 +1,5 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional, Tuple, Union, Any
|
||||
from typing import Optional, Tuple, Union, Any, List, Generator
|
||||
|
||||
from app import schemas
|
||||
from app.core.context import MediaInfo
|
||||
@ -86,3 +86,50 @@ class PlexModule(_ModuleBase):
|
||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||
user_count=1
|
||||
)
|
||||
|
||||
def mediaserver_librarys(self) -> List[schemas.MediaServerLibrary]:
|
||||
"""
|
||||
媒体库列表
|
||||
"""
|
||||
librarys = self.plex.get_librarys()
|
||||
if not librarys:
|
||||
return []
|
||||
return [schemas.MediaServerLibrary(
|
||||
server="plex",
|
||||
id=library.get("id"),
|
||||
name=library.get("name"),
|
||||
type=library.get("type"),
|
||||
path=library.get("path")
|
||||
) for library in librarys]
|
||||
|
||||
def mediaserver_items(self, library_id: str) -> Generator:
|
||||
"""
|
||||
媒体库项目列表
|
||||
"""
|
||||
items = self.plex.get_items(library_id)
|
||||
for item in items:
|
||||
yield schemas.MediaServerItem(
|
||||
server="plex",
|
||||
library=item.get("library"),
|
||||
item_id=item.get("id"),
|
||||
item_type=item.get("type"),
|
||||
title=item.get("title"),
|
||||
original_title=item.get("original_title"),
|
||||
year=item.get("year"),
|
||||
tmdbid=item.get("tmdbid"),
|
||||
imdbid=item.get("imdbid"),
|
||||
tvdbid=item.get("tvdbid"),
|
||||
path=item.get("path"),
|
||||
)
|
||||
|
||||
def mediaserver_tv_episodes(self, item_id: Union[str, int]) -> List[schemas.MediaServerSeasonInfo]:
|
||||
"""
|
||||
获取剧集信息
|
||||
"""
|
||||
seasoninfo = self.plex.get_tv_episodes(item_id=item_id)
|
||||
if not seasoninfo:
|
||||
return []
|
||||
return [schemas.MediaServerSeasonInfo(
|
||||
season=season,
|
||||
episodes=episodes
|
||||
) for season, episodes in seasoninfo.items()]
|
||||
|
@ -1,6 +1,6 @@
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import List, Optional, Dict, Tuple
|
||||
from typing import List, Optional, Dict, Tuple, Generator
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from plexapi import media
|
||||
@ -8,7 +8,7 @@ from plexapi.server import PlexServer
|
||||
|
||||
from app.core.config import settings
|
||||
from app.log import logger
|
||||
from app.schemas import RefreshMediaItem
|
||||
from app.schemas import RefreshMediaItem, MediaType
|
||||
from app.utils.singleton import Singleton
|
||||
|
||||
|
||||
@ -30,6 +30,34 @@ class Plex(metaclass=Singleton):
|
||||
self._plex = None
|
||||
logger.error(f"Plex服务器连接失败:{str(e)}")
|
||||
|
||||
def get_librarys(self):
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not self._plex:
|
||||
return []
|
||||
try:
|
||||
self._libraries = self._plex.library.sections()
|
||||
except Exception as err:
|
||||
logger.error(f"获取媒体服务器所有媒体库列表出错:{str(err)}")
|
||||
return []
|
||||
libraries = []
|
||||
for library in self._libraries:
|
||||
match library.type:
|
||||
case "movie":
|
||||
library_type = MediaType.MOVIE.value
|
||||
case "show":
|
||||
library_type = MediaType.TV.value
|
||||
case _:
|
||||
continue
|
||||
libraries.append({
|
||||
"id": library.key,
|
||||
"name": library.title,
|
||||
"path": library.locations,
|
||||
"type": library_type
|
||||
})
|
||||
return libraries
|
||||
|
||||
def get_activity_log(self, num: int = 30) -> Optional[List[dict]]:
|
||||
"""
|
||||
获取Plex活动记录
|
||||
@ -111,11 +139,13 @@ class Plex(metaclass=Singleton):
|
||||
return ret_movies
|
||||
|
||||
def get_tv_episodes(self,
|
||||
item_id: str = None,
|
||||
title: str = None,
|
||||
year: str = None,
|
||||
season: int = None) -> Optional[Dict[str, list]]:
|
||||
season: int = None) -> Optional[Dict[int, list]]:
|
||||
"""
|
||||
根据标题、年份、季查询电视剧所有集信息
|
||||
:param item_id: 媒体ID
|
||||
:param title: 标题
|
||||
:param year: 年份,可以为空,为空时不按年份过滤
|
||||
:param season: 季号,数字
|
||||
@ -123,7 +153,10 @@ class Plex(metaclass=Singleton):
|
||||
"""
|
||||
if not self._plex:
|
||||
return {}
|
||||
videos = self._plex.library.search(title=title, year=year, libtype="show")
|
||||
if item_id:
|
||||
videos = self._plex.library.sectionByID(item_id).all()
|
||||
else:
|
||||
videos = self._plex.library.search(title=title, year=year, libtype="show")
|
||||
if not videos:
|
||||
return {}
|
||||
episodes = videos[0].episodes()
|
||||
@ -252,6 +285,38 @@ class Plex(metaclass=Singleton):
|
||||
break
|
||||
return ids
|
||||
|
||||
def get_items(self, parent: str) -> Generator:
|
||||
"""
|
||||
获取媒体服务器所有媒体库列表
|
||||
"""
|
||||
if not parent:
|
||||
yield {}
|
||||
if not self._plex:
|
||||
yield {}
|
||||
try:
|
||||
section = self._plex.library.sectionByID(parent)
|
||||
if section:
|
||||
for item in section.all():
|
||||
if not item:
|
||||
continue
|
||||
ids = self.__get_ids(item.guids)
|
||||
path = None
|
||||
if item.locations:
|
||||
path = item.locations[0]
|
||||
yield {"id": item.key,
|
||||
"library": item.librarySectionID,
|
||||
"type": item.type,
|
||||
"title": item.title,
|
||||
"original_title": item.originalTitle,
|
||||
"year": item.year,
|
||||
"tmdbid": ids['tmdb_id'],
|
||||
"imdbid": ids['imdb_id'],
|
||||
"tvdbid": ids['tvdb_id'],
|
||||
"path": path}
|
||||
except Exception as err:
|
||||
logger.error(f"获取媒体库列表出错:{err}")
|
||||
yield {}
|
||||
|
||||
def get_webhook_message(self, message_str: str) -> dict:
|
||||
"""
|
||||
解析Plex报文
|
||||
|
@ -8,6 +8,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
||||
from app.chain import ChainBase
|
||||
from app.chain.cookiecloud import CookieCloudChain
|
||||
from app.chain.douban import DoubanChain
|
||||
from app.chain.mediaserver import MediaServerChain
|
||||
from app.chain.subscribe import SubscribeChain
|
||||
from app.chain.transfer import TransferChain
|
||||
from app.core.config import settings
|
||||
@ -48,6 +49,11 @@ class Scheduler(metaclass=Singleton):
|
||||
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1),
|
||||
name="同步CookieCloud站点")
|
||||
|
||||
# 媒体服务器同步
|
||||
if settings.MEDIASERVER_SYNC_INTERVAL:
|
||||
self._scheduler.add_job(MediaServerChain().sync, "interval",
|
||||
hours=settings.MEDIASERVER_SYNC_INTERVAL, name="同步媒体服务器")
|
||||
|
||||
# 新增订阅时搜索(5分钟检查一次)
|
||||
self._scheduler.add_job(SubscribeChain().search, "interval",
|
||||
minutes=5, kwargs={'state': 'N'})
|
||||
|
@ -8,3 +8,7 @@ from .servarr import *
|
||||
from .plugin import *
|
||||
from .history import *
|
||||
from .dashboard import *
|
||||
from .mediaserver import *
|
||||
from .message import *
|
||||
from .tmdb import *
|
||||
from .transfer import *
|
||||
|
@ -1,10 +1,7 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, List, Union
|
||||
from typing import Optional, Dict, List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.types import MediaType, NotificationType, MessageChannel
|
||||
|
||||
|
||||
class MetaInfo(BaseModel):
|
||||
"""
|
||||
@ -199,165 +196,3 @@ class Context(BaseModel):
|
||||
media_info: Optional[MediaInfo] = None
|
||||
# 种子信息
|
||||
torrent_info: Optional[TorrentInfo] = None
|
||||
|
||||
|
||||
class TransferTorrent(BaseModel):
|
||||
"""
|
||||
待转移任务信息
|
||||
"""
|
||||
title: Optional[str] = None
|
||||
path: Optional[Path] = None
|
||||
hash: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
|
||||
|
||||
class DownloadingTorrent(BaseModel):
|
||||
"""
|
||||
下载中任务信息
|
||||
"""
|
||||
hash: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
year: Optional[str] = None
|
||||
season_episode: Optional[str] = None
|
||||
size: Optional[float] = 0
|
||||
progress: Optional[float] = 0
|
||||
state: Optional[str] = 'downloading'
|
||||
upspeed: Optional[str] = None
|
||||
dlspeed: Optional[str] = None
|
||||
media: Optional[dict] = {}
|
||||
|
||||
|
||||
class TransferInfo(BaseModel):
|
||||
"""
|
||||
文件转移结果信息
|
||||
"""
|
||||
# 转移⼁路径
|
||||
path: Optional[Path] = None
|
||||
# 转移后路径
|
||||
target_path: Optional[Path] = None
|
||||
# 处理文件数
|
||||
file_count: Optional[int] = 0
|
||||
# 总文件大小
|
||||
total_size: Optional[float] = 0
|
||||
# 失败清单
|
||||
fail_list: Optional[list] = []
|
||||
# 错误信息
|
||||
message: Optional[str] = None
|
||||
|
||||
|
||||
class ExistMediaInfo(BaseModel):
|
||||
"""
|
||||
媒体服务器存在媒体信息
|
||||
"""
|
||||
# 类型 电影、电视剧
|
||||
type: Optional[MediaType]
|
||||
# 季
|
||||
seasons: Optional[Dict[int, list]] = {}
|
||||
|
||||
|
||||
class NotExistMediaInfo(BaseModel):
|
||||
"""
|
||||
媒体服务器不存在媒体信息
|
||||
"""
|
||||
# 季
|
||||
season: Optional[int] = None
|
||||
# 剧集列表
|
||||
episodes: Optional[list] = []
|
||||
# 总集数
|
||||
total_episodes: Optional[int] = 0
|
||||
# 开始集
|
||||
start_episode: Optional[int] = 0
|
||||
|
||||
|
||||
class RefreshMediaItem(BaseModel):
|
||||
"""
|
||||
媒体库刷新信息
|
||||
"""
|
||||
# 标题
|
||||
title: Optional[str] = None
|
||||
# 年份
|
||||
year: Optional[str] = None
|
||||
# 类型
|
||||
type: Optional[MediaType] = None
|
||||
# 类别
|
||||
category: Optional[str] = None
|
||||
# 目录
|
||||
target_path: Optional[Path] = None
|
||||
|
||||
|
||||
class TmdbSeason(BaseModel):
|
||||
"""
|
||||
TMDB季信息
|
||||
"""
|
||||
air_date: Optional[str] = None
|
||||
episode_count: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
overview: Optional[str] = None
|
||||
poster_path: Optional[str] = None
|
||||
season_number: Optional[int] = None
|
||||
vote_average: Optional[float] = None
|
||||
|
||||
|
||||
class TmdbEpisode(BaseModel):
|
||||
"""
|
||||
TMDB集信息
|
||||
"""
|
||||
air_date: Optional[str] = None
|
||||
episode_number: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
overview: Optional[str] = None
|
||||
runtime: Optional[int] = None
|
||||
season_number: Optional[int] = None
|
||||
still_path: Optional[str] = None
|
||||
vote_average: Optional[float] = None
|
||||
crew: Optional[list] = []
|
||||
guest_stars: Optional[list] = []
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
"""
|
||||
消息
|
||||
"""
|
||||
# 消息渠道
|
||||
channel: Optional[MessageChannel] = None
|
||||
# 消息类型
|
||||
mtype: Optional[NotificationType] = None
|
||||
# 标题
|
||||
title: Optional[str] = None
|
||||
# 文本内容
|
||||
text: Optional[str] = None
|
||||
# 图片
|
||||
image: Optional[str] = None
|
||||
# 链接
|
||||
link: Optional[str] = None
|
||||
# 用户ID
|
||||
userid: Optional[Union[str, int]] = None
|
||||
|
||||
|
||||
class CommingMessage(BaseModel):
|
||||
"""
|
||||
外来消息
|
||||
"""
|
||||
# 用户ID
|
||||
userid: Optional[Union[str, int]] = None
|
||||
# 用户名称
|
||||
username: Optional[str] = None
|
||||
# 消息渠道
|
||||
channel: Optional[MessageChannel] = None
|
||||
# 消息体
|
||||
text: Optional[str] = None
|
||||
|
||||
|
||||
class NotificationSwitch(BaseModel):
|
||||
"""
|
||||
消息开关
|
||||
"""
|
||||
# 消息类型
|
||||
mtype: Optional[str] = None
|
||||
# 微信开关
|
||||
wechat: Optional[bool] = False
|
||||
# TG开关
|
||||
telegram: Optional[bool] = False
|
||||
# Slack开关
|
||||
slack: Optional[bool] = False
|
||||
|
111
app/schemas/mediaserver.py
Normal file
111
app/schemas/mediaserver.py
Normal file
@ -0,0 +1,111 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional, Dict, Union, List
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.types import MediaType
|
||||
|
||||
|
||||
class ExistMediaInfo(BaseModel):
|
||||
"""
|
||||
媒体服务器存在媒体信息
|
||||
"""
|
||||
# 类型 电影、电视剧
|
||||
type: Optional[MediaType]
|
||||
# 季
|
||||
seasons: Optional[Dict[int, list]] = {}
|
||||
|
||||
|
||||
class NotExistMediaInfo(BaseModel):
|
||||
"""
|
||||
媒体服务器不存在媒体信息
|
||||
"""
|
||||
# 季
|
||||
season: Optional[int] = None
|
||||
# 剧集列表
|
||||
episodes: Optional[list] = []
|
||||
# 总集数
|
||||
total_episodes: Optional[int] = 0
|
||||
# 开始集
|
||||
start_episode: Optional[int] = 0
|
||||
|
||||
|
||||
class RefreshMediaItem(BaseModel):
|
||||
"""
|
||||
媒体库刷新信息
|
||||
"""
|
||||
# 标题
|
||||
title: Optional[str] = None
|
||||
# 年份
|
||||
year: Optional[str] = None
|
||||
# 类型
|
||||
type: Optional[MediaType] = None
|
||||
# 类别
|
||||
category: Optional[str] = None
|
||||
# 目录
|
||||
target_path: Optional[Path] = None
|
||||
|
||||
|
||||
class MediaServerLibrary(BaseModel):
|
||||
"""
|
||||
媒体服务器媒体库信息
|
||||
"""
|
||||
# 服务器
|
||||
server: Optional[str] = None
|
||||
# ID
|
||||
id: Optional[Union[str, int]] = None
|
||||
# 名称
|
||||
name: Optional[str] = None
|
||||
# 路径
|
||||
path: Optional[Union[str, list]] = None
|
||||
# 类型
|
||||
type: Optional[str] = None
|
||||
# 封面图
|
||||
image: Optional[str] = None
|
||||
|
||||
|
||||
class MediaServerItem(BaseModel):
|
||||
"""
|
||||
媒体服务器媒体信息
|
||||
"""
|
||||
# ID
|
||||
id: Optional[Union[str, int]] = None
|
||||
# 服务器
|
||||
server: Optional[str] = None
|
||||
# 媒体库ID
|
||||
library: Optional[Union[str, int]] = None
|
||||
# ID
|
||||
item_id: Optional[str] = None
|
||||
# 类型
|
||||
item_type: Optional[str] = None
|
||||
# 标题
|
||||
title: Optional[str] = None
|
||||
# 原标题
|
||||
original_title: Optional[str] = None
|
||||
# 年份
|
||||
year: Optional[str] = None
|
||||
# TMDBID
|
||||
tmdbid: Optional[int] = None
|
||||
# IMDBID
|
||||
imdbid: Optional[str] = None
|
||||
# TVDBID
|
||||
tvdbid: Optional[str] = None
|
||||
# 路径
|
||||
path: Optional[str] = None
|
||||
# 季集
|
||||
seasoninfo: Optional[Dict[int, list]] = None
|
||||
# 备注
|
||||
note: Optional[str] = None
|
||||
# 同步时间
|
||||
lst_mod_date: Optional[str] = None
|
||||
|
||||
class Config:
|
||||
orm_mode = True
|
||||
|
||||
|
||||
class MediaServerSeasonInfo(BaseModel):
|
||||
"""
|
||||
媒体服务器媒体剧集信息
|
||||
"""
|
||||
season: Optional[int] = None
|
||||
episodes: Optional[List[int]] = []
|
53
app/schemas/message.py
Normal file
53
app/schemas/message.py
Normal file
@ -0,0 +1,53 @@
|
||||
from typing import Optional, Union
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from app.schemas.types import NotificationType, MessageChannel
|
||||
|
||||
|
||||
class CommingMessage(BaseModel):
|
||||
"""
|
||||
外来消息
|
||||
"""
|
||||
# 用户ID
|
||||
userid: Optional[Union[str, int]] = None
|
||||
# 用户名称
|
||||
username: Optional[str] = None
|
||||
# 消息渠道
|
||||
channel: Optional[MessageChannel] = None
|
||||
# 消息体
|
||||
text: Optional[str] = None
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
"""
|
||||
消息
|
||||
"""
|
||||
# 消息渠道
|
||||
channel: Optional[MessageChannel] = None
|
||||
# 消息类型
|
||||
mtype: Optional[NotificationType] = None
|
||||
# 标题
|
||||
title: Optional[str] = None
|
||||
# 文本内容
|
||||
text: Optional[str] = None
|
||||
# 图片
|
||||
image: Optional[str] = None
|
||||
# 链接
|
||||
link: Optional[str] = None
|
||||
# 用户ID
|
||||
userid: Optional[Union[str, int]] = None
|
||||
|
||||
|
||||
class NotificationSwitch(BaseModel):
|
||||
"""
|
||||
消息开关
|
||||
"""
|
||||
# 消息类型
|
||||
mtype: Optional[str] = None
|
||||
# 微信开关
|
||||
wechat: Optional[bool] = False
|
||||
# TG开关
|
||||
telegram: Optional[bool] = False
|
||||
# Slack开关
|
||||
slack: Optional[bool] = False
|
32
app/schemas/tmdb.py
Normal file
32
app/schemas/tmdb.py
Normal file
@ -0,0 +1,32 @@
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TmdbSeason(BaseModel):
|
||||
"""
|
||||
TMDB季信息
|
||||
"""
|
||||
air_date: Optional[str] = None
|
||||
episode_count: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
overview: Optional[str] = None
|
||||
poster_path: Optional[str] = None
|
||||
season_number: Optional[int] = None
|
||||
vote_average: Optional[float] = None
|
||||
|
||||
|
||||
class TmdbEpisode(BaseModel):
|
||||
"""
|
||||
TMDB集信息
|
||||
"""
|
||||
air_date: Optional[str] = None
|
||||
episode_number: Optional[int] = None
|
||||
name: Optional[str] = None
|
||||
overview: Optional[str] = None
|
||||
runtime: Optional[int] = None
|
||||
season_number: Optional[int] = None
|
||||
still_path: Optional[str] = None
|
||||
vote_average: Optional[float] = None
|
||||
crew: Optional[list] = []
|
||||
guest_stars: Optional[list] = []
|
49
app/schemas/transfer.py
Normal file
49
app/schemas/transfer.py
Normal file
@ -0,0 +1,49 @@
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
|
||||
class TransferTorrent(BaseModel):
|
||||
"""
|
||||
待转移任务信息
|
||||
"""
|
||||
title: Optional[str] = None
|
||||
path: Optional[Path] = None
|
||||
hash: Optional[str] = None
|
||||
tags: Optional[str] = None
|
||||
|
||||
|
||||
class DownloadingTorrent(BaseModel):
|
||||
"""
|
||||
下载中任务信息
|
||||
"""
|
||||
hash: Optional[str] = None
|
||||
title: Optional[str] = None
|
||||
name: Optional[str] = None
|
||||
year: Optional[str] = None
|
||||
season_episode: Optional[str] = None
|
||||
size: Optional[float] = 0
|
||||
progress: Optional[float] = 0
|
||||
state: Optional[str] = 'downloading'
|
||||
upspeed: Optional[str] = None
|
||||
dlspeed: Optional[str] = None
|
||||
media: Optional[dict] = {}
|
||||
|
||||
|
||||
class TransferInfo(BaseModel):
|
||||
"""
|
||||
文件转移结果信息
|
||||
"""
|
||||
# 转移⼁路径
|
||||
path: Optional[Path] = None
|
||||
# 转移后路径
|
||||
target_path: Optional[Path] = None
|
||||
# 处理文件数
|
||||
file_count: Optional[int] = 0
|
||||
# 总文件大小
|
||||
total_size: Optional[float] = 0
|
||||
# 失败清单
|
||||
fail_list: Optional[list] = []
|
||||
# 错误信息
|
||||
message: Optional[str] = None
|
Loading…
x
Reference in New Issue
Block a user