Files
MeBox/internal/service/task_tracker_organize_items.go
truewhile 0aff87c69d 优化
优化
2026-08-24 17:00:18 +08:00

147 lines
4.5 KiB
Go

package service
import "strings"
// Item-level task status. A single BackgroundTask (one organize → scan →
// scrape run) is broken down into per-file / per-library TaskItemRecord rows so
// the operator can watch exactly which file is being organized, renamed, ingested
// or scraped, and retry only the failed ones.
const (
ItemStatusPending = "pending" // 待进行
ItemStatusRunning = "running" // 进行中
ItemStatusSucceeded = "succeeded" // 成功
ItemStatusFailed = "failed" // 失败
)
// Item kind / phase. 整理与重命名 share the "organize" phase (per file);
// scan(入库)与 scrape(刮削)are reported per library.
const (
ItemKindOrganize = "organize"
ItemKindScan = "scan"
ItemKindScrape = "scrape"
)
// TaskItemRecord is a single row on the live tasks board. Each record maps to
// exactly one file (organize) or one library (ingest / scrape).
type TaskItemRecord struct {
ID string `json:"id"`
Kind string `json:"kind"` // organize / scan / scrape
Status string `json:"status"` // pending / running / succeeded / failed
Name string `json:"name"` // display name (file base name or library name)
Source string `json:"source,omitempty"` // source file path (organize)
DestPath string `json:"dest_path,omitempty"`
LibraryID string `json:"library_id,omitempty"`
Error string `json:"error,omitempty"`
}
// organizeItemsFromResult converts the final organize result items into
// per-file task records. The organize result is only complete after the whole
// directory walk finishes, so this is called once at stage boundaries rather
// than per file. Each item is keyed by its source path for stable identity.
func organizeItemsFromResult(res *OrganizeResult) []TaskItemRecord {
if res == nil || len(res.Items) == 0 {
return nil
}
out := make([]TaskItemRecord, 0, len(res.Items))
for _, item := range res.Items {
rec := TaskItemRecord{
ID: "organize:" + item.Source,
Kind: ItemKindOrganize,
Name: itemBaseName(item.Source, item.Title),
Source: item.Source,
DestPath: item.Target,
}
switch item.Action {
case "error":
rec.Status = ItemStatusFailed
rec.Error = strings.TrimSpace(item.Reason)
default:
// organize / replace / reclassify / cleanup / skip all count as done.
rec.Status = ItemStatusSucceeded
}
out = append(out, rec)
}
return out
}
// scanItemsFromResult converts per-library scan summaries into task records.
func scanItemsFromResult(res *OrganizeResult) []TaskItemRecord {
if res == nil || len(res.Scans) == 0 {
return nil
}
out := make([]TaskItemRecord, 0, len(res.Scans))
for _, scan := range res.Scans {
rec := TaskItemRecord{
ID: "scan:" + scan.LibraryID,
Kind: ItemKindScan,
Name: scan.Name,
LibraryID: scan.LibraryID,
Status: ItemStatusSucceeded,
}
if scan.Error != "" {
rec.Status = ItemStatusFailed
rec.Error = scan.Error
}
out = append(out, rec)
}
return out
}
// scrapeItemsFromResult converts per-library scrape summaries into task records.
func scrapeItemsFromResult(res *OrganizeResult) []TaskItemRecord {
if res == nil || len(res.Scrapes) == 0 {
return nil
}
out := make([]TaskItemRecord, 0, len(res.Scrapes))
for _, scrape := range res.Scrapes {
rec := TaskItemRecord{
ID: "scrape:" + scrape.LibraryID,
Kind: ItemKindScrape,
Name: scrape.Name,
LibraryID: scrape.LibraryID,
Status: ItemStatusSucceeded,
}
if scrape.Error != "" {
rec.Status = ItemStatusFailed
rec.Error = scrape.Error
out = append(out, rec)
continue
}
if scrape.Skipped {
rec.Status = ItemStatusSucceeded
}
out = append(out, rec)
}
return out
}
// combineOrganizeItems merges organize + scan + scrape item rows into one flat
// list ordered by phase, deduping by ID (scan/scrape share the library ID and
// there is at most one of each per run).
func combineOrganizeItems(res *OrganizeResult) []TaskItemRecord {
items := organizeItemsFromResult(res)
items = append(items, scanItemsFromResult(res)...)
items = append(items, scrapeItemsFromResult(res)...)
return items
}
func itemBaseName(source, title string) string {
if strings.TrimSpace(title) != "" && !strings.Contains(title, ".") {
return strings.TrimSpace(title)
}
if source == "" {
return ""
}
base := source
if idx := strings.LastIndexAny(base, "/\\"); idx >= 0 {
base = base[idx+1:]
}
if idx := strings.LastIndex(base, "."); idx > 0 {
base = base[:idx]
}
if base == "" {
return source
}
return base
}