mirror of
https://github.com/jxxghp/MoviePilot-Frontend.git
synced 2026-05-12 02:21:06 +08:00
缓存管理页面
This commit is contained in:
@@ -40,6 +40,10 @@ export default {
|
||||
media: 'Media',
|
||||
unknown: 'Unknown',
|
||||
notice: 'Notice',
|
||||
itemsPerPage: 'Items per page',
|
||||
pageText: '{0}-{1} of {2}',
|
||||
noDataText: 'No data',
|
||||
loadingText: 'Loading...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
@@ -1109,7 +1113,7 @@ export default {
|
||||
},
|
||||
site: {
|
||||
siteSync: 'Site Synchronization',
|
||||
siteSyncDesc: 'Quickly sync site data from CookieCloud.',
|
||||
siteSyncDesc: 'Quickly sync site data from CookieCloud',
|
||||
enableLocalCookieCloud: 'Enable Local CookieCloud Server',
|
||||
enableLocalCookieCloudHint:
|
||||
'Use built-in CookieCloud service to sync site data, service address: http://localhost:3000/cookiecloud',
|
||||
@@ -1155,7 +1159,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: 'Notification Channels',
|
||||
channelsDesc: 'Set message sending channel parameters.',
|
||||
channelsDesc: 'Set message sending channel parameters',
|
||||
organizeSuccess: 'Media Import',
|
||||
downloadAdded: 'Download Added',
|
||||
subscribeAdded: 'Subscribe Added',
|
||||
@@ -1202,7 +1206,7 @@ export default {
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: 'Custom Identifiers',
|
||||
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification.',
|
||||
identifiersDesc: 'Add rules to preprocess torrent names or file names to correct identification',
|
||||
identifiersPlaceholder: 'Support regular expressions, special characters need \\ escape, one line for each rule',
|
||||
identifiersHint: 'Support regular expressions, special characters need \\ escape, one line for each rule',
|
||||
formatTitle: 'Supported configuration formats (mind the spaces):',
|
||||
@@ -1242,7 +1246,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: 'Basic Settings',
|
||||
basicSettingsDesc: 'Set data sources, rule groups and other basic information.',
|
||||
basicSettingsDesc: 'Set data sources, rule groups and other basic information',
|
||||
recognizeSource: 'Recognition Data Source',
|
||||
recognizeSourceDesc:
|
||||
'Default is TMDB. Douban is usually more friendly for Chinese works, but some foreign works have incomplete information.',
|
||||
@@ -1343,8 +1347,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
title: 'Scheduled Jobs',
|
||||
subtitle:
|
||||
"Includes built-in system services and plugin services. Manual execution will not affect the job's normal schedule.",
|
||||
subtitle: 'Includes built-in system services and plugin services',
|
||||
provider: 'Provider',
|
||||
taskName: 'Task Name',
|
||||
taskStatus: 'Task Status',
|
||||
@@ -1391,6 +1394,52 @@ export default {
|
||||
settingsSaveSuccess: 'Subscription basic settings saved successfully',
|
||||
settingsSaveFailed: 'Failed to save subscription basic settings!',
|
||||
},
|
||||
cache: {
|
||||
title: 'Cache',
|
||||
description: 'Site cache and media recognition data cache management',
|
||||
subtitle: 'Manage cached site resources',
|
||||
refresh: 'Refresh Cache',
|
||||
deleteSelected: 'Delete Selected',
|
||||
clearAll: 'Clear All Cache',
|
||||
refreshSuccess: 'Cache refresh completed',
|
||||
refreshFailed: 'Failed to refresh cache',
|
||||
clearSuccess: 'Cache clear completed',
|
||||
clearFailed: 'Failed to clear cache',
|
||||
deleteSuccess: 'Cache item deleted successfully',
|
||||
deleteFailed: 'Failed to delete cache item',
|
||||
deleteSelectedSuccess: 'Successfully deleted {count} cache items',
|
||||
deleteSelectedFailed: 'Failed to delete cache items',
|
||||
loadFailed: 'Failed to load cache data',
|
||||
selectDeleteWarning: 'Please select cache items to delete',
|
||||
reidentify: 'Re-identify',
|
||||
reidentifySuccess: 'Re-identification completed',
|
||||
reidentifyFailed: 'Re-identification failed',
|
||||
poster: 'Poster',
|
||||
torrentTitle: 'Title',
|
||||
site: 'Site',
|
||||
size: 'Size',
|
||||
publishTime: 'Publish Time',
|
||||
recognitionResult: 'Recognition Result',
|
||||
actions: 'Actions',
|
||||
unrecognized: 'Unrecognized',
|
||||
noData: 'No cache data',
|
||||
noDataHint: 'Click "Refresh Cache" button to get the latest torrent cache',
|
||||
reidentifyDialog: {
|
||||
title: 'Re-identify',
|
||||
torrentInfo: 'Torrent Info',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: 'Optional, manually specify TMDB ID for recognition',
|
||||
doubanId: 'Douban ID',
|
||||
doubanIdHint: 'Optional, manually specify Douban ID for recognition',
|
||||
autoHint: 'If no ID is specified, the torrent will be automatically re-identified',
|
||||
cancel: 'Cancel',
|
||||
confirm: 'Re-identify',
|
||||
},
|
||||
mediaType: {
|
||||
movie: 'Movie',
|
||||
tv: 'TV Show',
|
||||
},
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
media: '媒体',
|
||||
unknown: '未知',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每页条数',
|
||||
pageText: '{0}-{1} 共 {2} 条',
|
||||
noDataText: '没有数据',
|
||||
loadingText: '加载中...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
@@ -941,7 +945,7 @@ export default {
|
||||
system: {
|
||||
custom: '自定义',
|
||||
basicSettings: '基础设置',
|
||||
basicSettingsDesc: '设置服务器的全局功能。',
|
||||
basicSettingsDesc: '设置服务器的全局功能',
|
||||
appDomain: '访问域名',
|
||||
appDomainHint: '用于发送通知时,添加快捷跳转地址',
|
||||
wallpaper: '背景壁纸',
|
||||
@@ -1099,7 +1103,7 @@ export default {
|
||||
},
|
||||
site: {
|
||||
siteSync: '站点同步',
|
||||
siteSyncDesc: '从CookieCloud快速同步站点数据。',
|
||||
siteSyncDesc: '从CookieCloud快速同步站点数据',
|
||||
enableLocalCookieCloud: '启用本地CookieCloud服务器',
|
||||
enableLocalCookieCloudHint: '使用内建CookieCloud服务同步站点数据,服务地址为:http://localhost:3000/cookiecloud',
|
||||
serviceAddress: '服务地址',
|
||||
@@ -1142,7 +1146,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: '通知渠道',
|
||||
channelsDesc: '设置消息发送渠道参数。',
|
||||
channelsDesc: '设置消息发送渠道参数',
|
||||
organizeSuccess: '资源入库',
|
||||
downloadAdded: '资源下载',
|
||||
subscribeAdded: '添加订阅',
|
||||
@@ -1189,7 +1193,7 @@ export default {
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: '自定义识别词',
|
||||
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别。',
|
||||
identifiersDesc: '添加规则对种子名或者文件名进行预处理以校正识别',
|
||||
identifiersPlaceholder: '支持正则表达式,特殊字符需要\\转义,一行为一组',
|
||||
identifiersHint: '支持正则表达式,特殊字符需要\\转义,一行为一组',
|
||||
formatTitle: '支持的配置格式(注意空格):',
|
||||
@@ -1225,7 +1229,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: '基础设置',
|
||||
basicSettingsDesc: '设定数据源、规则组等基础信息。',
|
||||
basicSettingsDesc: '设定数据源、规则组等基础信息',
|
||||
recognizeSource: '识别数据源',
|
||||
recognizeSourceDesc: '默认使用TMDB。豆瓣识别中文作品通常更友好,但有些国外作品信息不完整。',
|
||||
themoviedb: 'TheMovieDb',
|
||||
@@ -1261,7 +1265,7 @@ export default {
|
||||
},
|
||||
directory: {
|
||||
storage: '存储',
|
||||
storageDesc: '设置本地或网盘存储。',
|
||||
storageDesc: '设置本地或网盘存储',
|
||||
directory: '目录',
|
||||
mediaType: '媒体类型',
|
||||
directoryDesc: '设置媒体文件整理目录结构,按先后顺序依次匹配。',
|
||||
@@ -1323,7 +1327,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
title: '定时作业',
|
||||
subtitle: '包含系统内置服务以及插件提供的服务,手动执行不会影响作业正常的时间表。',
|
||||
subtitle: '包含系统内置服务以及插件提供的服务',
|
||||
provider: '提供者',
|
||||
taskName: '任务名称',
|
||||
taskStatus: '任务状态',
|
||||
@@ -1373,6 +1377,48 @@ export default {
|
||||
cache: {
|
||||
title: '缓存',
|
||||
description: '种子缓存、图片文件缓存管理',
|
||||
subtitle: '管理缓存的站点资源',
|
||||
refresh: '刷新缓存',
|
||||
deleteSelected: '删除选中',
|
||||
clearAll: '清空缓存',
|
||||
refreshSuccess: '缓存刷新完成',
|
||||
refreshFailed: '刷新缓存失败',
|
||||
clearSuccess: '缓存清理完成',
|
||||
clearFailed: '清理缓存失败',
|
||||
deleteSuccess: '缓存项删除成功',
|
||||
deleteFailed: '删除缓存项失败',
|
||||
deleteSelectedSuccess: '成功删除 {count} 个缓存项',
|
||||
deleteSelectedFailed: '删除缓存项失败',
|
||||
loadFailed: '加载缓存数据失败',
|
||||
selectDeleteWarning: '请选择要删除的缓存项',
|
||||
reidentify: '重新识别',
|
||||
reidentifySuccess: '重新识别完成',
|
||||
reidentifyFailed: '重新识别失败',
|
||||
poster: '海报',
|
||||
torrentTitle: '标题',
|
||||
site: '站点',
|
||||
size: '大小',
|
||||
publishTime: '发布时间',
|
||||
recognitionResult: '识别结果',
|
||||
actions: '操作',
|
||||
unrecognized: '未识别',
|
||||
noData: '暂无缓存数据',
|
||||
noDataHint: '点击"刷新缓存"按钮获取最新的种子缓存',
|
||||
reidentifyDialog: {
|
||||
title: '重新识别',
|
||||
torrentInfo: '种子信息',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: '可选,手动指定TMDB ID进行识别',
|
||||
doubanId: '豆瓣 ID',
|
||||
doubanIdHint: '可选,手动指定豆瓣ID进行识别',
|
||||
autoHint: '如果不指定ID,将自动重新识别该种子',
|
||||
cancel: '取消',
|
||||
confirm: '重新识别',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '电影',
|
||||
tv: '电视剧',
|
||||
},
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
|
||||
@@ -40,6 +40,10 @@ export default {
|
||||
media: '媒體',
|
||||
unknown: '未知',
|
||||
notice: '注意',
|
||||
itemsPerPage: '每頁條數',
|
||||
pageText: '{0}-{1} 共 {2} 條',
|
||||
noDataText: '沒有數據',
|
||||
loadingText: '加載中...',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
@@ -943,7 +947,7 @@ export default {
|
||||
system: {
|
||||
custom: '自定義',
|
||||
basicSettings: '基礎設置',
|
||||
basicSettingsDesc: '設置服務器的全局功能。',
|
||||
basicSettingsDesc: '設置服務器的全局功能',
|
||||
appDomain: '訪問域名',
|
||||
appDomainHint: '用於發送通知時,添加快捷跳轉地址',
|
||||
wallpaper: '背景壁紙',
|
||||
@@ -1101,7 +1105,7 @@ export default {
|
||||
},
|
||||
site: {
|
||||
siteSync: '站點同步',
|
||||
siteSyncDesc: '從CookieCloud快速同步站點數據。',
|
||||
siteSyncDesc: '從CookieCloud快速同步站點數據',
|
||||
enableLocalCookieCloud: '啟用本地CookieCloud服務器',
|
||||
enableLocalCookieCloudHint: '使用內建CookieCloud服務同步站點數據,服務地址為:http://localhost:3000/cookiecloud',
|
||||
serviceAddress: '服務地址',
|
||||
@@ -1144,7 +1148,7 @@ export default {
|
||||
},
|
||||
notification: {
|
||||
channels: '通知渠道',
|
||||
channelsDesc: '設置消息發送渠道參數。',
|
||||
channelsDesc: '設置消息發送渠道參數',
|
||||
organizeSuccess: '資源入庫',
|
||||
downloadAdded: '資源下載',
|
||||
subscribeAdded: '添加訂閱',
|
||||
@@ -1191,7 +1195,7 @@ export default {
|
||||
},
|
||||
words: {
|
||||
customIdentifiers: '自定義識別詞',
|
||||
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別。',
|
||||
identifiersDesc: '添加規則對種子名或者文件名進行預處理以校正識別',
|
||||
identifiersPlaceholder: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
|
||||
identifiersHint: '支持正則表達式,特殊字符需要\\轉義,一行為一組',
|
||||
formatTitle: '支持的配置格式(注意空格):',
|
||||
@@ -1227,7 +1231,7 @@ export default {
|
||||
},
|
||||
search: {
|
||||
basicSettings: '基礎設置',
|
||||
basicSettingsDesc: '設定數據源、規則組等基礎信息。',
|
||||
basicSettingsDesc: '設定數據源、規則組等基礎信息',
|
||||
recognizeSource: '識別數據源',
|
||||
recognizeSourceDesc: '默認使用TMDB。豆瓣識別中文作品通常更友好,但有些國外作品信息不完整。',
|
||||
themoviedb: 'TheMovieDb',
|
||||
@@ -1263,7 +1267,7 @@ export default {
|
||||
},
|
||||
directory: {
|
||||
storage: '存儲',
|
||||
storageDesc: '設置本地或網盤存儲。',
|
||||
storageDesc: '設置本地或網盤存儲',
|
||||
directory: '目錄',
|
||||
directoryDesc: '設置媒體文件整理目錄結構,按先後順序依次匹配。',
|
||||
organizeAndScrap: '整理 & 刮削',
|
||||
@@ -1324,7 +1328,7 @@ export default {
|
||||
},
|
||||
scheduler: {
|
||||
scheduledTasks: '定時作業',
|
||||
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務,手動執行不會影響作業正常的時間表。',
|
||||
scheduledTasksDesc: '包含系統內置服務以及插件提供的服務',
|
||||
provider: '提供者',
|
||||
taskName: '任務名稱',
|
||||
taskStatus: '任務狀態',
|
||||
@@ -1371,6 +1375,52 @@ export default {
|
||||
settingsSaveSuccess: '訂閱基礎設置保存成功',
|
||||
settingsSaveFailed: '訂閱基礎設置保存失敗!',
|
||||
},
|
||||
cache: {
|
||||
title: '緩存',
|
||||
description: '種子緩存、圖片文件緩存管理',
|
||||
subtitle: '管理緩存的站點資源',
|
||||
refresh: '刷新緩存',
|
||||
deleteSelected: '刪除選中',
|
||||
clearAll: '清空緩存',
|
||||
refreshSuccess: '緩存刷新完成',
|
||||
refreshFailed: '刷新緩存失敗',
|
||||
clearSuccess: '緩存清理完成',
|
||||
clearFailed: '清理緩存失敗',
|
||||
deleteSuccess: '緩存項刪除成功',
|
||||
deleteFailed: '刪除緩存項失敗',
|
||||
deleteSelectedSuccess: '成功刪除 {count} 個緩存項',
|
||||
deleteSelectedFailed: '刪除緩存項失敗',
|
||||
loadFailed: '加載緩存數據失敗',
|
||||
selectDeleteWarning: '請選擇要刪除的緩存項',
|
||||
reidentify: '重新識別',
|
||||
reidentifySuccess: '重新識別完成',
|
||||
reidentifyFailed: '重新識別失敗',
|
||||
poster: '海報',
|
||||
torrentTitle: '標題',
|
||||
site: '站點',
|
||||
size: '大小',
|
||||
publishTime: '發布時間',
|
||||
recognitionResult: '識別結果',
|
||||
actions: '操作',
|
||||
unrecognized: '未識別',
|
||||
noData: '暫無緩存數據',
|
||||
noDataHint: '點擊"刷新緩存"按鈕獲取最新的種子緩存',
|
||||
reidentifyDialog: {
|
||||
title: '重新識別',
|
||||
torrentInfo: '種子信息',
|
||||
tmdbId: 'TMDB ID',
|
||||
tmdbIdHint: '可選,手動指定TMDB ID進行識別',
|
||||
doubanId: '豆瓣 ID',
|
||||
doubanIdHint: '可選,手動指定豆瓣ID進行識別',
|
||||
autoHint: '如果不指定ID,將自動重新識別該種子',
|
||||
cancel: '取消',
|
||||
confirm: '重新識別',
|
||||
},
|
||||
mediaType: {
|
||||
movie: '電影',
|
||||
tv: '電視劇',
|
||||
},
|
||||
},
|
||||
},
|
||||
dialog: {
|
||||
progress: {
|
||||
|
||||
@@ -1,199 +0,0 @@
|
||||
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)}")
|
||||
@@ -0,0 +1,388 @@
|
||||
<script lang="ts" setup>
|
||||
import { useToast } from 'vue-toast-notification'
|
||||
import api from '@/api'
|
||||
import type { TorrentCacheData, TorrentCacheItem } from '@/api/types'
|
||||
import { useI18n } from 'vue-i18n'
|
||||
import { useDisplay } from 'vuetify'
|
||||
|
||||
// 国际化
|
||||
const { t } = useI18n()
|
||||
|
||||
const display = useDisplay()
|
||||
|
||||
// 提示框
|
||||
const $toast = useToast()
|
||||
|
||||
// 缓存数据
|
||||
const cacheData = ref<TorrentCacheData>({
|
||||
count: 0,
|
||||
sites: 0,
|
||||
data: [],
|
||||
})
|
||||
|
||||
// 选中的缓存项
|
||||
const selectedItems = ref<string[]>([])
|
||||
|
||||
// 加载状态
|
||||
const loading = ref(false)
|
||||
|
||||
// 重新识别对话框
|
||||
const reidentifyDialog = ref(false)
|
||||
const currentReidentifyItem = ref<TorrentCacheItem | null>(null)
|
||||
const tmdbId = ref<number | undefined>()
|
||||
const doubanId = ref<string | undefined>()
|
||||
|
||||
// 调用API加载缓存数据
|
||||
async function loadCacheData() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.get('torrent/cache')
|
||||
cacheData.value = res.data
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.loadFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 清空所有缓存
|
||||
async function clearAllCache() {
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete('torrent/cache')
|
||||
$toast.success(t('setting.cache.clearSuccess'))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.clearFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 刷新缓存
|
||||
async function refreshCache() {
|
||||
try {
|
||||
loading.value = true
|
||||
const res: any = await api.post('torrent/cache/refresh')
|
||||
$toast.success(res.message || t('setting.cache.refreshSuccess'))
|
||||
await loadCacheData()
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.refreshFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除选中的缓存项
|
||||
async function deleteSelectedItems() {
|
||||
if (selectedItems.value.length === 0) {
|
||||
$toast.warning(t('setting.cache.selectDeleteWarning'))
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const deletePromises = selectedItems.value.map(hash => {
|
||||
const item = cacheData.value.data.find(d => d.hash === hash)
|
||||
if (item) {
|
||||
return api.delete(`torrent/cache/${item.domain}/${hash}`)
|
||||
}
|
||||
return Promise.resolve()
|
||||
})
|
||||
|
||||
await Promise.all(deletePromises)
|
||||
$toast.success(t('setting.cache.deleteSelectedSuccess', { count: selectedItems.value.length }))
|
||||
await loadCacheData()
|
||||
selectedItems.value = []
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteSelectedFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 删除单个缓存项
|
||||
async function deleteSingleItem(item: TorrentCacheItem) {
|
||||
try {
|
||||
loading.value = true
|
||||
await api.delete(`torrent/cache/${item.domain}/${item.hash}`)
|
||||
$toast.success(t('setting.cache.deleteSuccess'))
|
||||
await loadCacheData()
|
||||
// 从选中列表中移除
|
||||
const index = selectedItems.value.indexOf(item.hash)
|
||||
if (index > -1) {
|
||||
selectedItems.value.splice(index, 1)
|
||||
}
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.deleteFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 打开重新识别对话框
|
||||
function openReidentifyDialog(item: TorrentCacheItem) {
|
||||
currentReidentifyItem.value = item
|
||||
tmdbId.value = undefined
|
||||
doubanId.value = undefined
|
||||
reidentifyDialog.value = true
|
||||
}
|
||||
|
||||
// 重新识别
|
||||
async function performReidentify() {
|
||||
if (!currentReidentifyItem.value) return
|
||||
|
||||
try {
|
||||
loading.value = true
|
||||
const params: any = {}
|
||||
if (tmdbId.value) params.tmdbid = tmdbId.value
|
||||
if (doubanId.value) params.doubanid = doubanId.value
|
||||
|
||||
const res: any = await api.post(
|
||||
`torrent/cache/reidentify/${currentReidentifyItem.value.domain}/${currentReidentifyItem.value.hash}`,
|
||||
null,
|
||||
{
|
||||
params,
|
||||
},
|
||||
)
|
||||
|
||||
$toast.success(res.message || t('setting.cache.reidentifySuccess'))
|
||||
await loadCacheData()
|
||||
reidentifyDialog.value = false
|
||||
} catch (e) {
|
||||
console.log(e)
|
||||
$toast.error(t('setting.cache.reidentifyFailed'))
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// 格式化文件大小
|
||||
function formatFileSize(bytes: number): string {
|
||||
if (bytes === 0) return '0 B'
|
||||
const k = 1024
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB']
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
|
||||
}
|
||||
|
||||
// 格式化日期
|
||||
function formatDate(dateStr: string): string {
|
||||
if (!dateStr) return ''
|
||||
return new Date(dateStr).toLocaleString()
|
||||
}
|
||||
|
||||
// 获取媒体类型颜色
|
||||
function getMediaTypeColor(type: string): string {
|
||||
switch (type) {
|
||||
case t('setting.cache.mediaType.movie'):
|
||||
return 'primary'
|
||||
case t('setting.cache.mediaType.tv'):
|
||||
return 'success'
|
||||
default:
|
||||
return 'default'
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
loadCacheData()
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<VCard>
|
||||
<VCardItem>
|
||||
<VCardTitle>{{ t('setting.cache.title') }}</VCardTitle>
|
||||
<VCardSubtitle>{{ t('setting.cache.subtitle') }}</VCardSubtitle>
|
||||
|
||||
<template #append>
|
||||
<div class="d-flex gap-2">
|
||||
<VBtn icon color="primary" :loading="loading" @click="refreshCache">
|
||||
<VIcon>mdi-refresh</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.refresh') }}</VTooltip>
|
||||
</VBtn>
|
||||
|
||||
<VBtn
|
||||
icon
|
||||
color="warning"
|
||||
:loading="loading"
|
||||
:disabled="selectedItems.length === 0"
|
||||
@click="deleteSelectedItems"
|
||||
>
|
||||
<VIcon>mdi-delete</VIcon>
|
||||
<VTooltip activator="parent" location="bottom"
|
||||
>{{ t('setting.cache.deleteSelected') }} ({{ selectedItems.length }})</VTooltip
|
||||
>
|
||||
</VBtn>
|
||||
|
||||
<VBtn icon color="error" :loading="loading" @click="clearAllCache">
|
||||
<VIcon>mdi-delete-sweep</VIcon>
|
||||
<VTooltip activator="parent" location="bottom">{{ t('setting.cache.clearAll') }}</VTooltip>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
</VCardItem>
|
||||
|
||||
<!-- 缓存列表 -->
|
||||
<VDataTable
|
||||
v-model="selectedItems"
|
||||
:headers="[
|
||||
{ title: '', key: 'data-table-select', sortable: false, width: '48px' },
|
||||
{ title: t('setting.cache.poster'), key: 'poster', sortable: false, width: '80px' },
|
||||
{ title: t('setting.cache.torrentTitle'), key: 'title', sortable: true },
|
||||
{ title: t('setting.cache.site'), key: 'site_name', sortable: true, width: '120px' },
|
||||
{ title: t('setting.cache.size'), key: 'size', sortable: true, width: '100px' },
|
||||
{ title: t('setting.cache.publishTime'), key: 'pubdate', sortable: true, width: '150px' },
|
||||
{ title: t('setting.cache.recognitionResult'), key: 'media_info', sortable: false, width: '200px' },
|
||||
{ title: t('setting.cache.actions'), key: 'actions', sortable: false, width: '150px' },
|
||||
]"
|
||||
:items="cacheData.data"
|
||||
:loading="loading"
|
||||
item-value="hash"
|
||||
show-select
|
||||
class="text-no-wrap"
|
||||
:items-per-page-text="t('common.itemsPerPage')"
|
||||
:no-data-text="t('common.noDataText')"
|
||||
:loading-text="t('common.loadingText')"
|
||||
>
|
||||
<!-- 全选复选框 -->
|
||||
<template #header.data-table-select="{ allSelected, selectAll, someSelected }">
|
||||
<VCheckbox
|
||||
:indeterminate="someSelected && !allSelected"
|
||||
:model-value="allSelected"
|
||||
@update:model-value="(value: boolean | null) => selectAll(value as boolean)"
|
||||
/>
|
||||
</template>
|
||||
|
||||
<!-- 海报列 -->
|
||||
<template #item.poster="{ item }">
|
||||
<VAvatar size="60" rounded="lg" class="ma-1">
|
||||
<VImg v-if="item.poster_path" :src="item.poster_path" :alt="item.media_name || item.title" cover />
|
||||
<VIcon v-else size="30" color="grey-lighten-1"> mdi-movie-open </VIcon>
|
||||
</VAvatar>
|
||||
</template>
|
||||
|
||||
<!-- 标题列 -->
|
||||
<template #item.title="{ item }">
|
||||
<div class="d-flex flex-column">
|
||||
<div class="text-subtitle-2 font-weight-bold">
|
||||
{{ item.title }}
|
||||
</div>
|
||||
<div v-if="item.description" class="text-caption text-grey">
|
||||
{{ item.description }}
|
||||
</div>
|
||||
<div v-if="item.season_episode || item.resource_term" class="text-caption text-primary mt-1">
|
||||
{{ item.season_episode }} {{ item.resource_term }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 大小列 -->
|
||||
<template #item.size="{ item }">
|
||||
{{ formatFileSize(item.size) }}
|
||||
</template>
|
||||
|
||||
<!-- 发布时间列 -->
|
||||
<template #item.pubdate="{ item }">
|
||||
{{ formatDate(item.pubdate || '') }}
|
||||
</template>
|
||||
|
||||
<!-- 识别结果列 -->
|
||||
<template #item.media_info="{ item }">
|
||||
<div v-if="item.media_name" class="d-flex flex-column">
|
||||
<div class="text-subtitle-2">
|
||||
{{ item.media_name }}
|
||||
<VChip v-if="item.media_type" :color="getMediaTypeColor(item.media_type)" size="x-small" class="ml-1">
|
||||
{{ item.media_type }}
|
||||
</VChip>
|
||||
</div>
|
||||
<div v-if="item.media_year" class="text-caption text-grey">
|
||||
{{ item.media_year }}
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-caption text-grey">
|
||||
{{ t('setting.cache.unrecognized') }}
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 操作列 -->
|
||||
<template #item.actions="{ item }">
|
||||
<div class="d-flex gap-1">
|
||||
<VBtn size="small" color="primary" variant="text" @click="openReidentifyDialog(item)">
|
||||
<VIcon size="16">mdi-refresh</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn size="small" color="error" variant="text" @click="deleteSingleItem(item)">
|
||||
<VIcon size="16">mdi-delete</VIcon>
|
||||
</VBtn>
|
||||
|
||||
<VBtn v-if="item.page_url" size="small" color="info" variant="text" :href="item.page_url" target="_blank">
|
||||
<VIcon size="16">mdi-open-in-new</VIcon>
|
||||
</VBtn>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<!-- 空状态 -->
|
||||
<template #no-data>
|
||||
<div class="text-center pa-4">
|
||||
<VIcon size="64" color="grey-lighten-2" class="mb-4"> mdi-database-off </VIcon>
|
||||
<div class="text-h6 text-grey">{{ t('setting.cache.noData') }}</div>
|
||||
<div class="text-body-2 text-grey">
|
||||
{{ t('setting.cache.noDataHint') }}
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</VDataTable>
|
||||
</VCard>
|
||||
|
||||
<!-- 重新识别对话框 -->
|
||||
<VDialog v-model="reidentifyDialog" scrollable max-width="40rem" :fullscreen="!display.mdAndUp.value">
|
||||
<VCard>
|
||||
<VCardTitle>{{ t('setting.cache.reidentifyDialog.title') }}</VCardTitle>
|
||||
|
||||
<VCardText>
|
||||
<div class="mb-4">
|
||||
<div class="text-subtitle-2 mb-2">{{ t('setting.cache.reidentifyDialog.torrentInfo') }}</div>
|
||||
<div class="text-body-2">{{ currentReidentifyItem?.title }}</div>
|
||||
<div class="text-caption text-grey">{{ currentReidentifyItem?.site_name }}</div>
|
||||
</div>
|
||||
|
||||
<VTextField
|
||||
v-model="tmdbId"
|
||||
:label="t('setting.cache.reidentifyDialog.tmdbId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.tmdbIdHint')"
|
||||
type="number"
|
||||
clearable
|
||||
/>
|
||||
|
||||
<VTextField
|
||||
v-model="doubanId"
|
||||
:label="t('setting.cache.reidentifyDialog.doubanId')"
|
||||
:hint="t('setting.cache.reidentifyDialog.doubanIdHint')"
|
||||
clearable
|
||||
class="mt-4"
|
||||
/>
|
||||
|
||||
<VAlert type="info" variant="tonal" class="mt-4">
|
||||
{{ t('setting.cache.reidentifyDialog.autoHint') }}
|
||||
</VAlert>
|
||||
</VCardText>
|
||||
|
||||
<VCardActions>
|
||||
<VSpacer />
|
||||
<VBtn color="grey" variant="text" @click="reidentifyDialog = false">
|
||||
{{ t('setting.cache.reidentifyDialog.cancel') }}
|
||||
</VBtn>
|
||||
<VBtn color="primary" :loading="loading" @click="performReidentify">
|
||||
{{ t('setting.cache.reidentifyDialog.confirm') }}
|
||||
</VBtn>
|
||||
</VCardActions>
|
||||
</VCard>
|
||||
</VDialog>
|
||||
</template>
|
||||
|
||||
Reference in New Issue
Block a user