fix organize path mapping and task flow

This commit is contained in:
ShukeBta
2026-06-13 16:32:06 +08:00
parent 9070b4827c
commit 4e0929bbca
23 changed files with 1468 additions and 134 deletions
+22
View File
@@ -8,9 +8,31 @@ export interface ActiveTranscode {
playlist_ok: boolean
}
export interface BackgroundTask {
id: string
kind: string
name: string
status: 'running' | 'completed' | 'failed'
stage?: string
source_path?: string
dest_path?: string
message?: string
error?: string
metrics?: Record<string, number>
started_at: string
updated_at: string
finished_at?: string
}
export interface BackgroundTaskSnapshot {
active: BackgroundTask[]
recent: BackgroundTask[]
}
export interface TasksSnapshot {
transcodes: ActiveTranscode[]
torrents: QBitTorrent[] | null
background_tasks?: BackgroundTaskSnapshot
}
export const tasksAPI = {
+2 -2
View File
@@ -67,7 +67,7 @@ type AutoOrganizeConfig = {
const AUTO_ORGANIZE_DEFAULTS: AutoOrganizeConfig = {
enabled: 'false',
afterDownload: 'false',
scrapeAfter: 'false',
scrapeAfter: 'true',
sourceDir: '',
targetDir: '',
transferMode: 'hardlink',
@@ -127,7 +127,7 @@ export function FileManagerPage() {
const [organizeTransferMode, setOrganizeTransferMode] = useState('hardlink')
const [organizeMediaType, setOrganizeMediaType] = useState('auto')
const [scanAfter, setScanAfter] = useState(true)
const [scrapeAfter, setScrapeAfter] = useState(false)
const [scrapeAfter, setScrapeAfter] = useState(true)
const [organizeBusy, setOrganizeBusy] = useState('')
const [previewItems, setPreviewItems] = useState<Array<{
source: string
+100 -1
View File
@@ -1,7 +1,7 @@
import { useEffect, useState } from 'react'
import { Activity } from 'lucide-react'
import { tasksAPI, type TasksSnapshot } from '../api/tasks'
import { tasksAPI, type BackgroundTask, type TasksSnapshot } from '../api/tasks'
function fmtBytes(n: number): string {
if (!n || n <= 0) return '0 B'
@@ -15,6 +15,90 @@ function fmtBytes(n: number): string {
return `${v.toFixed(2)} ${u[i]}`
}
const metricLabels: Record<string, string> = {
organized: '新增',
replaced: '替换',
skipped: '跳过',
errors: '错误',
scans: '扫描库',
scan_visited: '访问',
scan_added: '入库',
scan_updated: '更新',
scan_removed: '移除',
scan_errors: '扫描错误',
scrapes: '刮削库',
scrape_matched: '匹配',
scrape_skipped: '刮削跳过',
scrape_errors: '刮削错误',
visited: '访问',
added: '入库',
updated: '更新',
probed: '探测',
local_metadata: '本地元数据',
removed: '移除',
matched: '匹配',
processed: '处理',
queued: '排队',
}
function formatMetrics(metrics?: Record<string, number>): string {
if (!metrics) return ''
return Object.entries(metrics)
.filter(([, value]) => Number.isFinite(value) && value !== 0)
.map(([key, value]) => `${metricLabels[key] ?? key} ${value}`)
.join(' · ')
}
function statusBadge(task: BackgroundTask) {
if (task.status === 'failed') {
return <span className="rounded-lg border border-red-400/40 px-1.5 py-0.5 text-xs text-red-500">failed</span>
}
if (task.status === 'completed') {
return <span className="rounded-lg border border-emerald-400/40 px-1.5 py-0.5 text-xs text-emerald-500">done</span>
}
return <span className="rounded-lg border border-yellow-400/40 px-1.5 py-0.5 text-xs text-yellow-500">running</span>
}
function BackgroundTaskTable({ tasks, empty }: { tasks: BackgroundTask[]; empty: string }) {
if (tasks.length === 0) return <p className="text-sand-500">{empty}</p>
return (
<table className="w-full text-left text-sm">
<thead className="text-xs uppercase tracking-wider text-sand-500">
<tr>
<th className="py-2">任务</th>
<th>阶段</th>
<th>状态</th>
<th>结果</th>
<th>时间</th>
</tr>
</thead>
<tbody>
{tasks.map((task) => (
<tr key={task.id} className="border-t border-gray-200 align-top">
<td className="max-w-md py-2">
<div className="font-medium text-ink-600">{task.name}</div>
<div className="truncate font-mono text-xs text-sand-500" title={task.source_path || task.dest_path}>
{task.source_path || task.dest_path || task.message || '-'}
</div>
</td>
<td className="text-ink-100">{task.stage || '-'}</td>
<td>{statusBadge(task)}</td>
<td className="max-w-md text-ink-100">
<div>{task.error || task.message || '-'}</div>
{formatMetrics(task.metrics) && (
<div className="mt-1 text-xs text-sand-500">{formatMetrics(task.metrics)}</div>
)}
</td>
<td className="text-ink-100">
{new Date(task.finished_at || task.updated_at || task.started_at).toLocaleTimeString()}
</td>
</tr>
))}
</tbody>
</table>
)
}
// TasksPage shows everything the backend is doing right now: ffmpeg
// transcodes + qBittorrent downloads. Refreshes every 3 s.
export function TasksPage() {
@@ -37,6 +121,7 @@ export function TasksPage() {
if (!snap) return <p className="text-sand-500">加载中…</p>
const torrents = snap.torrents ?? []
const background = snap.background_tasks ?? { active: [], recent: [] }
return (
<div className="space-y-8">
@@ -45,6 +130,20 @@ export function TasksPage() {
<h1 className="font-display text-3xl font-bold text-ink-600">实时任务</h1>
</header>
<section className="glass-panel">
<h2 className="mb-3 font-display text-lg font-semibold text-ink-600">整理 / 重命名 / 入库 / 刮削任务</h2>
<div className="space-y-5">
<div>
<h3 className="mb-2 text-sm font-semibold text-ink-500">运行中</h3>
<BackgroundTaskTable tasks={background.active} empty="暂无运行中的整理、重命名、入库或刮削任务。" />
</div>
<div>
<h3 className="mb-2 text-sm font-semibold text-ink-500">最近完成</h3>
<BackgroundTaskTable tasks={background.recent.slice(0, 10)} empty="暂无最近完成的后台任务。" />
</div>
</div>
</section>
<section className="glass-panel">
<h2 className="mb-3 font-display text-lg font-semibold text-ink-600">转码任务</h2>
{snap.transcodes.length === 0 && <p className="text-sand-500">暂无运行中转码。</p>}