Files
91/backend/internal/scanner/scanner.go
T
nianzhibai 3506328441 Add PikPak drive support
Add PikPak backend driver, fixed tag matching, cached transcode playback, fast cover handling, and LF normalization.
2026-05-10 23:55:04 +08:00

164 lines
3.5 KiB
Go

package scanner
import (
"context"
"fmt"
"log"
"path"
"strings"
"time"
"github.com/video-site/backend/internal/catalog"
"github.com/video-site/backend/internal/drives"
)
type Scanner struct {
Catalog *catalog.Catalog
Drive drives.Drive
Exts map[string]bool
MaxDepth int
// 回调:新视频被加入后触发 teaser 生成
OnNewVideo func(v *catalog.Video)
}
func New(cat *catalog.Catalog, drv drives.Drive, exts []string, maxDepth int, onNew func(v *catalog.Video)) *Scanner {
m := make(map[string]bool, len(exts))
for _, e := range exts {
m[strings.ToLower(e)] = true
}
if maxDepth == 0 {
maxDepth = 5
}
return &Scanner{
Catalog: cat,
Drive: drv,
Exts: m,
MaxDepth: maxDepth,
OnNewVideo: onNew,
}
}
type Stats struct {
Scanned int
Added int
}
// Run 从 Drive.RootID 开始扫描
func (s *Scanner) Run(ctx context.Context, startDirID string) (Stats, error) {
if startDirID == "" {
startDirID = s.Drive.RootID()
}
stats := Stats{}
if err := s.walk(ctx, startDirID, "", 0, &stats); err != nil {
return stats, err
}
return stats, nil
}
func (s *Scanner) walk(ctx context.Context, dirID, dirName string, depth int, stats *Stats) error {
if depth >= s.MaxDepth {
return nil
}
if err := ctx.Err(); err != nil {
return err
}
entries, err := s.Drive.List(ctx, dirID)
if err != nil {
return fmt.Errorf("list %s: %w", dirID, err)
}
for _, e := range entries {
if e.IsDir {
// 跳过 previews 目录,避免扫到自己生成的 teaser
if strings.EqualFold(e.Name, "previews") {
continue
}
if err := s.walk(ctx, e.ID, e.Name, depth+1, stats); err != nil {
log.Printf("[scanner] walk %s error: %v", e.Name, err)
}
continue
}
stats.Scanned++
ext := strings.ToLower(path.Ext(e.Name))
if !s.Exts[ext] {
continue
}
id := s.Drive.Kind() + "-" + s.Drive.ID() + "-" + e.ID
parsed := Parse(e.Name)
if parsed.Title == "" {
parsed.Title = strings.TrimSuffix(e.Name, ext)
}
existing, _ := s.Catalog.GetVideo(ctx, id)
if existing != nil {
// 已存在但轻量元数据空缺时,顺便补齐。
patch := catalog.VideoMetaPatch{}
if existing.Category == "" && dirName != "" {
patch.Category = dirName
}
if existing.ThumbnailURL == "" && e.ThumbnailURL != "" {
patch.ThumbnailURL = e.ThumbnailURL
}
if !sameTags(existing.Tags, parsed.Tags) {
patch.Tags = parsed.Tags
patch.TagsSet = true
}
if patch.Category != "" || patch.ThumbnailURL != "" || patch.TagsSet {
_ = s.Catalog.UpdateVideoMeta(ctx, id, patch)
}
continue
}
now := time.Now()
v := &catalog.Video{
ID: id,
DriveID: s.Drive.ID(),
FileID: e.ID,
ParentID: e.ParentID,
Title: parsed.Title,
Author: parsed.Author,
Tags: parsed.Tags,
Ext: strings.TrimPrefix(ext, "."),
Quality: "HD",
Size: e.Size,
ThumbnailURL: e.ThumbnailURL,
PreviewStatus: "pending",
Category: dirName,
PublishedAt: orDefault(e.ModTime, now),
CreatedAt: now,
UpdatedAt: now,
}
if err := s.Catalog.UpsertVideo(ctx, v); err != nil {
log.Printf("[scanner] upsert %s error: %v", v.Title, err)
continue
}
stats.Added++
if s.OnNewVideo != nil {
s.OnNewVideo(v)
}
}
return nil
}
func orDefault(t time.Time, d time.Time) time.Time {
if t.IsZero() {
return d
}
return t
}
func sameTags(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}