summaryrefslogtreecommitdiff
path: root/internal
diff options
context:
space:
mode:
authorPaul Buetow <paul@buetow.org>2026-04-29 00:18:40 +0300
committerPaul Buetow <paul@buetow.org>2026-04-29 00:18:40 +0300
commit655b01e5b19b72dc133e7b24c658e0ec3f611bb0 (patch)
treefbfcd935fe6e447a308a37b80fcf2752f74a2512 /internal
parent5b5978dabf2011a64720998cd03cbb01c706475d (diff)
feat: SQLite schema migrations, repository interfaces, and concrete SQLite implementations with :memory: table-driven tests (task l9)
Diffstat (limited to 'internal')
-rw-r--r--internal/repository/favorite.go62
-rw-r--r--internal/repository/media.go244
-rw-r--r--internal/repository/migrate.go134
-rw-r--r--internal/repository/mock.go635
-rw-r--r--internal/repository/note.go45
-rw-r--r--internal/repository/playback_accumulator.go35
-rw-r--r--internal/repository/playback_progress.go52
-rw-r--r--internal/repository/repository.go134
-rw-r--r--internal/repository/session.go58
-rw-r--r--internal/repository/set.go79
-rw-r--r--internal/repository/set_permission.go84
-rw-r--r--internal/repository/share.go95
-rw-r--r--internal/repository/sqlite.go57
-rw-r--r--internal/repository/sqlite_test.go737
-rw-r--r--internal/repository/tag.go108
-rw-r--r--internal/repository/user.go91
16 files changed, 2650 insertions, 0 deletions
diff --git a/internal/repository/favorite.go b/internal/repository/favorite.go
new file mode 100644
index 0000000..4d3e0f5
--- /dev/null
+++ b/internal/repository/favorite.go
@@ -0,0 +1,62 @@
+package repository
+
+import (
+ "context"
+ "fmt"
+
+ "github.com/paul/kiss-media-player/internal/model"
+)
+
+// ToggleFavorite inserts or deletes a favorite row, returning whether it is now favorited.
+func (s *SQLite) ToggleFavorite(ctx context.Context, userID, mediaID int64) (bool, error) {
+ row := s.db.QueryRowContext(ctx,
+ `SELECT 1 FROM favorites WHERE user_id = ? AND media_id = ?`, userID, mediaID)
+ var dummy int
+ err := row.Scan(&dummy)
+ if err != nil {
+ // Insert new favorite
+ _, err = s.db.ExecContext(ctx,
+ `INSERT INTO favorites (user_id, media_id) VALUES (?, ?)`, userID, mediaID)
+ if err != nil {
+ return false, fmt.Errorf("insert favorite: %w", err)
+ }
+ return true, nil
+ }
+ // Delete existing favorite
+ _, err = s.db.ExecContext(ctx,
+ `DELETE FROM favorites WHERE user_id = ? AND media_id = ?`, userID, mediaID)
+ if err != nil {
+ return false, fmt.Errorf("delete favorite: %w", err)
+ }
+ return false, nil
+}
+
+// IsFavorite returns true if the user has favorited the media.
+func (s *SQLite) IsFavorite(ctx context.Context, userID, mediaID int64) (bool, error) {
+ row := s.db.QueryRowContext(ctx,
+ `SELECT 1 FROM favorites WHERE user_id = ? AND media_id = ?`, userID, mediaID)
+ var dummy int
+ if err := row.Scan(&dummy); err != nil {
+ return false, nil
+ }
+ return true, nil
+}
+
+// ListFavoritesByUser returns all favorites for a user.
+func (s *SQLite) ListFavoritesByUser(ctx context.Context, userID int64) ([]model.Favorite, error) {
+ rows, err := s.db.QueryContext(ctx,
+ `SELECT user_id, media_id, created_at FROM favorites WHERE user_id = ? ORDER BY created_at DESC`, userID)
+ if err != nil {
+ return nil, fmt.Errorf("list favorites: %w", err)
+ }
+ defer rows.Close()
+ var favs []model.Favorite
+ for rows.Next() {
+ var f model.Favorite
+ if err := rows.Scan(&f.UserID, &f.MediaID, &f.CreatedAt); err != nil {
+ return nil, err
+ }
+ favs = append(favs, f)
+ }
+ return favs, rows.Err()
+}
diff --git a/internal/repository/media.go b/internal/repository/media.go
new file mode 100644
index 0000000..1f1b485
--- /dev/null
+++ b/internal/repository/media.go
@@ -0,0 +1,244 @@
+package repository
+
+import (
+ "context"
+ "database/sql"
+ "fmt"
+ "strings"
+
+ "github.com/paul/kiss-media-player/internal/model"
+)
+
+// CreateMedia inserts a new media and returns the generated ID.
+func (s *SQLite) CreateMedia(ctx context.Context, media *model.Media) (int64, error) {
+ res, err := s.db.ExecContext(ctx,
+ `INSERT INTO media (set_id, rel_path, file_name, abs_path, type, duration, codec, resolution, bitrate, file_size_bytes, thumbnail_path, play_count, deleted_at, created_at)
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
+ media.SetID, media.RelPath, media.FileName, media.AbsPath, string(media.Type),
+ media.Duration, media.Codec, media.Resolution, media.Bitrate, media.FileSizeBytes,
+ sqlNullString(media.ThumbnailPath), media.PlayCount, sqlNullTime(media.DeletedAt), media.CreatedAt,
+ )
+ if err != nil {
+ return 0, fmt.Errorf("insert media: %w", err)
+ }
+ return res.LastInsertId()
+}
+
+func scanMedia(row sqlScanner) (*model.Media, error) {
+ var m model.Media
+ var deleted sql.NullTime
+ var mediaType string
+ var thumbnail sql.NullString
+ var codec sql.NullString
+ var resolution sql.NullString
+ var duration sql.NullFloat64
+ var bitrate sql.NullInt64
+ var fileSize sql.NullInt64
+ err := row.Scan(
+ &m.ID, &m.SetID, &m.RelPath, &m.FileName, &m.AbsPath, &mediaType,
+ &duration, &codec, &resolution, &bitrate, &fileSize,
+ &thumbnail, &m.PlayCount, &deleted, &m.CreatedAt,
+ )
+ if err != nil {
+ return nil, err
+ }
+ m.Type = model.MediaType(mediaType)
+ if deleted.Valid {
+ m.DeletedAt = &deleted.Time
+ }
+ if thumbnail.Valid {
+ m.ThumbnailPath = thumbnail.String
+ }
+ if codec.Valid {
+ m.Codec = codec.String
+ }
+ if resolution.Valid {
+ m.Resolution = resolution.String
+ }
+ if duration.Valid {
+ m.Duration = duration.Float64
+ }
+ if bitrate.Valid {
+ m.Bitrate = int(bitrate.Int64)
+ }
+ if fileSize.Valid {
+ m.FileSizeBytes = fileSize.Int64
+ }
+ return &m, nil
+}
+
+// GetMediaByID retrieves a media by ID.
+func (s *SQLite) GetMediaByID(ctx context.Context, id int64) (*model.Media, error) {
+ row := s.db.QueryRowContext(ctx,
+ `SELECT id, set_id, rel_path, file_name, abs_path, type, duration, codec, resolution, bitrate, file_size_bytes, thumbnail_path, play_count, deleted_at, created_at FROM media WHERE id = ?`, id)
+ return scanMedia(row)
+}
+
+// UpdateMedia updates all mutable fields of a media record.
+func (s *SQLite) UpdateMedia(ctx context.Context, media *model.Media) error {
+ _, err := s.db.ExecContext(ctx,
+ `UPDATE media SET set_id = ?, rel_path = ?, file_name = ?, abs_path = ?, type = ?, duration = ?, codec = ?, resolution = ?, bitrate = ?, file_size_bytes = ?, thumbnail_path = ?, play_count = ?, deleted_at = ? WHERE id = ?`,
+ media.SetID, media.RelPath, media.FileName, media.AbsPath, string(media.Type), media.Duration,
+ media.Codec, media.Resolution, media.Bitrate, media.FileSizeBytes, sqlNullString(media.ThumbnailPath),
+ media.PlayCount, sqlNullTime(media.DeletedAt), media.ID,
+ )
+ if err != nil {
+ return fmt.Errorf("update media: %w", err)
+ }
+ return nil
+}
+
+// SoftDeleteMedia sets deleted_at to NOW().
+func (s *SQLite) SoftDeleteMedia(ctx context.Context, id int64) error {
+ _, err := s.db.ExecContext(ctx, `UPDATE media SET deleted_at = CURRENT_TIMESTAMP WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("soft delete media: %w", err)
+ }
+ return nil
+}
+
+// RestoreMedia clears deleted_at.
+func (s *SQLite) RestoreMedia(ctx context.Context, id int64) error {
+ _, err := s.db.ExecContext(ctx, `UPDATE media SET deleted_at = NULL WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("restore media: %w", err)
+ }
+ return nil
+}
+
+// HardDeleteMedia permanently deletes a media record.
+func (s *SQLite) HardDeleteMedia(ctx context.Context, id int64) error {
+ _, err := s.db.ExecContext(ctx, `DELETE FROM media WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("hard delete media: %w", err)
+ }
+ return nil
+}
+
+// ListMedia returns media matching the filter.
+func (s *SQLite) ListMedia(ctx context.Context, filter MediaFilter) ([]model.Media, error) {
+ var args []any
+ var conds []string
+ var joins string
+ query := `SELECT DISTINCT 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.thumbnail_path, media.play_count, media.deleted_at, media.created_at FROM media`
+
+ if filter.Search != "" {
+ conds = append(conds, `(media.file_name LIKE ? OR media.rel_path LIKE ?)`)
+ like := "%" + strings.ReplaceAll(filter.Search, "%", "\\%") + "%"
+ args = append(args, like, like)
+ }
+ if filter.Favorites != nil {
+ joins += ` INNER JOIN favorites f ON f.media_id = media.id AND f.user_id = ?`
+ args = append(args, *filter.Favorites)
+ }
+ if len(filter.Tags) > 0 {
+ joins += ` INNER JOIN media_tags mt ON mt.media_id = media.id INNER JOIN tags t ON t.id = mt.tag_id`
+ conds = append(conds, `t.name IN (`+placeholders(len(filter.Tags))+`)`)
+ for _, t := range filter.Tags {
+ args = append(args, t)
+ }
+ // Require all tags by grouping and checking count
+ // This is handled below via HAVING
+ }
+
+ if filter.SetID != nil {
+ conds = append(conds, `media.set_id = ?`)
+ args = append(args, *filter.SetID)
+ }
+ if filter.Type != nil {
+ conds = append(conds, `media.type = ?`)
+ args = append(args, string(*filter.Type))
+ }
+ if filter.MinDuration != nil {
+ conds = append(conds, `media.duration >= ?`)
+ args = append(args, *filter.MinDuration)
+ }
+ if filter.MaxDuration != nil {
+ conds = append(conds, `media.duration <= ?`)
+ args = append(args, *filter.MaxDuration)
+ }
+ conds = append(conds, `media.deleted_at IS NULL`)
+
+ query += joins
+ if len(conds) > 0 {
+ query += " WHERE " + strings.Join(conds, " AND ")
+ }
+ if len(filter.Tags) > 0 {
+ query += ` GROUP BY media.id HAVING COUNT(DISTINCT t.name) = ` + fmt.Sprintf("%d", len(filter.Tags))
+ }
+
+ switch filter.Sort {
+ case "duration":
+ query += " ORDER BY media.duration"
+ case "play_count":
+ query += " ORDER BY media.play_count DESC"
+ case "date":
+ query += " ORDER BY media.created_at DESC"
+ case "random":
+ query += " ORDER BY RANDOM()"
+ default:
+ query += " ORDER BY media.file_name"
+ }
+
+ if filter.Limit > 0 {
+ query += fmt.Sprintf(" LIMIT %d", filter.Limit)
+ }
+ if filter.Offset > 0 {
+ query += fmt.Sprintf(" OFFSET %d", filter.Offset)
+ }
+
+ rows, err := s.db.QueryContext(ctx, query, args...)
+ if err != nil {
+ return nil, fmt.Errorf("list media: %w", err)
+ }
+ defer rows.Close()
+
+ var media []model.Media
+ for rows.Next() {
+ m, err := scanMedia(rows)
+ if err != nil {
+ return nil, err
+ }
+ media = append(media, *m)
+ }
+ return media, rows.Err()
+}
+
+// ListDeletedMedia returns all soft-deleted media.
+func (s *SQLite) ListDeletedMedia(ctx context.Context) ([]model.Media, error) {
+ rows, err := s.db.QueryContext(ctx,
+ `SELECT id, set_id, rel_path, file_name, abs_path, type, duration, codec, resolution, bitrate, file_size_bytes, thumbnail_path, play_count, deleted_at, created_at FROM media WHERE deleted_at IS NOT NULL ORDER BY deleted_at DESC`)
+ if err != nil {
+ return nil, fmt.Errorf("list deleted media: %w", err)
+ }
+ defer rows.Close()
+ var media []model.Media
+ for rows.Next() {
+ m, err := scanMedia(rows)
+ if err != nil {
+ return nil, err
+ }
+ media = append(media, *m)
+ }
+ return media, rows.Err()
+}
+
+// IncrementPlayCount increments the play_count of a media by 1.
+func (s *SQLite) IncrementPlayCount(ctx context.Context, id int64) error {
+ _, err := s.db.ExecContext(ctx, `UPDATE media SET play_count = play_count + 1 WHERE id = ?`, id)
+ if err != nil {
+ return fmt.Errorf("increment play count: %w", err)
+ }
+ return nil
+}
+
+func placeholders(n int) string {
+ if n <= 0 {
+ return ""
+ }
+ parts := make([]string, n)
+ for i := range parts {
+ parts[i] = "?"
+ }
+ return strings.Join(parts, ",")
+}
diff --git a/internal/repository/migrate.go b/internal/repository/migrate.go
new file mode 100644
index 0000000..60cea03
--- /dev/null
+++ b/internal/repository/migrate.go
@@ -0,0 +1,134 @@
+package repository
+
+import (
+ "database/sql"
+ "fmt"
+)
+
+// Migrate creates the database schema if it does not exist.
+func Migrate(db *sql.DB) error {
+ if _, err := db.Exec(`PRAGMA foreign_keys = ON;`); err != nil {
+ return fmt.Errorf("enable foreign keys: %w", err)
+ }
+
+ schema := `
+CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ is_admin INTEGER NOT NULL DEFAULT 0,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS sets (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT NOT NULL,
+ root_path TEXT UNIQUE NOT NULL,
+ cover_thumbnail_path TEXT,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS set_permissions (
+ set_id INTEGER NOT NULL REFERENCES sets(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ role TEXT CHECK(role IN ('owner','viewer')) NOT NULL DEFAULT 'viewer',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (set_id, user_id)
+);
+
+CREATE TABLE IF NOT EXISTS media (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ set_id INTEGER NOT NULL REFERENCES sets(id) ON DELETE CASCADE,
+ rel_path TEXT NOT NULL,
+ file_name TEXT NOT NULL,
+ abs_path TEXT NOT NULL,
+ type TEXT CHECK(type IN ('video','audio')) NOT NULL,
+ duration REAL,
+ codec TEXT,
+ resolution TEXT,
+ bitrate INTEGER,
+ file_size_bytes INTEGER,
+ thumbnail_path TEXT,
+ play_count INTEGER NOT NULL DEFAULT 0,
+ deleted_at DATETIME,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(set_id, rel_path)
+);
+
+CREATE TABLE IF NOT EXISTS tags (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ name TEXT UNIQUE NOT NULL
+);
+
+CREATE TABLE IF NOT EXISTS media_tags (
+ media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE,
+ tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE,
+ PRIMARY KEY (media_id, tag_id)
+);
+
+CREATE TABLE IF NOT EXISTS favorites (
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (user_id, media_id)
+);
+
+CREATE TABLE IF NOT EXISTS playback_progress (
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE,
+ position_seconds REAL NOT NULL,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (user_id, media_id)
+);
+
+CREATE TABLE IF NOT EXISTS sessions (
+ id TEXT PRIMARY KEY,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ expires_at DATETIME NOT NULL,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP
+);
+
+CREATE TABLE IF NOT EXISTS playback_accumulator (
+ session_id TEXT NOT NULL REFERENCES sessions(id) ON DELETE CASCADE,
+ media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE,
+ last_position REAL NOT NULL DEFAULT 0,
+ accumulated_seconds REAL NOT NULL DEFAULT 0,
+ counted INTEGER NOT NULL DEFAULT 0,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (session_id, media_id)
+);
+
+CREATE TABLE IF NOT EXISTS shares (
+ token TEXT PRIMARY KEY,
+ media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE,
+ created_by INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ expires_at DATETIME NOT NULL,
+ max_uses INTEGER,
+ used_count INTEGER NOT NULL DEFAULT 0
+);
+
+CREATE TABLE IF NOT EXISTS media_notes (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ media_id INTEGER NOT NULL REFERENCES media(id) ON DELETE CASCADE,
+ user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE,
+ content TEXT NOT NULL DEFAULT '',
+ created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
+ UNIQUE(media_id, user_id)
+);
+
+CREATE INDEX IF NOT EXISTS idx_media_set_id ON media(set_id);
+CREATE INDEX IF NOT EXISTS idx_media_rel_path ON media(set_id, rel_path);
+CREATE INDEX IF NOT EXISTS idx_media_deleted_at ON media(deleted_at);
+CREATE INDEX IF NOT EXISTS idx_media_type ON media(type);
+CREATE INDEX IF NOT EXISTS idx_media_filename ON media(file_name);
+CREATE INDEX IF NOT EXISTS idx_permissions_user ON set_permissions(user_id);
+CREATE INDEX IF NOT EXISTS idx_permissions_set ON set_permissions(set_id);
+CREATE INDEX IF NOT EXISTS idx_shares_expires ON shares(expires_at);
+`
+ if _, err := db.Exec(schema); err != nil {
+ return fmt.Errorf("execute schema: %w", err)
+ }
+ return nil
+}
diff --git a/internal/repository/mock.go b/internal/repository/mock.go
new file mode 100644
index 0000000..7c54e5c
--- /dev/null
+++ b/internal/repository/mock.go
@@ -0,0 +1,635 @@
+// Package repository provides data access abstractions.
+package repository
+
+import (
+ "context"
+ "time"
+
+ "github.com/paul/kiss-media-player/internal/model"
+)
+
+// compile-time checks.
+var (
+ _ Store = (*MockStore)(nil)
+)
+
+// MockStore is a hand-written fake for all repository interfaces.
+// Each embedded struct provides default no-op / zero-value behavior;
+// callers override individual func fields to inject test behavior.
+type MockStore struct {
+ UserRepo MockUserRepo
+ SetRepo MockSetRepo
+ SetPermissionRepo MockSetPermissionRepo
+ MediaRepo MockMediaRepo
+ TagRepo MockTagRepo
+ FavoriteRepo MockFavoriteRepo
+ PlaybackProgressRepo MockPlaybackProgressRepo
+ PlaybackAccumulatorRepo MockPlaybackAccumulatorRepo
+ SessionRepo MockSessionRepo
+ ShareRepo MockShareRepo
+ NoteRepo MockNoteRepo
+}
+
+func (m *MockStore) CreateUser(ctx context.Context, user *model.User) (int64, error) {
+ return m.UserRepo.CreateUser(ctx, user)
+}
+func (m *MockStore) GetUserByID(ctx context.Context, id int64) (*model.User, error) {
+ return m.UserRepo.GetUserByID(ctx, id)
+}
+func (m *MockStore) GetUserByUsername(ctx context.Context, username string) (*model.User, error) {
+ return m.UserRepo.GetUserByUsername(ctx, username)
+}
+func (m *MockStore) ListUsers(ctx context.Context) ([]model.User, error) {
+ return m.UserRepo.ListUsers(ctx)
+}
+func (m *MockStore) DeleteUser(ctx context.Context, id int64) error {
+ return m.UserRepo.DeleteUser(ctx, id)
+}
+func (m *MockStore) CountUsers(ctx context.Context) (int, error) { return m.UserRepo.CountUsers(ctx) }
+
+func (m *MockStore) CreateSet(ctx context.Context, set *model.Set) (int64, error) {
+ return m.SetRepo.CreateSet(ctx, set)
+}
+func (m *MockStore) GetSetByID(ctx context.Context, id int64) (*model.Set, error) {
+ return m.SetRepo.GetSetByID(ctx, id)
+}
+func (m *MockStore) ListSets(ctx context.Context) ([]model.Set, error) {
+ return m.SetRepo.ListSets(ctx)
+}
+func (m *MockStore) UpdateSet(ctx context.Context, set *model.Set) error {
+ return m.SetRepo.UpdateSet(ctx, set)
+}
+func (m *MockStore) DeleteSet(ctx context.Context, id int64) error {
+ return m.SetRepo.DeleteSet(ctx, id)
+}
+
+func (m *MockStore) GrantPermission(ctx context.Context, perm *model.SetPermission) error {
+ return m.SetPermissionRepo.GrantPermission(ctx, perm)
+}
+func (m *MockStore) RevokePermission(ctx context.Context, setID, userID int64) error {
+ return m.SetPermissionRepo.RevokePermission(ctx, setID, userID)
+}
+func (m *MockStore) GetPermission(ctx context.Context, setID, userID int64) (*model.SetPermission, error) {
+ return m.SetPermissionRepo.GetPermission(ctx, setID, userID)
+}
+func (m *MockStore) ListPermissionsBySet(ctx context.Context, setID int64) ([]model.SetPermission, error) {
+ return m.SetPermissionRepo.ListPermissionsBySet(ctx, setID)
+}
+func (m *MockStore) ListPermissionsByUser(ctx context.Context, userID int64) ([]model.SetPermission, error) {
+ return m.SetPermissionRepo.ListPermissionsByUser(ctx, userID)
+}
+
+func (m *MockStore) CreateMedia(ctx context.Context, media *model.Media) (int64, error) {
+ return m.MediaRepo.CreateMedia(ctx, media)
+}
+func (m *MockStore) GetMediaByID(ctx context.Context, id int64) (*model.Media, error) {
+ return m.MediaRepo.GetMediaByID(ctx, id)
+}
+func (m *MockStore) UpdateMedia(ctx context.Context, media *model.Media) error {
+ return m.MediaRepo.UpdateMedia(ctx, media)
+}
+func (m *MockStore) SoftDeleteMedia(ctx context.Context, id int64) error {
+ return m.MediaRepo.SoftDeleteMedia(ctx, id)
+}
+func (m *MockStore) RestoreMedia(ctx context.Context, id int64) error {
+ return m.MediaRepo.RestoreMedia(ctx, id)
+}
+func (m *MockStore) HardDeleteMedia(ctx context.Context, id int64) error {
+ return m.MediaRepo.HardDeleteMedia(ctx, id)
+}
+func (m *MockStore) ListMedia(ctx context.Context, filter MediaFilter) ([]model.Media, error) {
+ return m.MediaRepo.ListMedia(ctx, filter)
+}
+func (m *MockStore) ListDeletedMedia(ctx context.Context) ([]model.Media, error) {
+ return m.MediaRepo.ListDeletedMedia(ctx)
+}
+func (m *MockStore) IncrementPlayCount(ctx context.Context, id int64) error {
+ return m.MediaRepo.IncrementPlayCount(ctx, id)
+}
+
+func (m *MockStore) CreateTag(ctx context.Context, name string) (int64, error) {
+ return m.TagRepo.CreateTag(ctx, name)
+}
+func (m *MockStore) GetTagByID(ctx context.Context, id int64) (*model.Tag, error) {
+ return m.TagRepo.GetTagByID(ctx, id)
+}
+func (m *MockStore) GetTagByName(ctx context.Context, name string) (*model.Tag, error) {
+ return m.TagRepo.GetTagByName(ctx, name)
+}
+func (m *MockStore) ListTags(ctx context.Context) ([]model.Tag, error) {
+ return m.TagRepo.ListTags(ctx)
+}
+func (m *MockStore) DeleteTag(ctx context.Context, id int64) error {
+ return m.TagRepo.DeleteTag(ctx, id)
+}
+func (m *MockStore) AssignTag(ctx context.Context, mediaID, tagID int64) error {
+ return m.TagRepo.AssignTag(ctx, mediaID, tagID)
+}
+func (m *MockStore) RemoveTag(ctx context.Context, mediaID, tagID int64) error {
+ return m.TagRepo.RemoveTag(ctx, mediaID, tagID)
+}
+func (m *MockStore) ListTagsByMedia(ctx context.Context, mediaID int64) ([]model.Tag, error) {
+ return m.TagRepo.ListTagsByMedia(ctx, mediaID)
+}
+
+func (m *MockStore) ToggleFavorite(ctx context.Context, userID, mediaID int64) (bool, error) {
+ return m.FavoriteRepo.ToggleFavorite(ctx, userID, mediaID)
+}
+func (m *MockStore) IsFavorite(ctx context.Context, userID, mediaID int64) (bool, error) {
+ return m.FavoriteRepo.IsFavorite(ctx, userID, mediaID)
+}
+func (m *MockStore) ListFavoritesByUser(ctx context.Context, userID int64) ([]model.Favorite, error) {
+ return m.FavoriteRepo.ListFavoritesByUser(ctx, userID)
+}
+
+func (m *MockStore) UpsertProgress(ctx context.Context, progress *model.PlaybackProgress) error {
+ return m.PlaybackProgressRepo.UpsertProgress(ctx, progress)
+}
+func (m *MockStore) GetProgress(ctx context.Context, userID, mediaID int64) (*model.PlaybackProgress, error) {
+ return m.PlaybackProgressRepo.GetProgress(ctx, userID, mediaID)
+}
+func (m *MockStore) ListProgressByUser(ctx context.Context, userID int64) ([]model.PlaybackProgress, error) {
+ return m.PlaybackProgressRepo.ListProgressByUser(ctx, userID)
+}
+
+func (m *MockStore) UpsertAccumulator(ctx context.Context, acc *model.PlaybackAccumulator) error {
+ return m.PlaybackAccumulatorRepo.UpsertAccumulator(ctx, acc)
+}
+func (m *MockStore) GetAccumulator(ctx context.Context, sessionID string, mediaID int64) (*model.PlaybackAccumulator, error) {
+ return m.PlaybackAccumulatorRepo.GetAccumulator(ctx, sessionID, mediaID)
+}
+
+func (m *MockStore) CreateSession(ctx context.Context, session *model.Session) error {
+ return m.SessionRepo.CreateSession(ctx, session)
+}
+func (m *MockStore) GetSessionByID(ctx context.Context, id string) (*model.Session, error) {
+ return m.SessionRepo.GetSessionByID(ctx, id)
+}
+func (m *MockStore) DeleteSession(ctx context.Context, id string) error {
+ return m.SessionRepo.DeleteSession(ctx, id)
+}
+func (m *MockStore) DeleteExpiredSessions(ctx context.Context, now time.Time) error {
+ return m.SessionRepo.DeleteExpiredSessions(ctx, now)
+}
+
+func (m *MockStore) CreateShare(ctx context.Context, share *model.Share) error {
+ return m.ShareRepo.CreateShare(ctx, share)
+}
+func (m *MockStore) GetShareByToken(ctx context.Context, token string) (*model.Share, error) {
+ return m.ShareRepo.GetShareByToken(ctx, token)
+}
+func (m *MockStore) ListSharesByMedia(ctx context.Context, mediaID int64) ([]model.Share, error) {
+ return m.ShareRepo.ListSharesByMedia(ctx, mediaID)
+}
+func (m *MockStore) UseShare(ctx context.Context, token string) error {
+ return m.ShareRepo.UseShare(ctx, token)
+}
+func (m *MockStore) DeleteShare(ctx context.Context, token string) error {
+ return m.ShareRepo.DeleteShare(ctx, token)
+}
+func (m *MockStore) DeleteExpiredShares(ctx context.Context, now time.Time) error {
+ return m.ShareRepo.DeleteExpiredShares(ctx, now)
+}
+
+func (m *MockStore) UpsertNote(ctx context.Context, note *model.Note) error {
+ return m.NoteRepo.UpsertNote(ctx, note)
+}
+func (m *MockStore) GetNote(ctx context.Context, mediaID, userID int64) (*model.Note, error) {
+ return m.NoteRepo.GetNote(ctx, mediaID, userID)
+}
+func (m *MockStore) DeleteNote(ctx context.Context, mediaID, userID int64) error {
+ return m.NoteRepo.DeleteNote(ctx, mediaID, userID)
+}
+
+// MockUserRepo is a fake UserRepo.
+type MockUserRepo struct {
+ CreateUserFunc func(ctx context.Context, user *model.User) (int64, error)
+ GetUserByIDFunc func(ctx context.Context, id int64) (*model.User, error)
+ GetUserByUsernameFunc func(ctx context.Context, username string) (*model.User, error)
+ ListUsersFunc func(ctx context.Context) ([]model.User, error)
+ DeleteUserFunc func(ctx context.Context, id int64) error
+ CountUsersFunc func(ctx context.Context) (int, error)
+}
+
+func (m *MockUserRepo) CreateUser(ctx context.Context, user *model.User) (int64, error) {
+ if m.CreateUserFunc != nil {
+ return m.CreateUserFunc(ctx, user)
+ }
+ return 1, nil
+}
+func (m *MockUserRepo) GetUserByID(ctx context.Context, id int64) (*model.User, error) {
+ if m.GetUserByIDFunc != nil {
+ return m.GetUserByIDFunc(ctx, id)
+ }
+ return nil, nil
+}
+func (m *MockUserRepo) GetUserByUsername(ctx context.Context, username string) (*model.User, error) {
+ if m.GetUserByUsernameFunc != nil {
+ return m.GetUserByUsernameFunc(ctx, username)
+ }
+ return nil, nil
+}
+func (m *MockUserRepo) ListUsers(ctx context.Context) ([]model.User, error) {
+ if m.ListUsersFunc != nil {
+ return m.ListUsersFunc(ctx)
+ }
+ return nil, nil
+}
+func (m *MockUserRepo) DeleteUser(ctx context.Context, id int64) error {
+ if m.DeleteUserFunc != nil {
+ return m.DeleteUserFunc(ctx, id)
+ }
+ return nil
+}
+func (m *MockUserRepo) CountUsers(ctx context.Context) (int, error) {
+ if m.CountUsersFunc != nil {
+ return m.CountUsersFunc(ctx)
+ }
+ return 0, nil
+}
+
+// MockSetRepo is a fake SetRepo.
+type MockSetRepo struct {
+ CreateSetFunc func(ctx context.Context, set *model.Set) (int64, error)
+ GetSetByIDFunc func(ctx context.Context, id int64) (*model.Set, error)
+ ListSetsFunc func(ctx context.Context) ([]model.Set, error)
+ UpdateSetFunc func(ctx context.Context, set *model.Set) error
+ DeleteSetFunc func(ctx context.Context, id int64) error
+}
+
+func (m *MockSetRepo) CreateSet(ctx context.Context, set *model.Set) (int64, error) {
+ if m.CreateSetFunc != nil {
+ return m.CreateSetFunc(ctx, set)
+ }
+ return 1, nil
+}
+func (m *MockSetRepo) GetSetByID(ctx context.Context, id int64) (*model.Set, error) {
+ if m.GetSetByIDFunc != nil {
+ return m.GetSetByIDFunc(ctx, id)
+ }
+ return nil, nil
+}
+func (m *MockSetRepo) ListSets(ctx context.Context) ([]model.Set, error) {
+ if m.ListSetsFunc != nil {
+ return m.ListSetsFunc(ctx)
+ }
+ return nil, nil
+}
+func (m *MockSetRepo) UpdateSet(ctx context.Context, set *model.Set) error {
+ if m.UpdateSetFunc != nil {
+ return m.UpdateSetFunc(ctx, set)
+ }
+ return nil
+}
+func (m *MockSetRepo) DeleteSet(ctx context.Context, id int64) error {
+ if m.DeleteSetFunc != nil {
+ return m.DeleteSetFunc(ctx, id)
+ }
+ return nil
+}
+
+// MockSetPermissionRepo is a fake SetPermissionRepo.
+type MockSetPermissionRepo struct {
+ GrantPermissionFunc func(ctx context.Context, perm *model.SetPermission) error
+ RevokePermissionFunc func(ctx context.Context, setID, userID int64) error
+ GetPermissionFunc func(ctx context.Context, setID, userID int64) (*model.SetPermission, error)
+ ListPermissionsBySetFunc func(ctx context.Context, setID int64) ([]model.SetPermission, error)
+ ListPermissionsByUserFunc func(ctx context.Context, userID int64) ([]model.SetPermission, error)
+}
+
+func (m *MockSetPermissionRepo) GrantPermission(ctx context.Context, perm *model.SetPermission) error {
+ if m.GrantPermissionFunc != nil {
+ return m.GrantPermissionFunc(ctx, perm)
+ }
+ return nil
+}
+func (m *MockSetPermissionRepo) RevokePermission(ctx context.Context, setID, userID int64) error {
+ if m.RevokePermissionFunc != nil {
+ return m.RevokePermissionFunc(ctx, setID, userID)
+ }
+ return nil
+}
+func (m *MockSetPermissionRepo) GetPermission(ctx context.Context, setID, userID int64) (*model.SetPermission, error) {
+ if m.GetPermissionFunc != nil {
+ return m.GetPermissionFunc(ctx, setID, userID)
+ }
+ return nil, nil
+}
+func (m *MockSetPermissionRepo) ListPermissionsBySet(ctx context.Context, setID int64) ([]model.SetPermission, error) {
+ if m.ListPermissionsBySetFunc != nil {
+ return m.ListPermissionsBySetFunc(ctx, setID)
+ }
+ return nil, nil
+}
+func (m *MockSetPermissionRepo) ListPermissionsByUser(ctx context.Context, userID int64) ([]model.SetPermission, error) {
+ if m.ListPermissionsByUserFunc != nil {
+ return m.ListPermissionsByUserFunc(ctx, userID)
+ }
+ return nil, nil
+}
+
+// MockMediaRepo is a fake MediaRepo.
+type MockMediaRepo struct {
+ CreateMediaFunc func(ctx context.Context, media *model.Media) (int64, error)
+ GetMediaByIDFunc func(ctx context.Context, id int64) (*model.Media, error)
+ UpdateMediaFunc func(ctx context.Context, media *model.Media) error
+ SoftDeleteMediaFunc func(ctx context.Context, id int64) error
+ RestoreMediaFunc func(ctx context.Context, id int64) error
+ HardDeleteMediaFunc func(ctx context.Context, id int64) error
+ ListMediaFunc func(ctx context.Context, filter MediaFilter) ([]model.Media, error)
+ ListDeletedMediaFunc func(ctx context.Context) ([]model.Media, error)
+ IncrementPlayCountFunc func(ctx context.Context, id int64) error
+}
+
+func (m *MockMediaRepo) CreateMedia(ctx context.Context, media *model.Media) (int64, error) {
+ if m.CreateMediaFunc != nil {
+ return m.CreateMediaFunc(ctx, media)
+ }
+ return 1, nil
+}
+func (m *MockMediaRepo) GetMediaByID(ctx context.Context, id int64) (*model.Media, error) {
+ if m.GetMediaByIDFunc != nil {
+ return m.GetMediaByIDFunc(ctx, id)
+ }
+ return nil, nil
+}
+func (m *MockMediaRepo) UpdateMedia(ctx context.Context, media *model.Media) error {
+ if m.UpdateMediaFunc != nil {
+ return m.UpdateMediaFunc(ctx, media)
+ }
+ return nil
+}
+func (m *MockMediaRepo) SoftDeleteMedia(ctx context.Context, id int64) error {
+ if m.SoftDeleteMediaFunc != nil {
+ return m.SoftDeleteMediaFunc(ctx, id)
+ }
+ return nil
+}
+func (m *MockMediaRepo) RestoreMedia(ctx context.Context, id int64) error {
+ if m.RestoreMediaFunc != nil {
+ return m.RestoreMediaFunc(ctx, id)
+ }
+ return nil
+}
+func (m *MockMediaRepo) HardDeleteMedia(ctx context.Context, id int64) error {
+ if m.HardDeleteMediaFunc != nil {
+ return m.HardDeleteMediaFunc(ctx, id)
+ }
+ return nil
+}
+func (m *MockMediaRepo) ListMedia(ctx context.Context, filter MediaFilter) ([]model.Media, error) {
+ if m.ListMediaFunc != nil {
+ return m.ListMediaFunc(ctx, filter)
+ }
+ return nil, nil
+}
+func (m *MockMediaRepo) ListDeletedMedia(ctx context.Context) ([]model.Media, error) {
+ if m.ListDeletedMediaFunc != nil {
+ return m.ListDeletedMediaFunc(ctx)
+ }
+ return nil, nil
+}
+func (m *MockMediaRepo) IncrementPlayCount(ctx context.Context, id int64) error {
+ if m.IncrementPlayCountFunc != nil {
+ return m.IncrementPlayCountFunc(ctx, id)
+ }
+ return nil
+}
+
+// MockTagRepo is a fake TagRepo.
+type MockTagRepo struct {
+ CreateTagFunc func(ctx context.Context, name string) (int64, error)
+ GetTagByIDFunc func(ctx context.Context, id int64) (*model.Tag, error)
+ GetTagByNameFunc func(ctx context.Context, name string) (*model.Tag, error)
+ ListTagsFunc func(ctx context.Context) ([]model.Tag, error)
+ DeleteTagFunc func(ctx context.Context, id int64) error
+ AssignTagFunc func(ctx context.Context, mediaID, tagID int64) error
+ RemoveTagFunc func(ctx context.Context, mediaID, tagID int64) error
+ ListTagsByMediaFunc func(ctx context.Context, mediaID int64) ([]model.Tag, error)
+}
+
+func (m *MockTagRepo) CreateTag(ctx context.Context, name string) (int64, error) {
+ if m.CreateTagFunc != nil {
+ return m.CreateTagFunc(ctx, name)
+ }
+ return 1, nil
+}
+func (m *MockTagRepo) GetTagByID(ctx context.Context, id int64) (*model.Tag, error) {
+ if m.GetTagByIDFunc != nil {
+ return m.GetTagByIDFunc(ctx, id)
+ }
+ return nil, nil
+}
+func (m *MockTagRepo) GetTagByName(ctx context.Context, name string) (*model.Tag, error) {
+ if m.GetTagByNameFunc != nil {
+ return m.GetTagByNameFunc(ctx, name)
+ }
+ return nil, nil
+}
+func (m *MockTagRepo) ListTags(ctx context.Context) ([]model.Tag, error) {
+ if m.ListTagsFunc != nil {
+ return m.ListTagsFunc(ctx)
+ }
+ return nil, nil
+}
+func (m *MockTagRepo) DeleteTag(ctx context.Context, id int64) error {
+ if m.DeleteTagFunc != nil {
+ return m.DeleteTagFunc(ctx, id)
+ }
+ return nil
+}
+func (m *MockTagRepo) AssignTag(ctx context.Context, mediaID, tagID int64) error {
+ if m.AssignTagFunc != nil {
+ return m.AssignTagFunc(ctx, mediaID, tagID)
+ }
+ return nil
+}
+func (m *MockTagRepo) RemoveTag(c