mirror of
https://github.com/nianzhibai/91.git
synced 2026-06-15 00:44:30 +08:00
Fix scanner cancellation and shorts UI
This commit is contained in:
@@ -1444,7 +1444,15 @@ func (a *App) runScanWithTaskContext(ctx context.Context, driveID string) {
|
||||
log.Printf("[scan] drive=%s start=%s skip_dirs=%d", driveID, startID, len(d.SkipDirIDs))
|
||||
stats, err := sc.Run(ctx, startID)
|
||||
if err != nil {
|
||||
log.Printf("[scan] drive=%s error: %v", driveID, err)
|
||||
if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) {
|
||||
log.Printf("[scan] drive=%s canceled: %v", driveID, err)
|
||||
} else {
|
||||
log.Printf("[scan] drive=%s error: %v", driveID, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
log.Printf("[scan] drive=%s canceled after scan: %v", driveID, err)
|
||||
return
|
||||
}
|
||||
log.Printf("[scan] drive=%s done scanned=%d added=%d errors=%d", driveID, stats.Scanned, stats.Added, stats.Errors)
|
||||
@@ -1461,12 +1469,20 @@ func (a *App) runScanWithTaskContext(ctx context.Context, driveID string) {
|
||||
} else {
|
||||
removed, err := a.cleanupMissingDriveVideos(ctx, driveID, stats.SeenFileIDs, stats.VisitedDirIDs, startID == drv.RootID())
|
||||
if err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
log.Printf("[cleanup] canceled stale cleanup drive=%s kind=%s: %v", driveID, drv.Kind(), ctxErr)
|
||||
return
|
||||
}
|
||||
log.Printf("[cleanup] stale cleanup drive=%s kind=%s error: %v", driveID, drv.Kind(), err)
|
||||
} else if removed > 0 {
|
||||
log.Printf("[cleanup] removed %d stale videos for drive=%s kind=%s", removed, driveID, drv.Kind())
|
||||
}
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
log.Printf("[scan] drive=%s canceled before enqueue generation: %v", driveID, err)
|
||||
return
|
||||
}
|
||||
a.scheduleFingerprintBackfill(ctx, driveID, fingerprintWorker)
|
||||
a.enqueueDriveGeneration(ctx, driveID, worker, thumbWorker)
|
||||
}
|
||||
|
||||
@@ -127,6 +127,9 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
}
|
||||
|
||||
for _, e := range entries {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if e.IsDir {
|
||||
// 跳过 previews 目录,避免扫到自己生成的预览视频
|
||||
if strings.EqualFold(e.Name, "previews") {
|
||||
@@ -137,6 +140,9 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
continue
|
||||
}
|
||||
if err := s.walk(ctx, e.ID, e.Name, stats, progress); err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
stats.Errors++
|
||||
log.Printf("[scanner] walk %s error: %v", e.Name, err)
|
||||
}
|
||||
@@ -155,6 +161,9 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
|
||||
id := s.Drive.Kind() + "-" + s.Drive.ID() + "-" + e.ID
|
||||
if deleted, err := s.Catalog.IsDeletedVideoCandidate(ctx, id, s.Drive.ID(), e.ID, e.Hash, e.Name, e.Size); err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
stats.Errors++
|
||||
log.Printf("[scanner] check deleted video %s error: %v", id, err)
|
||||
continue
|
||||
@@ -170,11 +179,20 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
if matched, err := s.Catalog.MatchTags(ctx, e.Name+" "+dirName+" "+parsed.Author); err == nil {
|
||||
tags = mergeTags(tags, matched)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if label, ok, err := s.Catalog.EnsureCollectionTag(ctx, dirName); err == nil && ok {
|
||||
tags = mergeTags(tags, []string{label})
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
existing, _ := s.Catalog.GetVideo(ctx, id)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if existing != nil {
|
||||
patch := catalog.VideoMetaPatch{}
|
||||
if e.Hash != "" && existing.ContentHash == "" {
|
||||
@@ -191,12 +209,21 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
}
|
||||
if patch.Category != "" || patch.ContentHash != "" || patch.FileName != "" {
|
||||
_ = s.Catalog.UpdateVideoMeta(ctx, id, patch)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if dup := s.findDuplicate(ctx, e.Hash, e.Name, e.Size, id); dup != nil {
|
||||
continue
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
if !sameTags(existing.Tags, tags) {
|
||||
_ = s.Catalog.SetAutoVideoTags(ctx, id, tags)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
continue
|
||||
}
|
||||
@@ -204,6 +231,9 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
if dup := s.findDuplicate(ctx, e.Hash, e.Name, e.Size, id); dup != nil {
|
||||
continue
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
v := &catalog.Video{
|
||||
@@ -226,9 +256,15 @@ func (s *Scanner) walk(ctx context.Context, dirID, dirName string, stats *Stats,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
if err := s.Catalog.UpsertVideo(ctx, v); err != nil {
|
||||
if ctxErr := ctx.Err(); ctxErr != nil {
|
||||
return ctxErr
|
||||
}
|
||||
log.Printf("[scanner] upsert %s error: %v", v.Title, err)
|
||||
continue
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return err
|
||||
}
|
||||
stats.Added++
|
||||
if s.OnNewVideo != nil {
|
||||
s.OnNewVideo(v)
|
||||
|
||||
@@ -3,6 +3,7 @@ package scanner
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
@@ -90,6 +91,50 @@ func TestRunIgnoresZeroSizeVideoFiles(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunStopsWhenContextCanceledDuringFileLoop(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cat, err := catalog.Open(t.TempDir() + "/catalog.db")
|
||||
if err != nil {
|
||||
t.Fatalf("open catalog: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
if err := cat.Close(); err != nil {
|
||||
t.Fatalf("close catalog: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
drv := &scannerFakeDrive{
|
||||
entries: []drives.Entry{
|
||||
{ID: "file-1", Name: "one.mp4", Size: 123},
|
||||
{ID: "file-2", Name: "two.mp4", Size: 123},
|
||||
{ID: "file-3", Name: "three.mp4", Size: 123},
|
||||
},
|
||||
}
|
||||
callbacks := 0
|
||||
sc := New(cat, drv, []string{".mp4"}, nil, func(*catalog.Video) {
|
||||
callbacks++
|
||||
cancel()
|
||||
})
|
||||
|
||||
stats, err := sc.Run(ctx, "")
|
||||
|
||||
if !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("scan error = %v, want context.Canceled", err)
|
||||
}
|
||||
if stats.Added != 1 || callbacks != 1 {
|
||||
t.Fatalf("added=%d callbacks=%d, want exactly one video before cancellation", stats.Added, callbacks)
|
||||
}
|
||||
if _, err := cat.GetVideo(context.Background(), "fake-drive-file-1"); err != nil {
|
||||
t.Fatalf("first video should be persisted before cancellation: %v", err)
|
||||
}
|
||||
if _, err := cat.GetVideo(context.Background(), "fake-drive-file-2"); err != sql.ErrNoRows {
|
||||
t.Fatalf("second video lookup error = %v, want sql.ErrNoRows", err)
|
||||
}
|
||||
if _, err := cat.GetVideo(context.Background(), "fake-drive-file-3"); err != sql.ErrNoRows {
|
||||
t.Fatalf("third video lookup error = %v, want sql.ErrNoRows", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunSkipsAdminDeletedVideo(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
cat, err := catalog.Open(t.TempDir() + "/catalog.db")
|
||||
|
||||
@@ -809,12 +809,25 @@ function ShortsSlide({
|
||||
}
|
||||
}, [isMarkedHidden]);
|
||||
|
||||
// 监听 video 的时长 / 进度 / 缓冲状态 / 音量物理键变化
|
||||
// 监听 video 的时长 / 进度 / 缓冲状态 / 音量物理键变化。
|
||||
// MOUNT_RADIUS 会让第三屏以后的 slide 先以海报占位,之后才挂载 video;
|
||||
// 因此这里必须跟随 shouldMount 重新绑定,否则后续视频没有 timeupdate 事件。
|
||||
useEffect(() => {
|
||||
if (!shouldMount) {
|
||||
setDuration(0);
|
||||
setCurrentTime(0);
|
||||
setIsBuffering(false);
|
||||
return;
|
||||
}
|
||||
const video = localRef.current;
|
||||
if (!video) return;
|
||||
const handleLoaded = () => {
|
||||
if (Number.isFinite(video.duration)) setDuration(video.duration);
|
||||
if (Number.isFinite(video.duration) && video.duration > 0) {
|
||||
setDuration(video.duration);
|
||||
} else {
|
||||
setDuration(0);
|
||||
}
|
||||
if (!scrubbingRef.current) setCurrentTime(video.currentTime || 0);
|
||||
};
|
||||
const handleTime = () => {
|
||||
// 拖动期间不要被 timeupdate 覆盖 UI
|
||||
@@ -838,6 +851,7 @@ function ShortsSlide({
|
||||
};
|
||||
|
||||
handleLoaded();
|
||||
handleTime();
|
||||
video.addEventListener("loadedmetadata", handleLoaded);
|
||||
video.addEventListener("durationchange", handleLoaded);
|
||||
video.addEventListener("timeupdate", handleTime);
|
||||
@@ -860,7 +874,7 @@ function ShortsSlide({
|
||||
video.removeEventListener("canplay", handlePlayingOrCanPlay);
|
||||
video.removeEventListener("volumechange", handleVolumeChange);
|
||||
};
|
||||
}, [muted, volume, setMuted, setVolume]);
|
||||
}, [shouldMount, item.id, muted, volume, setMuted, setVolume]);
|
||||
|
||||
// 长按 2 倍速:直接绑原生事件
|
||||
useEffect(() => {
|
||||
|
||||
@@ -392,11 +392,7 @@
|
||||
line-height: 1.4;
|
||||
font-weight: 600;
|
||||
color: #fff;
|
||||
/* 多层阴影叠加:近距离锐边 + 远距离弥散,浅色视频上也清楚 */
|
||||
text-shadow:
|
||||
0 1px 1px rgba(0, 0, 0, 0.9),
|
||||
0 2px 5px rgba(0, 0, 0, 0.8),
|
||||
0 4px 15px rgba(0, 0, 0, 0.6);
|
||||
text-shadow: none;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
|
||||
@@ -220,11 +220,11 @@
|
||||
font-weight: var(--weight-bold);
|
||||
color: var(--text-strong);
|
||||
letter-spacing: -0.005em;
|
||||
text-shadow: none;
|
||||
display: -webkit-box;
|
||||
-webkit-line-clamp: 2;
|
||||
-webkit-box-orient: vertical;
|
||||
overflow: hidden;
|
||||
text-shadow: 0 2px 12px rgba(0, 0, 0, 0.35);
|
||||
}
|
||||
|
||||
.vd-header__row {
|
||||
@@ -1200,7 +1200,6 @@
|
||||
-webkit-line-clamp: 3;
|
||||
overflow-wrap: anywhere;
|
||||
word-break: break-word;
|
||||
text-shadow: 0 1px 8px rgba(0, 0, 0, 0.4);
|
||||
}
|
||||
|
||||
.vd-header__row {
|
||||
|
||||
@@ -31,6 +31,18 @@ test("shorts progress dragging uses immediate pointer state", () => {
|
||||
assert.match(shortsPageSource, /onLostPointerCapture=\{handleProgressPointerEnd\}/);
|
||||
});
|
||||
|
||||
test("shorts progress listeners rebind when deferred videos mount", () => {
|
||||
assert.match(
|
||||
shortsPageSource,
|
||||
/MOUNT_RADIUS 会让第三屏以后的 slide 先以海报占位/
|
||||
);
|
||||
assert.match(shortsPageSource, /if \(!shouldMount\) \{\s*setDuration\(0\);\s*setCurrentTime\(0\);/);
|
||||
assert.match(
|
||||
shortsPageSource,
|
||||
/\}, \[shouldMount, item\.id, muted, volume, setMuted, setVolume\]\);/
|
||||
);
|
||||
});
|
||||
|
||||
test("shorts fullscreen changes preserve the active slide", () => {
|
||||
assert.match(shortsPageSource, /const activeIndexRef = useRef\(0\)/);
|
||||
assert.match(shortsPageSource, /const ignoreIntersectionUntilRef = useRef\(0\)/);
|
||||
|
||||
Reference in New Issue
Block a user