mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-09-05 15:38:19 +08:00
fix apis
This commit is contained in:
+2
-1
@@ -2,7 +2,7 @@ from fastapi import APIRouter
|
|||||||
|
|
||||||
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
from app.api.endpoints import login, user, site, message, webhook, subscribe, \
|
||||||
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
media, douban, search, plugin, tmdb, history, system, download, dashboard, \
|
||||||
transfer, mediaserver, bangumi, storage, discover, recommend, workflow
|
transfer, mediaserver, bangumi, storage, discover, recommend, workflow, torrent
|
||||||
|
|
||||||
api_router = APIRouter()
|
api_router = APIRouter()
|
||||||
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
api_router.include_router(login.router, prefix="/login", tags=["login"])
|
||||||
@@ -27,3 +27,4 @@ api_router.include_router(bangumi.router, prefix="/bangumi", tags=["bangumi"])
|
|||||||
api_router.include_router(discover.router, prefix="/discover", tags=["discover"])
|
api_router.include_router(discover.router, prefix="/discover", tags=["discover"])
|
||||||
api_router.include_router(recommend.router, prefix="/recommend", tags=["recommend"])
|
api_router.include_router(recommend.router, prefix="/recommend", tags=["recommend"])
|
||||||
api_router.include_router(workflow.router, prefix="/workflow", tags=["workflow"])
|
api_router.include_router(workflow.router, prefix="/workflow", tags=["workflow"])
|
||||||
|
api_router.include_router(torrent.router, prefix="/torrent", tags=["torrent"])
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from sqlalchemy.orm import Session
|
|||||||
from starlette.background import BackgroundTasks
|
from starlette.background import BackgroundTasks
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
|
from app.api.endpoints.plugin import register_plugin_api
|
||||||
from app.chain.site import SiteChain
|
from app.chain.site import SiteChain
|
||||||
from app.chain.torrents import TorrentsChain
|
from app.chain.torrents import TorrentsChain
|
||||||
from app.command import Command
|
from app.command import Command
|
||||||
@@ -17,13 +18,13 @@ from app.db.models.site import Site
|
|||||||
from app.db.models.siteicon import SiteIcon
|
from app.db.models.siteicon import SiteIcon
|
||||||
from app.db.models.sitestatistic import SiteStatistic
|
from app.db.models.sitestatistic import SiteStatistic
|
||||||
from app.db.models.siteuserdata import SiteUserData
|
from app.db.models.siteuserdata import SiteUserData
|
||||||
|
from app.db.site_oper import SiteOper
|
||||||
from app.db.systemconfig_oper import SystemConfigOper
|
from app.db.systemconfig_oper import SystemConfigOper
|
||||||
from app.db.user_oper import get_current_active_superuser
|
from app.db.user_oper import get_current_active_superuser
|
||||||
from app.helper.sites import SitesHelper
|
from app.helper.sites import SitesHelper
|
||||||
from app.scheduler import Scheduler
|
from app.scheduler import Scheduler
|
||||||
from app.schemas.types import SystemConfigKey, EventType
|
from app.schemas.types import SystemConfigKey, EventType
|
||||||
from app.utils.string import StringUtils
|
from app.utils.string import StringUtils
|
||||||
from startup.plugins_initializer import register_plugin_api
|
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -395,6 +396,21 @@ def auth_site(
|
|||||||
return schemas.Response(success=status, message=msg)
|
return schemas.Response(success=status, message=msg)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/mapping", summary="获取站点域名到名称的映射", response_model=schemas.Response)
|
||||||
|
def site_mapping(_: User = Depends(get_current_active_superuser)):
|
||||||
|
"""
|
||||||
|
获取站点域名到名称的映射关系
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
sites = SiteOper().list()
|
||||||
|
mapping = {}
|
||||||
|
for site in sites:
|
||||||
|
mapping[site.domain] = site.name
|
||||||
|
return schemas.Response(success=True, data=mapping)
|
||||||
|
except Exception as e:
|
||||||
|
return schemas.Response(success=False, message=f"获取映射失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{site_id}", summary="站点详情", response_model=schemas.Site)
|
@router.get("/{site_id}", summary="站点详情", response_model=schemas.Site)
|
||||||
def read_site(
|
def read_site(
|
||||||
site_id: int,
|
site_id: int,
|
||||||
|
|||||||
+3
-428
@@ -5,14 +5,13 @@ import tempfile
|
|||||||
from collections import deque
|
from collections import deque
|
||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional, Union, Annotated, List
|
from typing import Optional, Union, Annotated
|
||||||
|
|
||||||
import aiofiles
|
import aiofiles
|
||||||
import pillow_avif # noqa 用于自动注册AVIF支持
|
import pillow_avif # noqa 用于自动注册AVIF支持
|
||||||
from PIL import Image
|
from PIL import Image
|
||||||
from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response
|
from fastapi import APIRouter, Depends, HTTPException, Header, Request, Response
|
||||||
from fastapi.responses import StreamingResponse
|
from fastapi.responses import StreamingResponse
|
||||||
from pydantic import BaseModel
|
|
||||||
|
|
||||||
from app import schemas
|
from app import schemas
|
||||||
from app.chain.search import SearchChain
|
from app.chain.search import SearchChain
|
||||||
@@ -30,15 +29,15 @@ from app.helper.progress import ProgressHelper
|
|||||||
from app.helper.rule import RuleHelper
|
from app.helper.rule import RuleHelper
|
||||||
from app.helper.sites import SitesHelper
|
from app.helper.sites import SitesHelper
|
||||||
from app.helper.subscribe import SubscribeHelper
|
from app.helper.subscribe import SubscribeHelper
|
||||||
|
from app.helper.system import SystemHelper
|
||||||
from app.log import logger
|
from app.log import logger
|
||||||
from app.monitor import Monitor
|
from app.monitor import Monitor
|
||||||
from app.scheduler import Scheduler
|
from app.scheduler import Scheduler
|
||||||
from app.schemas.types import SystemConfigKey, MediaType
|
from app.schemas.types import SystemConfigKey
|
||||||
from app.utils.crypto import HashUtils
|
from app.utils.crypto import HashUtils
|
||||||
from app.utils.http import RequestUtils
|
from app.utils.http import RequestUtils
|
||||||
from app.utils.security import SecurityUtils
|
from app.utils.security import SecurityUtils
|
||||||
from app.utils.url import UrlUtils
|
from app.utils.url import UrlUtils
|
||||||
from app.helper.system import SystemHelper
|
|
||||||
from version import APP_VERSION
|
from version import APP_VERSION
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
@@ -519,427 +518,3 @@ def run_scheduler2(jobid: str,
|
|||||||
|
|
||||||
Scheduler().start(jobid)
|
Scheduler().start(jobid)
|
||||||
return schemas.Response(success=True)
|
return schemas.Response(success=True)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/sites/mapping", summary="获取站点域名到名称的映射", response_model=schemas.Response)
|
|
||||||
def get_sites_mapping(_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
获取站点域名到名称的映射关系
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from app.db.site_oper import SiteOper
|
|
||||||
|
|
||||||
site_oper = SiteOper()
|
|
||||||
sites = site_oper.list()
|
|
||||||
|
|
||||||
mapping = {}
|
|
||||||
for site in sites:
|
|
||||||
mapping[site.domain] = site.name
|
|
||||||
|
|
||||||
return schemas.Response(success=True, data=mapping)
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取站点映射失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"获取映射失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/cache/torrents", summary="获取种子缓存", response_model=schemas.Response)
|
|
||||||
def get_torrents_cache(_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
获取当前种子缓存数据
|
|
||||||
"""
|
|
||||||
from app.chain.torrents import TorrentsChain
|
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
|
|
||||||
# 获取spider和rss两种缓存
|
|
||||||
spider_cache = torrents_chain.get_torrents("spider")
|
|
||||||
rss_cache = torrents_chain.get_torrents("rss")
|
|
||||||
|
|
||||||
# 统计信息
|
|
||||||
spider_count = sum(len(torrents) for torrents in spider_cache.values())
|
|
||||||
rss_count = sum(len(torrents) for torrents in rss_cache.values())
|
|
||||||
|
|
||||||
# 转换为前端需要的格式
|
|
||||||
spider_data = []
|
|
||||||
for domain, contexts in spider_cache.items():
|
|
||||||
for context in contexts:
|
|
||||||
torrent_hash = HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}")
|
|
||||||
spider_data.append({
|
|
||||||
"hash": torrent_hash,
|
|
||||||
"domain": domain,
|
|
||||||
"title": context.torrent_info.title,
|
|
||||||
"description": context.torrent_info.description,
|
|
||||||
"size": context.torrent_info.size,
|
|
||||||
"pubdate": context.torrent_info.pubdate,
|
|
||||||
"site_name": context.torrent_info.site_name,
|
|
||||||
"media_name": context.media_info.title if context.media_info else "",
|
|
||||||
"media_year": context.media_info.year if context.media_info else "",
|
|
||||||
"media_type": context.media_info.type if context.media_info else "",
|
|
||||||
"season_episode": context.meta_info.season_episode if context.meta_info else "",
|
|
||||||
"resource_term": context.meta_info.resource_term if context.meta_info else "",
|
|
||||||
"enclosure": context.torrent_info.enclosure,
|
|
||||||
"page_url": context.torrent_info.page_url,
|
|
||||||
"poster_path": context.media_info.get_poster_image() if context.media_info else "",
|
|
||||||
"backdrop_path": context.media_info.get_backdrop_image() if context.media_info else ""
|
|
||||||
})
|
|
||||||
|
|
||||||
rss_data = []
|
|
||||||
for domain, contexts in rss_cache.items():
|
|
||||||
for context in contexts:
|
|
||||||
torrent_hash = HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}")
|
|
||||||
rss_data.append({
|
|
||||||
"hash": torrent_hash,
|
|
||||||
"domain": domain,
|
|
||||||
"title": context.torrent_info.title,
|
|
||||||
"description": context.torrent_info.description,
|
|
||||||
"size": context.torrent_info.size,
|
|
||||||
"pubdate": context.torrent_info.pubdate,
|
|
||||||
"site_name": context.torrent_info.site_name,
|
|
||||||
"media_name": context.media_info.title if context.media_info else "",
|
|
||||||
"media_year": context.media_info.year if context.media_info else "",
|
|
||||||
"media_type": context.media_info.type if context.media_info else "",
|
|
||||||
"season_episode": context.meta_info.season_episode if context.meta_info else "",
|
|
||||||
"resource_term": context.meta_info.resource_term if context.meta_info else "",
|
|
||||||
"enclosure": context.torrent_info.enclosure,
|
|
||||||
"page_url": context.torrent_info.page_url,
|
|
||||||
"poster_path": context.media_info.get_poster_image() if context.media_info else "",
|
|
||||||
"backdrop_path": context.media_info.get_backdrop_image() if context.media_info else ""
|
|
||||||
})
|
|
||||||
|
|
||||||
return schemas.Response(success=True, data={
|
|
||||||
"spider": {
|
|
||||||
"count": spider_count,
|
|
||||||
"sites": len(spider_cache),
|
|
||||||
"data": spider_data
|
|
||||||
},
|
|
||||||
"rss": {
|
|
||||||
"count": rss_count,
|
|
||||||
"sites": len(rss_cache),
|
|
||||||
"data": rss_data
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/cache/torrents/refresh", summary="刷新种子缓存", response_model=schemas.Response)
|
|
||||||
def refresh_torrents_cache(cache_type: str = "auto", _: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
刷新种子缓存
|
|
||||||
:param cache_type: 缓存类型 auto/spider/rss
|
|
||||||
"""
|
|
||||||
from app.chain.torrents import TorrentsChain
|
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if cache_type == "auto":
|
|
||||||
cache_type = None
|
|
||||||
|
|
||||||
result = torrents_chain.refresh(stype=cache_type, sites=None)
|
|
||||||
|
|
||||||
# 统计刷新结果
|
|
||||||
total_count = sum(len(torrents) for torrents in result.values())
|
|
||||||
sites_count = len(result)
|
|
||||||
|
|
||||||
return schemas.Response(success=True, message=f"缓存刷新完成,共刷新 {sites_count} 个站点,{total_count} 个种子")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"刷新种子缓存失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"刷新失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/cache/torrents", summary="清理种子缓存", response_model=schemas.Response)
|
|
||||||
def clear_torrents_cache(_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
清理所有种子缓存
|
|
||||||
"""
|
|
||||||
from app.chain.torrents import TorrentsChain
|
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
|
|
||||||
try:
|
|
||||||
torrents_chain.clear_torrents()
|
|
||||||
return schemas.Response(success=True, message="种子缓存清理完成")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"清理种子缓存失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"清理失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/cache/torrents/stats", summary="获取种子缓存统计", response_model=schemas.Response)
|
|
||||||
def get_torrents_cache_stats(_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
获取种子缓存统计信息
|
|
||||||
"""
|
|
||||||
from app.chain.torrents import TorrentsChain
|
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
|
|
||||||
# 获取缓存配置
|
|
||||||
cache_limit = settings.CACHE_CONF.get("torrents", 100)
|
|
||||||
refresh_limit = settings.CACHE_CONF.get("refresh", 30)
|
|
||||||
|
|
||||||
# 获取缓存数据
|
|
||||||
spider_cache = torrents_chain.get_torrents("spider")
|
|
||||||
rss_cache = torrents_chain.get_torrents("rss")
|
|
||||||
|
|
||||||
# 统计各站点缓存情况
|
|
||||||
spider_stats = []
|
|
||||||
for domain, contexts in spider_cache.items():
|
|
||||||
spider_stats.append({
|
|
||||||
"domain": domain,
|
|
||||||
"count": len(contexts),
|
|
||||||
"latest_date": max([ctx.torrent_info.pubdate for ctx in contexts if ctx.torrent_info.pubdate], default="")
|
|
||||||
})
|
|
||||||
|
|
||||||
rss_stats = []
|
|
||||||
for domain, contexts in rss_cache.items():
|
|
||||||
rss_stats.append({
|
|
||||||
"domain": domain,
|
|
||||||
"count": len(contexts),
|
|
||||||
"latest_date": max([ctx.torrent_info.pubdate for ctx in contexts if ctx.torrent_info.pubdate], default="")
|
|
||||||
})
|
|
||||||
|
|
||||||
return schemas.Response(success=True, data={
|
|
||||||
"config": {
|
|
||||||
"cache_limit": cache_limit,
|
|
||||||
"refresh_limit": refresh_limit,
|
|
||||||
"current_mode": settings.SUBSCRIBE_MODE
|
|
||||||
},
|
|
||||||
"spider": {
|
|
||||||
"total_count": sum(len(torrents) for torrents in spider_cache.values()),
|
|
||||||
"sites_count": len(spider_cache),
|
|
||||||
"sites": spider_stats
|
|
||||||
},
|
|
||||||
"rss": {
|
|
||||||
"total_count": sum(len(torrents) for torrents in rss_cache.values()),
|
|
||||||
"sites_count": len(rss_cache),
|
|
||||||
"sites": rss_stats
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/cache/torrents/{cache_type}/{domain}/{torrent_hash}", summary="删除指定种子缓存", response_model=schemas.Response)
|
|
||||||
def delete_torrent_cache(cache_type: str, domain: str, torrent_hash: str,
|
|
||||||
_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
删除指定的种子缓存
|
|
||||||
:param cache_type: 缓存类型 spider/rss
|
|
||||||
:param domain: 站点域名
|
|
||||||
:param torrent_hash: 种子hash(使用title+description的md5)
|
|
||||||
"""
|
|
||||||
from app.chain.torrents import TorrentsChain
|
|
||||||
from app.utils.crypto import HashUtils
|
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 获取当前缓存
|
|
||||||
cache_data = torrents_chain.get_torrents(cache_type)
|
|
||||||
|
|
||||||
if domain not in cache_data:
|
|
||||||
return schemas.Response(success=False, message=f"站点 {domain} 缓存不存在")
|
|
||||||
|
|
||||||
# 查找并删除指定种子
|
|
||||||
original_count = len(cache_data[domain])
|
|
||||||
cache_data[domain] = [
|
|
||||||
context for context in cache_data[domain]
|
|
||||||
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}") != torrent_hash
|
|
||||||
]
|
|
||||||
|
|
||||||
if len(cache_data[domain]) == original_count:
|
|
||||||
return schemas.Response(success=False, message="未找到指定的种子")
|
|
||||||
|
|
||||||
# 保存更新后的缓存
|
|
||||||
if cache_type == "spider":
|
|
||||||
torrents_chain.save_cache(cache_data, torrents_chain._spider_file)
|
|
||||||
else:
|
|
||||||
torrents_chain.save_cache(cache_data, torrents_chain._rss_file)
|
|
||||||
|
|
||||||
return schemas.Response(success=True, message="种子删除成功")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"删除种子缓存失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"删除失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/cache/torrents/{cache_type}/{domain}/{torrent_hash}/reidentify", summary="重新识别种子", response_model=schemas.Response)
|
|
||||||
def reidentify_torrent_cache(cache_type: str, domain: str, torrent_hash: str,
|
|
||||||
tmdbid: Optional[int] = None, doubanid: Optional[str] = None,
|
|
||||||
_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
重新识别指定的种子
|
|
||||||
:param cache_type: 缓存类型 spider/rss
|
|
||||||
:param domain: 站点域名
|
|
||||||
:param torrent_hash: 种子hash(使用title+description的md5)
|
|
||||||
:param tmdbid: 手动指定的TMDB ID
|
|
||||||
:param doubanid: 手动指定的豆瓣ID
|
|
||||||
"""
|
|
||||||
from app.chain.torrents import TorrentsChain
|
|
||||||
from app.chain.media import MediaChain
|
|
||||||
from app.core.metainfo import MetaInfo
|
|
||||||
from app.core.context import MediaInfo
|
|
||||||
from app.utils.crypto import HashUtils
|
|
||||||
from app.schemas.types import MediaType
|
|
||||||
|
|
||||||
torrents_chain = TorrentsChain()
|
|
||||||
media_chain = MediaChain()
|
|
||||||
|
|
||||||
try:
|
|
||||||
# 获取当前缓存
|
|
||||||
cache_data = torrents_chain.get_torrents(cache_type)
|
|
||||||
|
|
||||||
if domain not in cache_data:
|
|
||||||
return schemas.Response(success=False, message=f"站点 {domain} 缓存不存在")
|
|
||||||
|
|
||||||
# 查找指定种子
|
|
||||||
target_context = None
|
|
||||||
for context in cache_data[domain]:
|
|
||||||
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}") == torrent_hash:
|
|
||||||
target_context = context
|
|
||||||
break
|
|
||||||
|
|
||||||
if not target_context:
|
|
||||||
return schemas.Response(success=False, message="未找到指定的种子")
|
|
||||||
|
|
||||||
# 重新识别
|
|
||||||
if tmdbid or doubanid:
|
|
||||||
# 手动指定媒体信息
|
|
||||||
if tmdbid:
|
|
||||||
# 先尝试电影类型
|
|
||||||
tmdbinfo = media_chain.tmdb_info(tmdbid=tmdbid, mtype=MediaType.MOVIE)
|
|
||||||
if not tmdbinfo:
|
|
||||||
# 再尝试电视剧类型
|
|
||||||
tmdbinfo = media_chain.tmdb_info(tmdbid=tmdbid, mtype=MediaType.TV)
|
|
||||||
|
|
||||||
if tmdbinfo:
|
|
||||||
mediainfo = MediaInfo()
|
|
||||||
mediainfo.set_tmdb_info(tmdbinfo)
|
|
||||||
else:
|
|
||||||
mediainfo = None
|
|
||||||
else:
|
|
||||||
# 先尝试电影类型
|
|
||||||
doubaninfo = media_chain.douban_info(doubanid=doubanid, mtype=MediaType.MOVIE)
|
|
||||||
if not doubaninfo:
|
|
||||||
# 再尝试电视剧类型
|
|
||||||
doubaninfo = media_chain.douban_info(doubanid=doubanid, mtype=MediaType.TV)
|
|
||||||
|
|
||||||
if doubaninfo:
|
|
||||||
mediainfo = MediaInfo()
|
|
||||||
mediainfo.set_douban_info(doubaninfo)
|
|
||||||
else:
|
|
||||||
mediainfo = None
|
|
||||||
else:
|
|
||||||
# 自动重新识别
|
|
||||||
meta = MetaInfo(title=target_context.torrent_info.title,
|
|
||||||
subtitle=target_context.torrent_info.description)
|
|
||||||
mediainfo = media_chain.recognize_by_meta(meta)
|
|
||||||
|
|
||||||
if not mediainfo:
|
|
||||||
# 创建空的媒体信息
|
|
||||||
mediainfo = MediaInfo()
|
|
||||||
else:
|
|
||||||
# 清理多余数据
|
|
||||||
mediainfo.clear()
|
|
||||||
|
|
||||||
# 更新上下文中的媒体信息
|
|
||||||
target_context.media_info = mediainfo
|
|
||||||
|
|
||||||
# 保存更新后的缓存
|
|
||||||
if cache_type == "spider":
|
|
||||||
torrents_chain.save_cache(cache_data, torrents_chain._spider_file)
|
|
||||||
else:
|
|
||||||
torrents_chain.save_cache(cache_data, torrents_chain._rss_file)
|
|
||||||
|
|
||||||
return schemas.Response(success=True, message="重新识别完成", data={
|
|
||||||
"media_name": mediainfo.title if mediainfo else "",
|
|
||||||
"media_year": mediainfo.year if mediainfo else "",
|
|
||||||
"media_type": mediainfo.type.value if mediainfo and mediainfo.type else ""
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"重新识别种子失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"重新识别失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/cache/images/stats", summary="获取图片缓存统计", response_model=schemas.Response)
|
|
||||||
def get_images_cache_stats(_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
获取图片缓存统计信息
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
try:
|
|
||||||
images_cache_path = settings.CACHE_PATH / "images"
|
|
||||||
|
|
||||||
if not images_cache_path.exists():
|
|
||||||
return schemas.Response(success=True, data={
|
|
||||||
"total_files": 0,
|
|
||||||
"total_size": 0,
|
|
||||||
"cache_enabled": settings.GLOBAL_IMAGE_CACHE
|
|
||||||
})
|
|
||||||
|
|
||||||
total_files = 0
|
|
||||||
total_size = 0
|
|
||||||
|
|
||||||
# 递归统计所有图片文件
|
|
||||||
for root, dirs, files in os.walk(images_cache_path):
|
|
||||||
for file in files:
|
|
||||||
file_path = Path(root) / file
|
|
||||||
if file_path.suffix.lower() in settings.SECURITY_IMAGE_SUFFIXES:
|
|
||||||
total_files += 1
|
|
||||||
try:
|
|
||||||
total_size += file_path.stat().st_size
|
|
||||||
except (OSError, IOError):
|
|
||||||
continue
|
|
||||||
|
|
||||||
return schemas.Response(success=True, data={
|
|
||||||
"total_files": total_files,
|
|
||||||
"total_size": total_size,
|
|
||||||
"cache_enabled": settings.GLOBAL_IMAGE_CACHE,
|
|
||||||
"cache_path": str(images_cache_path)
|
|
||||||
})
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"获取图片缓存统计失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"获取统计失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.delete("/cache/images", summary="清理图片缓存", response_model=schemas.Response)
|
|
||||||
def clear_images_cache(_: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
清理所有图片缓存
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from app.utils.system import SystemUtils
|
|
||||||
|
|
||||||
images_cache_path = settings.CACHE_PATH / "images"
|
|
||||||
|
|
||||||
if not images_cache_path.exists():
|
|
||||||
return schemas.Response(success=True, message="图片缓存目录不存在")
|
|
||||||
|
|
||||||
# 清理图片缓存目录
|
|
||||||
cleared_count = SystemUtils.clear(images_cache_path, days=0)
|
|
||||||
|
|
||||||
return schemas.Response(success=True, message=f"图片缓存清理完成,清理了 {cleared_count} 个文件")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"清理图片缓存失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"清理失败:{str(e)}")
|
|
||||||
|
|
||||||
|
|
||||||
@router.post("/cache/images/clean", summary="清理过期图片缓存", response_model=schemas.Response)
|
|
||||||
def clean_expired_images_cache(days: int = 7, _: User = Depends(get_current_active_superuser)):
|
|
||||||
"""
|
|
||||||
清理过期的图片缓存
|
|
||||||
:param days: 保留天数,默认7天
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
from app.utils.system import SystemUtils
|
|
||||||
|
|
||||||
images_cache_path = settings.CACHE_PATH / "images"
|
|
||||||
|
|
||||||
if not images_cache_path.exists():
|
|
||||||
return schemas.Response(success=True, message="图片缓存目录不存在")
|
|
||||||
|
|
||||||
# 清理过期图片缓存
|
|
||||||
cleared_count = SystemUtils.clear(images_cache_path, days=days)
|
|
||||||
|
|
||||||
return schemas.Response(success=True, message=f"过期图片缓存清理完成,清理了 {cleared_count} 个文件")
|
|
||||||
except Exception as e:
|
|
||||||
logger.error(f"清理过期图片缓存失败:{str(e)}")
|
|
||||||
return schemas.Response(success=False, message=f"清理失败:{str(e)}")
|
|
||||||
|
|||||||
@@ -0,0 +1,199 @@
|
|||||||
|
from typing import Optional
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends
|
||||||
|
|
||||||
|
from app import schemas
|
||||||
|
from app.chain.media import MediaChain
|
||||||
|
from app.chain.torrents import TorrentsChain
|
||||||
|
from app.core.config import settings
|
||||||
|
from app.core.context import MediaInfo
|
||||||
|
from app.core.metainfo import MetaInfo
|
||||||
|
from app.db.models import User
|
||||||
|
from app.db.user_oper import get_current_active_superuser
|
||||||
|
from app.utils.crypto import HashUtils
|
||||||
|
|
||||||
|
router = APIRouter()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/cache", summary="获取种子缓存", response_model=schemas.Response)
|
||||||
|
def torrents_cache(_: User = Depends(get_current_active_superuser)):
|
||||||
|
"""
|
||||||
|
获取当前种子缓存数据
|
||||||
|
"""
|
||||||
|
torrents_chain = TorrentsChain()
|
||||||
|
|
||||||
|
# 获取spider和rss两种缓存
|
||||||
|
if settings.SUBSCRIBE_MODE == "rss":
|
||||||
|
cache_info = torrents_chain.get_torrents("rss")
|
||||||
|
else:
|
||||||
|
cache_info = torrents_chain.get_torrents("spider")
|
||||||
|
|
||||||
|
# 统计信息
|
||||||
|
torrent_count = sum(len(torrents) for torrents in cache_info.values())
|
||||||
|
|
||||||
|
# 转换为前端需要的格式
|
||||||
|
torrent_data = []
|
||||||
|
for domain, contexts in cache_info.items():
|
||||||
|
for context in contexts:
|
||||||
|
torrent_hash = HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}")
|
||||||
|
torrent_data.append({
|
||||||
|
"hash": torrent_hash,
|
||||||
|
"domain": domain,
|
||||||
|
"title": context.torrent_info.title,
|
||||||
|
"description": context.torrent_info.description,
|
||||||
|
"size": context.torrent_info.size,
|
||||||
|
"pubdate": context.torrent_info.pubdate,
|
||||||
|
"site_name": context.torrent_info.site_name,
|
||||||
|
"media_name": context.media_info.title if context.media_info else "",
|
||||||
|
"media_year": context.media_info.year if context.media_info else "",
|
||||||
|
"media_type": context.media_info.type if context.media_info else "",
|
||||||
|
"season_episode": context.meta_info.season_episode if context.meta_info else "",
|
||||||
|
"resource_term": context.meta_info.resource_term if context.meta_info else "",
|
||||||
|
"enclosure": context.torrent_info.enclosure,
|
||||||
|
"page_url": context.torrent_info.page_url,
|
||||||
|
"poster_path": context.media_info.get_poster_image() if context.media_info else "",
|
||||||
|
"backdrop_path": context.media_info.get_backdrop_image() if context.media_info else ""
|
||||||
|
})
|
||||||
|
|
||||||
|
return schemas.Response(success=True, data={
|
||||||
|
"count": torrent_count,
|
||||||
|
"sites": len(cache_info),
|
||||||
|
"data": torrent_data
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/cache/{domain}/{torrent_hash}", summary="删除指定种子缓存",
|
||||||
|
response_model=schemas.Response)
|
||||||
|
def delete_cache(domain: str, torrent_hash: str, _: User = Depends(get_current_active_superuser)):
|
||||||
|
"""
|
||||||
|
删除指定的种子缓存
|
||||||
|
:param domain: 站点域名
|
||||||
|
:param torrent_hash: 种子hash(使用title+description的md5)
|
||||||
|
:param _: 当前用户,必须是超级用户
|
||||||
|
"""
|
||||||
|
|
||||||
|
torrents_chain = TorrentsChain()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取当前缓存
|
||||||
|
cache_data = torrents_chain.get_torrents()
|
||||||
|
|
||||||
|
if domain not in cache_data:
|
||||||
|
return schemas.Response(success=False, message=f"站点 {domain} 缓存不存在")
|
||||||
|
|
||||||
|
# 查找并删除指定种子
|
||||||
|
original_count = len(cache_data[domain])
|
||||||
|
cache_data[domain] = [
|
||||||
|
context for context in cache_data[domain]
|
||||||
|
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}") != torrent_hash
|
||||||
|
]
|
||||||
|
|
||||||
|
if len(cache_data[domain]) == original_count:
|
||||||
|
return schemas.Response(success=False, message="未找到指定的种子")
|
||||||
|
|
||||||
|
# 保存更新后的缓存
|
||||||
|
torrents_chain.save_cache(cache_data, torrents_chain.cache_file)
|
||||||
|
|
||||||
|
return schemas.Response(success=True, message="种子删除成功")
|
||||||
|
except Exception as e:
|
||||||
|
return schemas.Response(success=False, message=f"删除失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.delete("/cache", summary="清理种子缓存", response_model=schemas.Response)
|
||||||
|
def clear_cache(_: User = Depends(get_current_active_superuser)):
|
||||||
|
"""
|
||||||
|
清理所有种子缓存
|
||||||
|
"""
|
||||||
|
torrents_chain = TorrentsChain()
|
||||||
|
|
||||||
|
try:
|
||||||
|
torrents_chain.clear_torrents()
|
||||||
|
return schemas.Response(success=True, message="种子缓存清理完成")
|
||||||
|
except Exception as e:
|
||||||
|
return schemas.Response(success=False, message=f"清理失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cache/refresh", summary="刷新种子缓存", response_model=schemas.Response)
|
||||||
|
def refresh_cache(_: User = Depends(get_current_active_superuser)):
|
||||||
|
"""
|
||||||
|
刷新种子缓存
|
||||||
|
"""
|
||||||
|
from app.chain.torrents import TorrentsChain
|
||||||
|
|
||||||
|
torrents_chain = TorrentsChain()
|
||||||
|
|
||||||
|
try:
|
||||||
|
result = torrents_chain.refresh()
|
||||||
|
|
||||||
|
# 统计刷新结果
|
||||||
|
total_count = sum(len(torrents) for torrents in result.values())
|
||||||
|
sites_count = len(result)
|
||||||
|
|
||||||
|
return schemas.Response(success=True, message=f"缓存刷新完成,共刷新 {sites_count} 个站点,{total_count} 个种子")
|
||||||
|
except Exception as e:
|
||||||
|
return schemas.Response(success=False, message=f"刷新失败:{str(e)}")
|
||||||
|
|
||||||
|
|
||||||
|
@router.post("/cache/reidentify/{domain}/{torrent_hash}", summary="重新识别种子", response_model=schemas.Response)
|
||||||
|
def reidentify_cache(domain: str, torrent_hash: str,
|
||||||
|
tmdbid: Optional[int] = None, doubanid: Optional[str] = None,
|
||||||
|
_: User = Depends(get_current_active_superuser)):
|
||||||
|
"""
|
||||||
|
重新识别指定的种子
|
||||||
|
:param domain: 站点域名
|
||||||
|
:param torrent_hash: 种子hash(使用title+description的md5)
|
||||||
|
:param tmdbid: 手动指定的TMDB ID
|
||||||
|
:param doubanid: 手动指定的豆瓣ID
|
||||||
|
:param _: 当前用户,必须是超级用户
|
||||||
|
"""
|
||||||
|
|
||||||
|
torrents_chain = TorrentsChain()
|
||||||
|
media_chain = MediaChain()
|
||||||
|
|
||||||
|
try:
|
||||||
|
# 获取当前缓存
|
||||||
|
cache_data = torrents_chain.get_torrents()
|
||||||
|
|
||||||
|
if domain not in cache_data:
|
||||||
|
return schemas.Response(success=False, message=f"站点 {domain} 缓存不存在")
|
||||||
|
|
||||||
|
# 查找指定种子
|
||||||
|
target_context = None
|
||||||
|
for context in cache_data[domain]:
|
||||||
|
if HashUtils.md5(f"{context.torrent_info.title}{context.torrent_info.description}") == torrent_hash:
|
||||||
|
target_context = context
|
||||||
|
break
|
||||||
|
|
||||||
|
if not target_context:
|
||||||
|
return schemas.Response(success=False, message="未找到指定的种子")
|
||||||
|
|
||||||
|
# 重新识别
|
||||||
|
meta = MetaInfo(title=target_context.torrent_info.title,
|
||||||
|
subtitle=target_context.torrent_info.description)
|
||||||
|
if tmdbid or doubanid:
|
||||||
|
# 手动指定媒体信息
|
||||||
|
mediainfo = MediaChain().recognize_media(meta=meta, tmdbid=tmdbid, doubanid=doubanid)
|
||||||
|
else:
|
||||||
|
# 自动重新识别
|
||||||
|
mediainfo = media_chain.recognize_by_meta(meta)
|
||||||
|
|
||||||
|
if not mediainfo:
|
||||||
|
# 创建空的媒体信息
|
||||||
|
mediainfo = MediaInfo()
|
||||||
|
else:
|
||||||
|
# 清理多余数据
|
||||||
|
mediainfo.clear()
|
||||||
|
|
||||||
|
# 更新上下文中的媒体信息
|
||||||
|
target_context.media_info = mediainfo
|
||||||
|
|
||||||
|
# 保存更新后的缓存
|
||||||
|
torrents_chain.save_cache(cache_data, TorrentsChain().cache_file)
|
||||||
|
|
||||||
|
return schemas.Response(success=True, message="重新识别完成", data={
|
||||||
|
"media_name": mediainfo.title if mediainfo else "",
|
||||||
|
"media_year": mediainfo.year if mediainfo else "",
|
||||||
|
"media_type": mediainfo.type.value if mediainfo and mediainfo.type else ""
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
return schemas.Response(success=False, message=f"重新识别失败:{str(e)}")
|
||||||
@@ -38,6 +38,15 @@ class TorrentsChain(ChainBase, metaclass=Singleton):
|
|||||||
self.mediachain = MediaChain()
|
self.mediachain = MediaChain()
|
||||||
self.torrenthelper = TorrentHelper()
|
self.torrenthelper = TorrentHelper()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def cache_file(self) -> str:
|
||||||
|
"""
|
||||||
|
返回缓存文件列表
|
||||||
|
"""
|
||||||
|
if settings.SUBSCRIBE_MODE == 'spider':
|
||||||
|
return self._spider_file
|
||||||
|
return self._rss_file
|
||||||
|
|
||||||
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
|
def remote_refresh(self, channel: MessageChannel, userid: Union[str, int] = None):
|
||||||
"""
|
"""
|
||||||
远程刷新订阅,发送消息
|
远程刷新订阅,发送消息
|
||||||
|
|||||||
Reference in New Issue
Block a user