mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-09 01:16:50 +08:00
Merge remote-tracking branch 'upstream/dev' into feature/db
This commit is contained in:
@@ -14,6 +14,9 @@ app/plugins/**
|
|||||||
config/cookies/**
|
config/cookies/**
|
||||||
config/user.db
|
config/user.db
|
||||||
config/sites/**
|
config/sites/**
|
||||||
|
config/logs/
|
||||||
|
config/temp/
|
||||||
|
config/cache/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.log
|
*.log
|
||||||
.vscode
|
.vscode
|
||||||
|
|||||||
@@ -398,7 +398,7 @@ def user_subscribes(
|
|||||||
return Subscribe.list_by_username(db, username)
|
return Subscribe.list_by_username(db, username)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/files/{subscribe_id}", summary="订阅相关文件信息", response_model=List[schemas.SubscrbieInfo])
|
@router.get("/files/{subscribe_id}", summary="订阅相关文件信息", response_model=schemas.SubscrbieInfo)
|
||||||
def subscribe_files(
|
def subscribe_files(
|
||||||
subscribe_id: int,
|
subscribe_id: int,
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
|
|||||||
+44
-16
@@ -30,6 +30,10 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
# 临时识别结果 {title, name, year, season, episode}
|
# 临时识别结果 {title, name, year, season, episode}
|
||||||
recognize_temp: Optional[dict] = None
|
recognize_temp: Optional[dict] = None
|
||||||
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__()
|
||||||
|
self.storagechain = StorageChain()
|
||||||
|
|
||||||
def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
|
def metadata_nfo(self, meta: MetaBase, mediainfo: MediaInfo,
|
||||||
season: int = None, episode: int = None) -> Optional[str]:
|
season: int = None, episode: int = None) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
@@ -348,37 +352,35 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
|
|
||||||
def scrape_metadata(self, fileitem: schemas.FileItem,
|
def scrape_metadata(self, fileitem: schemas.FileItem,
|
||||||
meta: MetaBase = None, mediainfo: MediaInfo = None,
|
meta: MetaBase = None, mediainfo: MediaInfo = None,
|
||||||
init_folder: bool = True, parent: schemas.FileItem = None):
|
init_folder: bool = True, parent: schemas.FileItem = None,
|
||||||
|
overwrite: bool = False):
|
||||||
"""
|
"""
|
||||||
手动刮削媒体信息
|
手动刮削媒体信息
|
||||||
|
:param fileitem: 刮削目录或文件
|
||||||
|
:param meta: 元数据
|
||||||
|
:param mediainfo: 媒体信息
|
||||||
|
:param init_folder: 是否刮削根目录
|
||||||
|
:param parent: 上级目录
|
||||||
|
:param overwrite: 是否覆盖已有文件
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __list_files(_fileitem: schemas.FileItem):
|
def __list_files(_fileitem: schemas.FileItem):
|
||||||
"""
|
"""
|
||||||
列出下级文件
|
列出下级文件
|
||||||
"""
|
"""
|
||||||
return StorageChain().list_files(fileitem=_fileitem)
|
return self.storagechain.list_files(fileitem=_fileitem)
|
||||||
|
|
||||||
def __save_file(_fileitem: schemas.FileItem, _path: Path, _content: Union[bytes, str],
|
def __save_file(_fileitem: schemas.FileItem, _path: Path, _content: Union[bytes, str]):
|
||||||
overwrite: bool = False):
|
|
||||||
"""
|
"""
|
||||||
保存或上传文件
|
保存或上传文件
|
||||||
:param _fileitem: 关联的媒体文件项
|
:param _fileitem: 关联的媒体文件项
|
||||||
:param _path: 元数据文件路径
|
:param _path: 元数据文件路径
|
||||||
:param _content: 文件内容
|
:param _content: 文件内容
|
||||||
:param overwrite: 是否覆盖
|
|
||||||
"""
|
"""
|
||||||
if not overwrite and _path.exists():
|
|
||||||
return
|
|
||||||
tmp_file = settings.TEMP_PATH / _path.name
|
tmp_file = settings.TEMP_PATH / _path.name
|
||||||
tmp_file.write_bytes(_content)
|
tmp_file.write_bytes(_content)
|
||||||
upload_item = copy.deepcopy(_fileitem)
|
|
||||||
upload_item.path = str(_path)
|
|
||||||
upload_item.name = _path.name
|
|
||||||
upload_item.basename = _path.stem
|
|
||||||
upload_item.extension = _path.suffix
|
|
||||||
logger.info(f"保存文件:【{_fileitem.storage}】{_path}")
|
logger.info(f"保存文件:【{_fileitem.storage}】{_path}")
|
||||||
StorageChain().upload_file(fileitem=upload_item, path=tmp_file)
|
self.storagechain.upload_file(fileitem=_fileitem, path=tmp_file)
|
||||||
if tmp_file.exists():
|
if tmp_file.exists():
|
||||||
tmp_file.unlink()
|
tmp_file.unlink()
|
||||||
|
|
||||||
@@ -419,7 +421,11 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
logger.warn(f"{filepath.name} nfo文件生成失败!")
|
logger.warn(f"{filepath.name} nfo文件生成失败!")
|
||||||
return
|
return
|
||||||
# 保存或上传nfo文件到上级目录
|
# 保存或上传nfo文件到上级目录
|
||||||
__save_file(_fileitem=parent, _path=filepath.with_suffix(".nfo"), _content=movie_nfo)
|
nfo_path = filepath.with_suffix(".nfo")
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||||
|
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||||
|
return
|
||||||
|
__save_file(_fileitem=parent, _path=nfo_path, _content=movie_nfo)
|
||||||
else:
|
else:
|
||||||
# 电影目录
|
# 电影目录
|
||||||
files = __list_files(_fileitem=fileitem)
|
files = __list_files(_fileitem=fileitem)
|
||||||
@@ -438,6 +444,9 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
and attr_value.startswith("http"):
|
and attr_value.startswith("http"):
|
||||||
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
image_name = attr_name.replace("_path", "") + Path(attr_value).suffix
|
||||||
image_path = filepath / image_name
|
image_path = filepath / image_name
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path):
|
||||||
|
logger.info(f"已存在图片文件:{image_path}")
|
||||||
|
continue
|
||||||
# 下载图片
|
# 下载图片
|
||||||
content = __download_image(_url=attr_value)
|
content = __download_image(_url=attr_value)
|
||||||
# 写入图片到当前目录
|
# 写入图片到当前目录
|
||||||
@@ -461,17 +470,24 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
logger.warn(f"{filepath.name} nfo生成失败!")
|
logger.warn(f"{filepath.name} nfo生成失败!")
|
||||||
return
|
return
|
||||||
# 保存或上传nfo文件到上级目录
|
# 保存或上传nfo文件到上级目录
|
||||||
__save_file(_fileitem=parent, _path=filepath.with_suffix(".nfo"), _content=episode_nfo)
|
nfo_path = filepath.with_suffix(".nfo")
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||||
|
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||||
|
return
|
||||||
|
__save_file(_fileitem=parent, _path=nfo_path, _content=episode_nfo)
|
||||||
# 获取集的图片
|
# 获取集的图片
|
||||||
image_dict = self.metadata_img(mediainfo=file_mediainfo,
|
image_dict = self.metadata_img(mediainfo=file_mediainfo,
|
||||||
season=file_meta.begin_season, episode=file_meta.begin_episode)
|
season=file_meta.begin_season, episode=file_meta.begin_episode)
|
||||||
if image_dict:
|
if image_dict:
|
||||||
for episode, image_url in image_dict.items():
|
for episode, image_url in image_dict.items():
|
||||||
image_path = filepath.with_suffix(Path(image_url).suffix)
|
image_path = filepath.with_suffix(Path(image_url).suffix)
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path):
|
||||||
|
logger.info(f"已存在图片文件:{image_path}")
|
||||||
|
continue
|
||||||
# 下载图片
|
# 下载图片
|
||||||
content = __download_image(image_url)
|
content = __download_image(image_url)
|
||||||
# 保存图片文件到当前目录
|
# 保存图片文件到当前目录
|
||||||
__save_file(_fileitem=fileitem, _path=image_path, _content=content)
|
__save_file(_fileitem=parent, _path=image_path, _content=content)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# 当前为目录,处理目录内的文件
|
# 当前为目录,处理目录内的文件
|
||||||
@@ -493,12 +509,18 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
return
|
return
|
||||||
# 写入nfo到根目录
|
# 写入nfo到根目录
|
||||||
nfo_path = filepath / "season.nfo"
|
nfo_path = filepath / "season.nfo"
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||||
|
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||||
|
return
|
||||||
__save_file(_fileitem=fileitem, _path=nfo_path, _content=season_nfo)
|
__save_file(_fileitem=fileitem, _path=nfo_path, _content=season_nfo)
|
||||||
# TMDB季poster图片
|
# TMDB季poster图片
|
||||||
image_dict = self.metadata_img(mediainfo=mediainfo, season=season_meta.begin_season)
|
image_dict = self.metadata_img(mediainfo=mediainfo, season=season_meta.begin_season)
|
||||||
if image_dict:
|
if image_dict:
|
||||||
for image_name, image_url in image_dict.items():
|
for image_name, image_url in image_dict.items():
|
||||||
image_path = filepath.with_name(image_name)
|
image_path = filepath.with_name(image_name)
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path):
|
||||||
|
logger.info(f"已存在图片文件:{image_path}")
|
||||||
|
continue
|
||||||
# 下载图片
|
# 下载图片
|
||||||
content = __download_image(image_url)
|
content = __download_image(image_url)
|
||||||
# 保存图片文件到当前目录
|
# 保存图片文件到当前目录
|
||||||
@@ -511,12 +533,18 @@ class MediaChain(ChainBase, metaclass=Singleton):
|
|||||||
return
|
return
|
||||||
# 写入tvshow nfo到根目录
|
# 写入tvshow nfo到根目录
|
||||||
nfo_path = filepath / "tvshow.nfo"
|
nfo_path = filepath / "tvshow.nfo"
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=nfo_path):
|
||||||
|
logger.info(f"已存在nfo文件:{nfo_path}")
|
||||||
|
return
|
||||||
__save_file(_fileitem=fileitem, _path=nfo_path, _content=tv_nfo)
|
__save_file(_fileitem=fileitem, _path=nfo_path, _content=tv_nfo)
|
||||||
# 生成目录图片
|
# 生成目录图片
|
||||||
image_dict = self.metadata_img(mediainfo=mediainfo)
|
image_dict = self.metadata_img(mediainfo=mediainfo)
|
||||||
if image_dict:
|
if image_dict:
|
||||||
for image_name, image_url in image_dict.items():
|
for image_name, image_url in image_dict.items():
|
||||||
image_path = filepath / image_name
|
image_path = filepath / image_name
|
||||||
|
if not overwrite and self.storagechain.get_file_item(storage=fileitem.storage, path=image_path):
|
||||||
|
logger.info(f"已存在图片文件:{image_path}")
|
||||||
|
continue
|
||||||
# 下载图片
|
# 下载图片
|
||||||
content = __download_image(image_url)
|
content = __download_image(image_url)
|
||||||
# 保存图片文件到当前目录
|
# 保存图片文件到当前目录
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ class SiteChain(ChainBase):
|
|||||||
userdata: SiteUserData = self.run_module("refresh_userdata", site=site)
|
userdata: SiteUserData = self.run_module("refresh_userdata", site=site)
|
||||||
if userdata:
|
if userdata:
|
||||||
self.siteoper.update_userdata(domain=StringUtils.get_url_domain(site.get("domain")),
|
self.siteoper.update_userdata(domain=StringUtils.get_url_domain(site.get("domain")),
|
||||||
|
name=site.get("name"),
|
||||||
payload=userdata.dict())
|
payload=userdata.dict())
|
||||||
return userdata
|
return userdata
|
||||||
|
|
||||||
|
|||||||
+37
-37
@@ -25,7 +25,8 @@ from app.helper.message import MessageHelper
|
|||||||
from app.helper.subscribe import SubscribeHelper
|
from app.helper.subscribe import SubscribeHelper
|
||||||
from app.helper.torrent import TorrentHelper
|
from app.helper.torrent import TorrentHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo
|
from app.schemas import NotExistMediaInfo, Notification, SubscrbieInfo, SubscribeEpisodeInfo, SubscribeDownloadFileInfo, \
|
||||||
|
SubscribeLibraryFileInfo
|
||||||
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
|
from app.schemas.types import MediaType, SystemConfigKey, MessageChannel, NotificationType, EventType
|
||||||
|
|
||||||
|
|
||||||
@@ -1167,47 +1168,34 @@ class SubscribeChain(ChainBase):
|
|||||||
return
|
return
|
||||||
|
|
||||||
# 返回订阅数据
|
# 返回订阅数据
|
||||||
subscribe_info = SubscrbieInfo(
|
subscribe_info = SubscrbieInfo()
|
||||||
id=subscribe.id,
|
|
||||||
name=subscribe.name,
|
|
||||||
year=subscribe.year,
|
|
||||||
type=subscribe.type,
|
|
||||||
tmdbid=subscribe.tmdbid,
|
|
||||||
doubanid=subscribe.doubanid,
|
|
||||||
season=subscribe.season,
|
|
||||||
poster=subscribe.poster,
|
|
||||||
backdrop=subscribe.backdrop,
|
|
||||||
vote=subscribe.vote,
|
|
||||||
description=subscribe.description,
|
|
||||||
episodes_info={}
|
|
||||||
)
|
|
||||||
|
|
||||||
# 所有集的数据
|
# 所有集的数据
|
||||||
episodes_info = {}
|
episodes: Dict[int, SubscribeEpisodeInfo] = {}
|
||||||
if subscribe.tmdbid and subscribe.type == MediaType.TV.value:
|
if subscribe.tmdbid and subscribe.type == MediaType.TV.value:
|
||||||
# 查询TMDB中的集信息
|
# 查询TMDB中的集信息
|
||||||
tmdb_episodes = self.tmdbchain.tmdb_episodes(
|
tmdb_episodes = self.tmdbchain.tmdb_episodes(
|
||||||
tmdbid=subscribe.tmdb_id,
|
tmdbid=subscribe.tmdbid,
|
||||||
season=subscribe.season
|
season=subscribe.season
|
||||||
)
|
)
|
||||||
if tmdb_episodes:
|
if tmdb_episodes:
|
||||||
for episode in tmdb_episodes:
|
for episode in tmdb_episodes:
|
||||||
episode_info = SubscribeEpisodeInfo()
|
info = SubscribeEpisodeInfo()
|
||||||
episodes_info.title = episode.name
|
info.title = episode.name
|
||||||
episodes_info.description = episode.overview
|
info.description = episode.overview
|
||||||
episodes_info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}"
|
info.backdrop = f"https://{settings.TMDB_IMAGE_DOMAIN}/t/p/w500${episode.still_path}"
|
||||||
episodes_info[episode.episode_number] = episode_info
|
episodes[episode.episode_number] = info
|
||||||
elif subscribe.type == MediaType.TV.value:
|
elif subscribe.type == MediaType.TV.value:
|
||||||
# 根据开始结束集计算集信息
|
# 根据开始结束集计算集信息
|
||||||
for i in range(subscribe.start_episode or 1, subscribe.total_episode + 1):
|
for i in range(subscribe.start_episode or 1, subscribe.total_episode + 1):
|
||||||
episode_info = SubscribeEpisodeInfo()
|
info = SubscribeEpisodeInfo()
|
||||||
episode_info.title = f'第 {i} 集'
|
info.title = f'第 {i} 集'
|
||||||
episodes_info[i] = episode_info
|
episodes[i] = info
|
||||||
else:
|
else:
|
||||||
# 电影
|
# 电影
|
||||||
episode_info = SubscribeEpisodeInfo()
|
info = SubscribeEpisodeInfo()
|
||||||
episode_info.title = subscribe.name
|
info.title = subscribe.name
|
||||||
episodes_info[0] = episode_info
|
episodes[0] = info
|
||||||
|
|
||||||
# 所有下载记录
|
# 所有下载记录
|
||||||
download_his = self.downloadhis.get_by_mediaid(tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid)
|
download_his = self.downloadhis.get_by_mediaid(tmdbid=subscribe.tmdbid, doubanid=subscribe.doubanid)
|
||||||
@@ -1221,14 +1209,20 @@ class SubscribeChain(ChainBase):
|
|||||||
for file in files:
|
for file in files:
|
||||||
# 识别文件名
|
# 识别文件名
|
||||||
file_meta = MetaInfo(file.filepath)
|
file_meta = MetaInfo(file.filepath)
|
||||||
|
# 下载文件信息
|
||||||
|
file_info = SubscribeDownloadFileInfo(
|
||||||
|
torrent_title=his.torrent_name,
|
||||||
|
site_name=his.torrent_site,
|
||||||
|
downloader=file.downloader,
|
||||||
|
hash=his.download_hash,
|
||||||
|
file_path=file.fullpath,
|
||||||
|
)
|
||||||
if subscribe.type == MediaType.TV.value:
|
if subscribe.type == MediaType.TV.value:
|
||||||
episode_number = file_meta.begin_episode
|
episode_number = file_meta.begin_episode
|
||||||
if episode_number and episodes_info.get(episode_number):
|
if episode_number and episodes.get(episode_number):
|
||||||
episodes_info[episode_number].download_file = file.fullpath
|
episodes[episode_number].download.append(file_info)
|
||||||
episodes_info[episode_number].torrent = torrent_url
|
|
||||||
else:
|
else:
|
||||||
episodes_info[0].download_file = file.fullpath
|
episodes[0].download.append(file_info)
|
||||||
episodes_info[0].torrent = torrent_url
|
|
||||||
|
|
||||||
# 生成元数据
|
# 生成元数据
|
||||||
meta = MetaInfo(subscribe.name)
|
meta = MetaInfo(subscribe.name)
|
||||||
@@ -1255,13 +1249,19 @@ class SubscribeChain(ChainBase):
|
|||||||
for fileitem in library_fileitems:
|
for fileitem in library_fileitems:
|
||||||
# 识别文件名
|
# 识别文件名
|
||||||
file_meta = MetaInfo(fileitem.path)
|
file_meta = MetaInfo(fileitem.path)
|
||||||
|
# 媒体库文件信息
|
||||||
|
file_info = SubscribeLibraryFileInfo(
|
||||||
|
storage=fileitem.storage,
|
||||||
|
file_path=fileitem.path,
|
||||||
|
)
|
||||||
if subscribe.type == MediaType.TV.value:
|
if subscribe.type == MediaType.TV.value:
|
||||||
episode_number = file_meta.begin_episode
|
episode_number = file_meta.begin_episode
|
||||||
if episode_number and episodes_info.get(episode_number):
|
if episode_number and episodes.get(episode_number):
|
||||||
episodes_info[episode_number].library_file = fileitem.path
|
episodes[episode_number].library.append(file_info)
|
||||||
else:
|
else:
|
||||||
episodes_info[0].library_file = fileitem.path
|
episodes[0].library.append(file_info)
|
||||||
|
|
||||||
# 更新订阅信息
|
# 更新订阅信息
|
||||||
subscribe_info.episodes_info = episodes_info
|
subscribe_info.subscribe = Subscribe(**subscribe.to_dict())
|
||||||
|
subscribe_info.episodes = episodes
|
||||||
return subscribe_info
|
return subscribe_info
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ class SiteUserData(Base):
|
|||||||
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
|
id = Column(Integer, Sequence('id'), primary_key=True, index=True)
|
||||||
# 站点域名
|
# 站点域名
|
||||||
domain = Column(String, index=True)
|
domain = Column(String, index=True)
|
||||||
|
# 站点名称
|
||||||
|
name = Column(String)
|
||||||
# 用户名
|
# 用户名
|
||||||
username = Column(String)
|
username = Column(String)
|
||||||
# 用户ID
|
# 用户ID
|
||||||
|
|||||||
+8
-1
@@ -104,7 +104,7 @@ class SiteOper(DbOper):
|
|||||||
})
|
})
|
||||||
return True, "更新站点RSS地址成功"
|
return True, "更新站点RSS地址成功"
|
||||||
|
|
||||||
def update_userdata(self, domain: str, payload: dict) -> Tuple[bool, str]:
|
def update_userdata(self, domain: str, name: str, payload: dict) -> Tuple[bool, str]:
|
||||||
"""
|
"""
|
||||||
更新站点用户数据
|
更新站点用户数据
|
||||||
"""
|
"""
|
||||||
@@ -113,6 +113,7 @@ class SiteOper(DbOper):
|
|||||||
current_time = datetime.now().strftime('%H:%M:%S')
|
current_time = datetime.now().strftime('%H:%M:%S')
|
||||||
payload.update({
|
payload.update({
|
||||||
"domain": domain,
|
"domain": domain,
|
||||||
|
"name": name,
|
||||||
"updated_day": current_day,
|
"updated_day": current_day,
|
||||||
"updated_time": current_time
|
"updated_time": current_time
|
||||||
})
|
})
|
||||||
@@ -127,6 +128,12 @@ class SiteOper(DbOper):
|
|||||||
SiteUserData(**payload).create(self._db)
|
SiteUserData(**payload).create(self._db)
|
||||||
return True, "更新站点用户数据成功"
|
return True, "更新站点用户数据成功"
|
||||||
|
|
||||||
|
def get_userdata(self) -> List[SiteUserData]:
|
||||||
|
"""
|
||||||
|
获取站点用户数据
|
||||||
|
"""
|
||||||
|
return SiteUserData.list(self._db)
|
||||||
|
|
||||||
def get_userdata_by_domain(self, domain: str, workdate: str = None) -> List[SiteUserData]:
|
def get_userdata_by_domain(self, domain: str, workdate: str = None) -> List[SiteUserData]:
|
||||||
"""
|
"""
|
||||||
获取站点用户数据
|
获取站点用户数据
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
class DownloaderHelper:
|
from app.helper.servicebase import ServiceBaseHelper
|
||||||
|
from app.schemas import DownloaderConf
|
||||||
|
from app.schemas.types import SystemConfigKey
|
||||||
|
|
||||||
|
|
||||||
|
class DownloaderHelper(ServiceBaseHelper[DownloaderConf]):
|
||||||
"""
|
"""
|
||||||
下载器帮助类
|
下载器帮助类
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
config_key=SystemConfigKey.Downloaders,
|
||||||
|
conf_type=DownloaderConf,
|
||||||
|
modules=["QbittorrentModule", "TransmissionModule"]
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,16 @@
|
|||||||
class MediaServerHelper:
|
from app.helper.servicebase import ServiceBaseHelper
|
||||||
|
from app.schemas import MediaServerConf
|
||||||
|
from app.schemas.types import SystemConfigKey
|
||||||
|
|
||||||
|
|
||||||
|
class MediaServerHelper(ServiceBaseHelper[MediaServerConf]):
|
||||||
"""
|
"""
|
||||||
媒体服务器帮助类
|
媒体服务器帮助类
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
config_key=SystemConfigKey.MediaServers,
|
||||||
|
conf_type=MediaServerConf,
|
||||||
|
modules=["PlexModule", "EmbyModule", "JellyfinModule"]
|
||||||
|
)
|
||||||
|
|||||||
@@ -1,5 +1,17 @@
|
|||||||
class NotificationHelper:
|
from app.helper.servicebase import ServiceBaseHelper
|
||||||
|
from app.schemas import NotificationConf
|
||||||
|
from app.schemas.types import SystemConfigKey
|
||||||
|
|
||||||
|
|
||||||
|
class NotificationHelper(ServiceBaseHelper[NotificationConf]):
|
||||||
"""
|
"""
|
||||||
消息通知渠道帮助类
|
消息通知帮助类
|
||||||
"""
|
"""
|
||||||
pass
|
|
||||||
|
def __init__(self):
|
||||||
|
super().__init__(
|
||||||
|
config_key=SystemConfigKey.Notifications,
|
||||||
|
conf_type=NotificationConf,
|
||||||
|
modules=["WechatModule", "WebPushModule", "VoceChatModule", "TelegramModule", "SynologyChatModule",
|
||||||
|
"SlackModule"]
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
from typing import Dict, List, Optional, Type, TypeVar, Generic, Iterator
|
||||||
|
|
||||||
|
from app.core.module import ModuleManager
|
||||||
|
from app.helper.serviceconfig import ServiceConfigHelper
|
||||||
|
from app.schemas import ServiceInfo
|
||||||
|
from app.schemas.types import SystemConfigKey
|
||||||
|
|
||||||
|
TConf = TypeVar("TConf")
|
||||||
|
|
||||||
|
|
||||||
|
class ServiceBaseHelper(Generic[TConf]):
|
||||||
|
"""
|
||||||
|
通用服务帮助类,抽象获取配置和服务实例的通用逻辑
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, config_key: SystemConfigKey, conf_type: Type[TConf], modules: List[str]):
|
||||||
|
self.modulemanager = ModuleManager()
|
||||||
|
self.config_key = config_key
|
||||||
|
self.conf_type = conf_type
|
||||||
|
self.modules = modules
|
||||||
|
|
||||||
|
def get_configs(self, include_disabled: bool = False) -> Dict[str, TConf]:
|
||||||
|
"""
|
||||||
|
获取配置列表
|
||||||
|
|
||||||
|
:param include_disabled: 是否包含禁用的配置,默认 False(仅返回启用的配置)
|
||||||
|
:return: 配置字典
|
||||||
|
"""
|
||||||
|
configs: List[TConf] = ServiceConfigHelper.get_configs(self.config_key, self.conf_type)
|
||||||
|
return {
|
||||||
|
config.name: config
|
||||||
|
for config in configs
|
||||||
|
if (config.name and config.type and config.enabled) or include_disabled
|
||||||
|
} if configs else {}
|
||||||
|
|
||||||
|
def get_config(self, name: str) -> Optional[TConf]:
|
||||||
|
"""
|
||||||
|
获取指定名称配置
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
configs = self.get_configs()
|
||||||
|
return configs.get(name)
|
||||||
|
|
||||||
|
def iterate_module_instances(self) -> Iterator[ServiceInfo]:
|
||||||
|
"""
|
||||||
|
迭代所有模块的实例及其对应的配置,返回 ServiceInfo 实例
|
||||||
|
"""
|
||||||
|
configs = self.get_configs()
|
||||||
|
for module_name in self.modules:
|
||||||
|
module = self.modulemanager.get_running_module(module_name)
|
||||||
|
if not module:
|
||||||
|
continue
|
||||||
|
module_instances = module.get_instances()
|
||||||
|
if not isinstance(module_instances, dict):
|
||||||
|
continue
|
||||||
|
for name, instance in module_instances.items():
|
||||||
|
if not instance:
|
||||||
|
continue
|
||||||
|
config = configs.get(name)
|
||||||
|
service_info = ServiceInfo(
|
||||||
|
name=name,
|
||||||
|
instance=instance,
|
||||||
|
module=module,
|
||||||
|
type=config.type if config else None,
|
||||||
|
config=config
|
||||||
|
)
|
||||||
|
yield service_info
|
||||||
|
|
||||||
|
def get_services(self, type_filter: Optional[str] = None) -> Dict[str, ServiceInfo]:
|
||||||
|
"""
|
||||||
|
获取服务信息列表,并根据类型过滤
|
||||||
|
|
||||||
|
:param type_filter: 需要过滤的服务类型
|
||||||
|
:return: 过滤后的服务信息字典
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
service_info.name: service_info
|
||||||
|
for service_info in self.iterate_module_instances()
|
||||||
|
if service_info.config and (type_filter is None or service_info.type == type_filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
def get_service(self, name: str, type_filter: Optional[str] = None) -> Optional[ServiceInfo]:
|
||||||
|
"""
|
||||||
|
获取指定名称的服务信息,并根据类型过滤
|
||||||
|
|
||||||
|
:param name: 服务名称
|
||||||
|
:param type_filter: 需要过滤的服务类型
|
||||||
|
:return: 对应的服务信息,若不存在或类型不匹配则返回 None
|
||||||
|
"""
|
||||||
|
if not name:
|
||||||
|
return None
|
||||||
|
for service_info in self.iterate_module_instances():
|
||||||
|
if service_info.name == name:
|
||||||
|
if service_info.config and (type_filter is None or service_info.type == type_filter):
|
||||||
|
return service_info
|
||||||
|
return None
|
||||||
@@ -56,7 +56,7 @@ class ServiceConfigHelper:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def get_notification_switch(mtype: NotificationType) -> Optional[str]:
|
def get_notification_switch(mtype: NotificationType) -> Optional[str]:
|
||||||
"""
|
"""
|
||||||
获取消息通知场景开关
|
获取指定类型的消息通知场景的开关
|
||||||
"""
|
"""
|
||||||
switchs = ServiceConfigHelper.get_notification_switches()
|
switchs = ServiceConfigHelper.get_notification_switches()
|
||||||
for switch in switchs:
|
for switch in switchs:
|
||||||
|
|||||||
@@ -90,6 +90,14 @@ class ServiceBase(Generic[TService, TConf], metaclass=ABCMeta):
|
|||||||
# 如果传入的是工厂函数,直接调用工厂函数
|
# 如果传入的是工厂函数,直接调用工厂函数
|
||||||
self._instances[conf.name] = service_type(conf)
|
self._instances[conf.name] = service_type(conf)
|
||||||
|
|
||||||
|
def get_instances(self) -> Dict[str, TService]:
|
||||||
|
"""
|
||||||
|
获取服务实例列表
|
||||||
|
|
||||||
|
:return: 返回服务实例列表
|
||||||
|
"""
|
||||||
|
return self._instances
|
||||||
|
|
||||||
def get_instance(self, name: str) -> Optional[TService]:
|
def get_instance(self, name: str) -> Optional[TService]:
|
||||||
"""
|
"""
|
||||||
获取服务实例
|
获取服务实例
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ class EmbyModule(_ModuleBase, _MediaServerBase[Emby]):
|
|||||||
"""
|
"""
|
||||||
server: Emby = self.get_instance(server)
|
server: Emby = self.get_instance(server)
|
||||||
if server:
|
if server:
|
||||||
yield from server.get_items(library_id, start_index, limit)
|
return server.get_items(library_id, start_index, limit)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
||||||
|
|||||||
@@ -1093,9 +1093,6 @@ class FileManagerModule(_ModuleBase):
|
|||||||
media_path = dir_path / rel_path.parts[0]
|
media_path = dir_path / rel_path.parts[0]
|
||||||
else:
|
else:
|
||||||
continue
|
continue
|
||||||
# 检查媒体文件夹是否存在
|
|
||||||
if not media_path.exists():
|
|
||||||
continue
|
|
||||||
# 检索媒体文件
|
# 检索媒体文件
|
||||||
fileitem = storage_oper.get_item(media_path)
|
fileitem = storage_oper.get_item(media_path)
|
||||||
if not fileitem:
|
if not fileitem:
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ class JellyfinModule(_ModuleBase, _MediaServerBase[Jellyfin]):
|
|||||||
"""
|
"""
|
||||||
server: Jellyfin = self.get_instance(server)
|
server: Jellyfin = self.get_instance(server)
|
||||||
if server:
|
if server:
|
||||||
yield from server.get_items(library_id, start_index, limit)
|
return server.get_items(library_id, start_index, limit)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
||||||
|
|||||||
@@ -173,7 +173,7 @@ class PlexModule(_ModuleBase, _MediaServerBase[Plex]):
|
|||||||
"""
|
"""
|
||||||
server: Plex = self.get_instance(server)
|
server: Plex = self.get_instance(server)
|
||||||
if server:
|
if server:
|
||||||
yield from server.get_items(library_id, start_index, limit)
|
return server.get_items(library_id, start_index, limit)
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
def mediaserver_iteminfo(self, server: str, item_id: str) -> Optional[schemas.MediaServerItem]:
|
||||||
|
|||||||
+27
-29
@@ -81,43 +81,41 @@ class Subscribe(BaseModel):
|
|||||||
orm_mode = True
|
orm_mode = True
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeDownloadFileInfo(BaseModel):
|
||||||
|
# 种子名称
|
||||||
|
torrent_title: Optional[str] = None
|
||||||
|
# 站点名称
|
||||||
|
site_name: Optional[str] = None
|
||||||
|
# 下载器
|
||||||
|
downloader: Optional[str] = None
|
||||||
|
# hash
|
||||||
|
hash: Optional[str] = None
|
||||||
|
# 文件路径
|
||||||
|
file_path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
|
class SubscribeLibraryFileInfo(BaseModel):
|
||||||
|
# 存储
|
||||||
|
storage: Optional[str] = "local"
|
||||||
|
# 文件路径
|
||||||
|
file_path: Optional[str] = None
|
||||||
|
|
||||||
|
|
||||||
class SubscribeEpisodeInfo(BaseModel):
|
class SubscribeEpisodeInfo(BaseModel):
|
||||||
# 种子地址
|
|
||||||
torrent: Optional[str] = None
|
|
||||||
# 下载文件路程
|
|
||||||
download_file: Optional[str] = None
|
|
||||||
# 媒体库文件路径
|
|
||||||
library_file: Optional[str] = None
|
|
||||||
# 标题
|
# 标题
|
||||||
title: Optional[str] = None
|
title: Optional[str] = None
|
||||||
# 描述
|
# 描述
|
||||||
description: Optional[str] = None
|
description: Optional[str] = None
|
||||||
# 背景图
|
# 背景图
|
||||||
backdrop: Optional[str] = None
|
backdrop: Optional[str] = None
|
||||||
|
# 下载文件信息
|
||||||
|
download: Optional[List[SubscribeDownloadFileInfo]] = []
|
||||||
|
# 媒体库文件信息
|
||||||
|
library: Optional[List[SubscribeLibraryFileInfo]] = []
|
||||||
|
|
||||||
|
|
||||||
class SubscrbieInfo(BaseModel):
|
class SubscrbieInfo(BaseModel):
|
||||||
# 订阅ID
|
# 订阅信息
|
||||||
id: Optional[int] = None
|
subscribe: Optional[Subscribe] = None
|
||||||
# 订阅名称
|
|
||||||
name: Optional[str] = None
|
|
||||||
# 订阅年份
|
|
||||||
year: Optional[str] = None
|
|
||||||
# 订阅类型 电影/电视剧
|
|
||||||
type: Optional[str] = None
|
|
||||||
# 媒体ID
|
|
||||||
tmdbid: Optional[int] = None
|
|
||||||
doubanid: Optional[str] = None
|
|
||||||
bangumiid: Optional[int] = None
|
|
||||||
# 季号
|
|
||||||
season: Optional[int] = None
|
|
||||||
# 海报
|
|
||||||
poster: Optional[str] = None
|
|
||||||
# 背景图
|
|
||||||
backdrop: Optional[str] = None
|
|
||||||
# 评分
|
|
||||||
vote: Optional[int] = 0
|
|
||||||
# 描述
|
|
||||||
description: Optional[str] = None
|
|
||||||
# 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
|
# 集信息 {集号: {download: 文件路径,library: 文件路径, backdrop: url, title: 标题, description: 描述}}
|
||||||
episodes_info: Optional[Dict[int, SubscribeEpisodeInfo]] = {}
|
episodes: Optional[Dict[int, SubscribeEpisodeInfo]] = {}
|
||||||
|
|||||||
+19
-1
@@ -1,8 +1,26 @@
|
|||||||
from typing import Optional
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Any
|
||||||
|
|
||||||
from pydantic import BaseModel
|
from pydantic import BaseModel
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class ServiceInfo:
|
||||||
|
"""
|
||||||
|
封装服务相关信息的数据类
|
||||||
|
"""
|
||||||
|
# 名称
|
||||||
|
name: Optional[str] = None
|
||||||
|
# 实例
|
||||||
|
instance: Optional[Any] = None
|
||||||
|
# 模块
|
||||||
|
module: Optional[Any] = None
|
||||||
|
# 类型
|
||||||
|
type: Optional[str] = None
|
||||||
|
# 配置
|
||||||
|
config: Optional[Any] = None
|
||||||
|
|
||||||
|
|
||||||
class MediaServerConf(BaseModel):
|
class MediaServerConf(BaseModel):
|
||||||
"""
|
"""
|
||||||
媒体服务器配置
|
媒体服务器配置
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ class TorrentStatus(Enum):
|
|||||||
class EventType(Enum):
|
class EventType(Enum):
|
||||||
# 插件需要重载
|
# 插件需要重载
|
||||||
PluginReload = "plugin.reload"
|
PluginReload = "plugin.reload"
|
||||||
# 插件动作
|
# 触发插件动作
|
||||||
PluginAction = "plugin.action"
|
PluginAction = "plugin.action"
|
||||||
|
# 插件触发事件
|
||||||
|
PluginTriggered = "plugin.triggered"
|
||||||
# 执行命令
|
# 执行命令
|
||||||
CommandExcute = "command.excute"
|
CommandExcute = "command.excute"
|
||||||
# 站点已删除
|
# 站点已删除
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
"""2.0.2
|
||||||
|
|
||||||
|
Revision ID: 0fb94bf69b38
|
||||||
|
Revises: 262735d025da
|
||||||
|
Create Date: 2024-09-30 10:03:58.546036
|
||||||
|
|
||||||
|
"""
|
||||||
|
import contextlib
|
||||||
|
|
||||||
|
from alembic import op
|
||||||
|
import sqlalchemy as sa
|
||||||
|
|
||||||
|
|
||||||
|
# revision identifiers, used by Alembic.
|
||||||
|
revision = '0fb94bf69b38'
|
||||||
|
down_revision = '262735d025da'
|
||||||
|
branch_labels = None
|
||||||
|
depends_on = None
|
||||||
|
|
||||||
|
|
||||||
|
def upgrade() -> None:
|
||||||
|
# ### commands auto generated by Alembic - please adjust! ###
|
||||||
|
with contextlib.suppress(Exception):
|
||||||
|
with op.batch_alter_table("siteuserdata") as batch_op:
|
||||||
|
batch_op.add_column(sa.Column('name', sa.VARCHAR))
|
||||||
|
# ### end Alembic commands ###
|
||||||
|
|
||||||
|
|
||||||
|
def downgrade() -> None:
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user