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:** 媒体服务器,支持`emby`/`jellyfin`/`plex`,同时还需要配置对应媒体服务器的环境变量,非对应媒体服务器的变量可删除,推荐使用`emby`
|
||||||
|
|
||||||
|
**MEDIASERVER_SYNC_INTERVAL:** 媒体服务器同步间隔(小时),默认`6`,留空则不同步
|
||||||
|
|
||||||
`emby`设置项:
|
`emby`设置项:
|
||||||
|
|
||||||
- **EMBY_HOST:** Emby服务器地址,格式:`ip:port`,https需要添加`https://`前缀
|
- **EMBY_HOST:** Emby服务器地址,格式:`ip:port`,https需要添加`https://`前缀
|
||||||
@ -218,10 +220,10 @@ docker pull jxxghp/moviepilot:latest
|
|||||||
- [x] 自定义识别词
|
- [x] 自定义识别词
|
||||||
- [x] 便捷工具
|
- [x] 便捷工具
|
||||||
- [x] 过滤规则维护
|
- [x] 过滤规则维护
|
||||||
|
- [x] 本地存在标识
|
||||||
|
- [ ] 插件管理
|
||||||
- [ ] 手动整理功能增强
|
- [ ] 手动整理功能增强
|
||||||
- [ ] 本地存在标识
|
|
||||||
- [ ] 媒体详情页面
|
- [ ] 媒体详情页面
|
||||||
- [ ] 洗版支持
|
- [ ] 洗版支持
|
||||||
- [ ] 插件管理
|
|
||||||
|
|
||||||
|
|
||||||
|
@ -1,10 +1,13 @@
|
|||||||
from typing import List, Any
|
from typing import List, Any
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends
|
from fastapi import APIRouter, Depends
|
||||||
|
from sqlalchemy.orm import Session
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.media import MediaChain
|
from app.chain.media import MediaChain
|
||||||
from app.core.security import verify_token
|
from app.core.security import verify_token
|
||||||
|
from app.db import get_db
|
||||||
|
from app.db.mediaserver_oper import MediaServerOper
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@ -35,3 +38,22 @@ def search_by_title(title: str,
|
|||||||
if medias:
|
if medias:
|
||||||
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
return [media.to_dict() for media in medias[(page - 1) * count: page * count]]
|
||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/exists", summary="本地是否存在", response_model=schemas.Response)
|
||||||
|
def exists(title: str = None,
|
||||||
|
year: int = None,
|
||||||
|
mtype: str = None,
|
||||||
|
tmdbid: int = None,
|
||||||
|
season: int = None,
|
||||||
|
db: Session = Depends(get_db),
|
||||||
|
_: schemas.TokenPayload = Depends(verify_token)) -> Any:
|
||||||
|
"""
|
||||||
|
判断本地是否存在
|
||||||
|
"""
|
||||||
|
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 json
|
|
||||||
import time
|
import time
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from typing import Union
|
from typing import Union
|
||||||
|
@ -12,7 +12,7 @@ from app.core.context import MediaInfo
|
|||||||
from app.core.metainfo import MetaInfo
|
from app.core.metainfo import MetaInfo
|
||||||
from app.helper.rss import RssHelper
|
from app.helper.rss import RssHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import MediaType, Notification, MessageChannel, NotificationType
|
from app.schemas import MediaType, Notification, MessageChannel
|
||||||
|
|
||||||
|
|
||||||
class DoubanChain(ChainBase):
|
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))
|
title="开始同步豆瓣想看 ...", userid=userid))
|
||||||
self.sync()
|
self.sync()
|
||||||
self.post_message(Notification(channel=channel, mtype=NotificationType.Subscribe,
|
self.post_message(Notification(channel=channel,
|
||||||
title="同步豆瓣想看数据完成!", userid=userid))
|
title="同步豆瓣想看数据完成!", userid=userid))
|
||||||
|
|
||||||
def sync(self):
|
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:
|
if not site:
|
||||||
self.post_message(Notification(
|
self.post_message(Notification(
|
||||||
channel=channel,
|
channel=channel,
|
||||||
title=f"站点编号 {site_id} 不存在!", userid=userid))
|
title=f"站点编号 {site_id} 不存在!",
|
||||||
|
userid=userid))
|
||||||
return
|
return
|
||||||
# 禁用站点
|
# 禁用站点
|
||||||
self.siteoper.update(site_id, {
|
self.siteoper.update(site_id, {
|
||||||
|
@ -6,6 +6,7 @@ from app.chain import ChainBase
|
|||||||
from app.chain.cookiecloud import CookieCloudChain
|
from app.chain.cookiecloud import CookieCloudChain
|
||||||
from app.chain.douban import DoubanChain
|
from app.chain.douban import DoubanChain
|
||||||
from app.chain.download import DownloadChain
|
from app.chain.download import DownloadChain
|
||||||
|
from app.chain.mediaserver import MediaServerChain
|
||||||
from app.chain.site import SiteChain
|
from app.chain.site import SiteChain
|
||||||
from app.chain.subscribe import SubscribeChain
|
from app.chain.subscribe import SubscribeChain
|
||||||
from app.chain.transfer import TransferChain
|
from app.chain.transfer import TransferChain
|
||||||
@ -74,6 +75,11 @@ class Command(metaclass=Singleton):
|
|||||||
"description": "同步豆瓣想看",
|
"description": "同步豆瓣想看",
|
||||||
"data": {}
|
"data": {}
|
||||||
},
|
},
|
||||||
|
"/mediaserver_sync": {
|
||||||
|
"func": MediaServerChain().remote_sync,
|
||||||
|
"description": "同步媒体服务器",
|
||||||
|
"data": {}
|
||||||
|
},
|
||||||
"/subscribes": {
|
"/subscribes": {
|
||||||
"func": SubscribeChain().remote_list,
|
"func": SubscribeChain().remote_list,
|
||||||
"description": "查询订阅",
|
"description": "查询订阅",
|
||||||
|
@ -113,6 +113,8 @@ class Settings(BaseSettings):
|
|||||||
DOWNLOAD_CATEGORY: bool = False
|
DOWNLOAD_CATEGORY: bool = False
|
||||||
# 媒体服务器 emby/jellyfin/plex
|
# 媒体服务器 emby/jellyfin/plex
|
||||||
MEDIASERVER: str = "emby"
|
MEDIASERVER: str = "emby"
|
||||||
|
# 媒体服务器同步间隔(小时)
|
||||||
|
MEDIASERVER_SYNC_INTERVAL: int = 6
|
||||||
# EMBY服务器地址,IP:PORT
|
# EMBY服务器地址,IP:PORT
|
||||||
EMBY_HOST: str = None
|
EMBY_HOST: str = None
|
||||||
# EMBY Api Key
|
# 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
|
return
|
||||||
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
|
xml_str = doc.toprettyxml(indent=" ", encoding="utf-8")
|
||||||
file_path.write_bytes(xml_str)
|
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 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 import schemas
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
@ -97,3 +97,50 @@ class EmbyModule(_ModuleBase):
|
|||||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
user_count=user_count 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 json
|
||||||
import re
|
import re
|
||||||
from pathlib import Path
|
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.core.config import settings
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
@ -43,7 +43,7 @@ class Emby(metaclass=Singleton):
|
|||||||
logger.error(f"连接Library/SelectableMediaFolders 出错:" + str(e))
|
logger.error(f"连接Library/SelectableMediaFolders 出错:" + str(e))
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def get_emby_librarys(self) -> List[dict]:
|
def __get_emby_librarys(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
获取Emby媒体库列表
|
获取Emby媒体库列表
|
||||||
"""
|
"""
|
||||||
@ -61,6 +61,29 @@ class Emby(metaclass=Singleton):
|
|||||||
logger.error(f"连接User/Views 出错:" + str(e))
|
logger.error(f"连接User/Views 出错:" + str(e))
|
||||||
return []
|
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]]:
|
def get_user(self, user_name: str = None) -> Optional[Union[str, int]]:
|
||||||
"""
|
"""
|
||||||
获得管理员用户
|
获得管理员用户
|
||||||
@ -269,12 +292,14 @@ class Emby(metaclass=Singleton):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_tv_episodes(self,
|
def get_tv_episodes(self,
|
||||||
|
item_id: str = None,
|
||||||
title: str = None,
|
title: str = None,
|
||||||
year: str = None,
|
year: str = None,
|
||||||
tmdb_id: int = None,
|
tmdb_id: int = None,
|
||||||
season: int = None) -> Optional[Dict[int, list]]:
|
season: int = None) -> Optional[Dict[int, list]]:
|
||||||
"""
|
"""
|
||||||
根据标题和年份和季,返回Emby中的剧集列表
|
根据标题和年份和季,返回Emby中的剧集列表
|
||||||
|
:param item_id: Emby中的ID
|
||||||
:param title: 标题
|
:param title: 标题
|
||||||
:param year: 年份
|
:param year: 年份
|
||||||
:param tmdb_id: TMDBID
|
:param tmdb_id: TMDBID
|
||||||
@ -284,16 +309,17 @@ class Emby(metaclass=Singleton):
|
|||||||
if not self._host or not self._apikey:
|
if not self._host or not self._apikey:
|
||||||
return None
|
return None
|
||||||
# 电视剧
|
# 电视剧
|
||||||
item_id = self.__get_emby_series_id_by_name(title, year)
|
|
||||||
if item_id is None:
|
|
||||||
return None
|
|
||||||
if not item_id:
|
if not item_id:
|
||||||
return {}
|
item_id = self.__get_emby_series_id_by_name(title, year)
|
||||||
# 验证tmdbid是否相同
|
if item_id is None:
|
||||||
item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb")
|
return None
|
||||||
if tmdb_id and item_tmdbid:
|
if not item_id:
|
||||||
if str(tmdb_id) != str(item_tmdbid):
|
|
||||||
return {}
|
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 查集的信息
|
# /Shows/Id/Episodes 查集的信息
|
||||||
if not season:
|
if not season:
|
||||||
season = ""
|
season = ""
|
||||||
@ -475,6 +501,42 @@ class Emby(metaclass=Singleton):
|
|||||||
logger.error(f"连接Items/Id出错:" + str(e))
|
logger.error(f"连接Items/Id出错:" + str(e))
|
||||||
return {}
|
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:
|
def get_webhook_message(self, message_str: str) -> dict:
|
||||||
"""
|
"""
|
||||||
解析Emby Webhook报文
|
解析Emby Webhook报文
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
import json
|
import json
|
||||||
from pathlib import Path
|
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 import schemas
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
@ -89,3 +89,50 @@ class JellyfinModule(_ModuleBase):
|
|||||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
user_count=user_count 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 json
|
||||||
import re
|
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.core.config import settings
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
|
from app.schemas import MediaType
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
@ -22,7 +23,7 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
self._user = self.get_user()
|
self._user = self.get_user()
|
||||||
self._serverid = self.get_server_id()
|
self._serverid = self.get_server_id()
|
||||||
|
|
||||||
def get_jellyfin_librarys(self) -> List[dict]:
|
def __get_jellyfin_librarys(self) -> List[dict]:
|
||||||
"""
|
"""
|
||||||
获取Jellyfin媒体库的信息
|
获取Jellyfin媒体库的信息
|
||||||
"""
|
"""
|
||||||
@ -40,6 +41,29 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
logger.error(f"连接Users/Views 出错:" + str(e))
|
logger.error(f"连接Users/Views 出错:" + str(e))
|
||||||
return []
|
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:
|
def get_user_count(self) -> int:
|
||||||
"""
|
"""
|
||||||
获得用户数量
|
获得用户数量
|
||||||
@ -243,12 +267,14 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
def get_tv_episodes(self,
|
def get_tv_episodes(self,
|
||||||
|
item_id: str = None,
|
||||||
title: str = None,
|
title: str = None,
|
||||||
year: str = None,
|
year: str = None,
|
||||||
tmdb_id: int = None,
|
tmdb_id: int = None,
|
||||||
season: int = None) -> Optional[Dict[str, list]]:
|
season: int = None) -> Optional[Dict[int, list]]:
|
||||||
"""
|
"""
|
||||||
根据标题和年份和季,返回Jellyfin中的剧集列表
|
根据标题和年份和季,返回Jellyfin中的剧集列表
|
||||||
|
:param item_id: Jellyfin中的Id
|
||||||
:param title: 标题
|
:param title: 标题
|
||||||
:param year: 年份
|
:param year: 年份
|
||||||
:param tmdb_id: TMDBID
|
:param tmdb_id: TMDBID
|
||||||
@ -258,16 +284,17 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
if not self._host or not self._apikey or not self._user:
|
if not self._host or not self._apikey or not self._user:
|
||||||
return None
|
return None
|
||||||
# 查TVID
|
# 查TVID
|
||||||
item_id = self.__get_jellyfin_series_id_by_name(title, year)
|
|
||||||
if item_id is None:
|
|
||||||
return None
|
|
||||||
if not item_id:
|
if not item_id:
|
||||||
return {}
|
item_id = self.__get_jellyfin_series_id_by_name(title, year)
|
||||||
# 验证tmdbid是否相同
|
if item_id is None:
|
||||||
item_tmdbid = self.get_iteminfo(item_id).get("ProviderIds", {}).get("Tmdb")
|
return None
|
||||||
if tmdb_id and item_tmdbid:
|
if not item_id:
|
||||||
if str(tmdb_id) != str(item_tmdbid):
|
|
||||||
return {}
|
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:
|
if not season:
|
||||||
season = ""
|
season = ""
|
||||||
try:
|
try:
|
||||||
@ -338,6 +365,24 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
logger.error(f"连接Library/Refresh出错:" + str(e))
|
logger.error(f"连接Library/Refresh出错:" + str(e))
|
||||||
return False
|
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:
|
def get_iteminfo(self, itemid: str) -> dict:
|
||||||
"""
|
"""
|
||||||
获取单个项目详情
|
获取单个项目详情
|
||||||
@ -356,20 +401,38 @@ class Jellyfin(metaclass=Singleton):
|
|||||||
logger.error(f"连接Users/Items出错:" + str(e))
|
logger.error(f"连接Users/Items出错:" + str(e))
|
||||||
return {}
|
return {}
|
||||||
|
|
||||||
def get_webhook_message(self, message: dict) -> dict:
|
def get_items(self, parent: str) -> Generator:
|
||||||
"""
|
"""
|
||||||
解析Jellyfin报文
|
获取媒体服务器所有媒体库列表
|
||||||
"""
|
"""
|
||||||
eventItem = {'event': message.get('NotificationType', ''),
|
if not parent:
|
||||||
'item_name': message.get('Name'),
|
yield {}
|
||||||
'user_name': message.get('NotificationUsername'),
|
if not self._host or not self._apikey:
|
||||||
"channel": "jellyfin"
|
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 eventItem.get("item_id"):
|
if res and res.status_code == 200:
|
||||||
# 根据返回的item_id去调用媒体服务器获取
|
results = res.json().get("Items") or []
|
||||||
eventItem['image_url'] = self.get_remote_image_by_id(item_id=eventItem.get('item_id'),
|
for result in results:
|
||||||
image_type="Backdrop")
|
if not result:
|
||||||
|
continue
|
||||||
return eventItem
|
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 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 import schemas
|
||||||
from app.core.context import MediaInfo
|
from app.core.context import MediaInfo
|
||||||
@ -86,3 +86,50 @@ class PlexModule(_ModuleBase):
|
|||||||
episode_count=media_statistic.get("EpisodeCount") or 0,
|
episode_count=media_statistic.get("EpisodeCount") or 0,
|
||||||
user_count=1
|
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
|
import json
|
||||||
from pathlib import Path
|
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 urllib.parse import quote_plus
|
||||||
|
|
||||||
from plexapi import media
|
from plexapi import media
|
||||||
@ -8,7 +8,7 @@ from plexapi.server import PlexServer
|
|||||||
|
|
||||||
from app.core.config import settings
|
from app.core.config import settings
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import RefreshMediaItem
|
from app.schemas import RefreshMediaItem, MediaType
|
||||||
from app.utils.singleton import Singleton
|
from app.utils.singleton import Singleton
|
||||||
|
|
||||||
|
|
||||||
@ -30,6 +30,34 @@ class Plex(metaclass=Singleton):
|
|||||||
self._plex = None
|
self._plex = None
|
||||||
logger.error(f"Plex服务器连接失败:{str(e)}")
|
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]]:
|
def get_activity_log(self, num: int = 30) -> Optional[List[dict]]:
|
||||||
"""
|
"""
|
||||||
获取Plex活动记录
|
获取Plex活动记录
|
||||||
@ -111,11 +139,13 @@ class Plex(metaclass=Singleton):
|
|||||||
return ret_movies
|
return ret_movies
|
||||||
|
|
||||||
def get_tv_episodes(self,
|
def get_tv_episodes(self,
|
||||||
|
item_id: str = None,
|
||||||
title: str = None,
|
title: str = None,
|
||||||
year: 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 title: 标题
|
||||||
:param year: 年份,可以为空,为空时不按年份过滤
|
:param year: 年份,可以为空,为空时不按年份过滤
|
||||||
:param season: 季号,数字
|
:param season: 季号,数字
|
||||||
@ -123,7 +153,10 @@ class Plex(metaclass=Singleton):
|
|||||||
"""
|
"""
|
||||||
if not self._plex:
|
if not self._plex:
|
||||||
return {}
|
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:
|
if not videos:
|
||||||
return {}
|
return {}
|
||||||
episodes = videos[0].episodes()
|
episodes = videos[0].episodes()
|
||||||
@ -252,6 +285,38 @@ class Plex(metaclass=Singleton):
|
|||||||
break
|
break
|
||||||
return ids
|
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:
|
def get_webhook_message(self, message_str: str) -> dict:
|
||||||
"""
|
"""
|
||||||
解析Plex报文
|
解析Plex报文
|
||||||
|
@ -8,6 +8,7 @@ from apscheduler.schedulers.background import BackgroundScheduler
|
|||||||
from app.chain import ChainBase
|
from app.chain import ChainBase
|
||||||
from app.chain.cookiecloud import CookieCloudChain
|
from app.chain.cookiecloud import CookieCloudChain
|
||||||
from app.chain.douban import DoubanChain
|
from app.chain.douban import DoubanChain
|
||||||
|
from app.chain.mediaserver import MediaServerChain
|
||||||
from app.chain.subscribe import SubscribeChain
|
from app.chain.subscribe import SubscribeChain
|
||||||
from app.chain.transfer import TransferChain
|
from app.chain.transfer import TransferChain
|
||||||
from app.core.config import settings
|
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),
|
next_run_time=datetime.now(pytz.timezone(settings.TZ)) + timedelta(minutes=1),
|
||||||
name="同步CookieCloud站点")
|
name="同步CookieCloud站点")
|
||||||
|
|
||||||
|
# 媒体服务器同步
|
||||||
|
if settings.MEDIASERVER_SYNC_INTERVAL:
|
||||||
|
self._scheduler.add_job(MediaServerChain().sync, "interval",
|
||||||
|
hours=settings.MEDIASERVER_SYNC_INTERVAL, name="同步媒体服务器")
|
||||||
|
|
||||||
# 新增订阅时搜索(5分钟检查一次)
|
# 新增订阅时搜索(5分钟检查一次)
|
||||||
self._scheduler.add_job(SubscribeChain().search, "interval",
|
self._scheduler.add_job(SubscribeChain().search, "interval",
|
||||||
minutes=5, kwargs={'state': 'N'})
|
minutes=5, kwargs={'state': 'N'})
|
||||||
|
@ -8,3 +8,7 @@ from .servarr import *
|
|||||||
from .plugin import *
|
from .plugin import *
|
||||||
from .history import *
|
from .history import *
|
||||||
from .dashboard 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
|
||||||
from typing import Optional, Dict, List, Union
|
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
from app.schemas.types import MediaType, NotificationType, MessageChannel
|
|
||||||
|
|
||||||
|
|
||||||
class MetaInfo(BaseModel):
|
class MetaInfo(BaseModel):
|
||||||
"""
|
"""
|
||||||
@ -199,165 +196,3 @@ class Context(BaseModel):
|
|||||||
media_info: Optional[MediaInfo] = None
|
media_info: Optional[MediaInfo] = None
|
||||||
# 种子信息
|
# 种子信息
|
||||||
torrent_info: Optional[TorrentInfo] = 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