diff options
| -rw-r--r-- | internal/repository/playback_progress.go | 16 | ||||
| -rw-r--r-- | internal/repository/repository.go | 2 | ||||
| -rw-r--r-- | internal/repository/sqlite_test.go | 24 |
3 files changed, 38 insertions, 4 deletions
diff --git a/internal/repository/playback_progress.go b/internal/repository/playback_progress.go index a2f753e..d044f92 100644 --- a/internal/repository/playback_progress.go +++ b/internal/repository/playback_progress.go @@ -76,10 +76,22 @@ func (s *SQLite) ListProgressByUser(ctx context.Context, userID int64) ([]model. return pp, rows.Err() } -// ListInProgressMedia returns unfinished, non-deleted media with saved progress for a user. +// ListInProgressMedia returns unfinished, non-deleted media with at least 60s accumulated playback. func (s *SQLite) ListInProgressMedia(ctx context.Context, userID int64, filter MediaFilter) ([]model.Media, error) { args := []any{userID} - conds := []string{`pp.user_id = ?`, `pp.finished = 0`, `media.deleted_at IS NULL`} + conds := []string{ + `pp.user_id = ?`, + `pp.finished = 0`, + `media.deleted_at IS NULL`, + `EXISTS ( + SELECT 1 + FROM playback_accumulator pa + INNER JOIN sessions s ON s.id = pa.session_id + WHERE pa.media_id = media.id + AND s.user_id = pp.user_id + AND pa.accumulated_seconds >= 60 + )`, + } query := `SELECT media.id, media.set_id, media.rel_path, media.file_name, media.abs_path, media.type, media.duration, media.codec, media.resolution, media.bitrate, media.file_size_bytes, media.width, media.height, media.exif_camera, media.exif_lens, media.exif_date, media.exif_iso, media.exif_f_number, media.exif_exposure, media.exif_focal_length, media.thumbnail_path, media.play_count, media.deleted_at, media.created_at FROM playback_progress pp INNER JOIN media ON media.id = pp.media_id` if len(filter.AllowedSetIDs) > 0 { diff --git a/internal/repository/repository.go b/internal/repository/repository.go index 2dacc90..4c4741e 100644 --- a/internal/repository/repository.go +++ b/internal/repository/repository.go @@ -266,7 +266,7 @@ type PlaybackProgressRepo interface { MarkFinished(ctx context.Context, userID, mediaID int64) error // ListProgressByUser returns all saved playback positions for a user. ListProgressByUser(ctx context.Context, userID int64) ([]model.PlaybackProgress, error) - // ListInProgressMedia returns unfinished media with saved playback positions. + // ListInProgressMedia returns unfinished media with at least 60s accumulated playback. ListInProgressMedia(ctx context.Context, userID int64, filter MediaFilter) ([]model.Media, error) } diff --git a/internal/repository/sqlite_test.go b/internal/repository/sqlite_test.go index adb24d7..79e7f81 100644 --- a/internal/repository/sqlite_test.go +++ b/internal/repository/sqlite_test.go @@ -507,19 +507,28 @@ func TestSQLite_PlaybackProgressRepo(t *testing.T) { }, }, { - name: "list in-progress media respects allowed sets and excludes finished and deleted", + name: "list in-progress media respects threshold allowed sets and excludes finished and deleted", run: func(t *testing.T, ctx context.Context, s *SQLite) { now := time.Now().Truncate(time.Second) uid, _ := s.CreateUser(ctx, &model.User{Username: "u", PasswordHash: "h", CreatedAt: now}) + otherUserID, _ := s.CreateUser(ctx, &model.User{Username: "other", PasswordHash: "h", CreatedAt: now}) setAllowed, _ := s.CreateSet(ctx, &model.Set{Name: "allowed", RootPath: "/allowed", CreatedAt: now}) setOther, _ := s.CreateSet(ctx, &model.Set{Name: "other", RootPath: "/other", CreatedAt: now}) keepID, _ := s.CreateMedia(ctx, &model.Media{SetID: setAllowed, RelPath: "keep.mp4", FileName: "keep.mp4", AbsPath: "/allowed/keep.mp4", Type: model.MediaTypeVideo, CreatedAt: now}) + belowThresholdID, _ := s.CreateMedia(ctx, &model.Media{SetID: setAllowed, RelPath: "below.mp4", FileName: "below.mp4", AbsPath: "/allowed/below.mp4", Type: model.MediaTypeVideo, CreatedAt: now}) finishedID, _ := s.CreateMedia(ctx, &model.Media{SetID: setAllowed, RelPath: "finished.mp4", FileName: "finished.mp4", AbsPath: "/allowed/finished.mp4", Type: model.MediaTypeVideo, CreatedAt: now}) deletedID, _ := s.CreateMedia(ctx, &model.Media{SetID: setAllowed, RelPath: "deleted.mp4", FileName: "deleted.mp4", AbsPath: "/allowed/deleted.mp4", Type: model.MediaTypeVideo, CreatedAt: now}) otherID, _ := s.CreateMedia(ctx, &model.Media{SetID: setOther, RelPath: "other.mp4", FileName: "other.mp4", AbsPath: "/other/other.mp4", Type: model.MediaTypeVideo, CreatedAt: now}) + if err := s.CreateSession(ctx, &model.Session{ID: "sess", UserID: uid, ExpiresAt: now.Add(time.Hour), CreatedAt: now}); err != nil { + t.Fatalf("create session: %v", err) + } + if err := s.CreateSession(ctx, &model.Session{ID: "other-sess", UserID: otherUserID, ExpiresAt: now.Add(time.Hour), CreatedAt: now}); err != nil { + t.Fatalf("create other session: %v", err) + } progress := []model.PlaybackProgress{ {UserID: uid, MediaID: keepID, PositionSeconds: 10, UpdatedAt: now.Add(3 * time.Second)}, + {UserID: uid, MediaID: belowThresholdID, PositionSeconds: 10, UpdatedAt: now.Add(4 * time.Second)}, {UserID: uid, MediaID: finishedID, PositionSeconds: 20, Finished: true, UpdatedAt: now.Add(2 * time.Second)}, {UserID: uid, MediaID: deletedID, PositionSeconds: 30, UpdatedAt: now.Add(time.Second)}, {UserID: uid, MediaID: otherID, PositionSeconds: 40, UpdatedAt: now}, @@ -529,6 +538,19 @@ func TestSQLite_PlaybackProgressRepo(t *testing.T) { t.Fatalf("upsert progress %d: %v", i, err) } } + accs := []model.PlaybackAccumulator{ + {SessionID: "sess", MediaID: keepID, LastPosition: 61, AccumulatedSeconds: 61, UpdatedAt: now}, + {SessionID: "sess", MediaID: belowThresholdID, LastPosition: 59, AccumulatedSeconds: 59, UpdatedAt: now}, + {SessionID: "other-sess", MediaID: belowThresholdID, LastPosition: 120, AccumulatedSeconds: 120, UpdatedAt: now}, + {SessionID: "sess", MediaID: finishedID, LastPosition: 61, AccumulatedSeconds: 61, UpdatedAt: now}, + {SessionID: "sess", MediaID: deletedID, LastPosition: 61, AccumulatedSeconds: 61, UpdatedAt: now}, + {SessionID: "sess", MediaID: otherID, LastPosition: 61, AccumulatedSeconds: 61, UpdatedAt: now}, + } + for i := range accs { + if err := s.UpsertAccumulator(ctx, &accs[i]); err != nil { + t.Fatalf("upsert accumulator %d: %v", i, err) + } + } if err := s.SoftDeleteMedia(ctx, deletedID); err != nil { t.Fatalf("soft delete: %v", err) } |
