media.go
| 1 | package repository |
| 2 | |
| 3 | import ( |
| 4 | "database/sql" |
| 5 | "strings" |
| 6 | "vidarchive/internal/models" |
| 7 | ) |
| 8 | |
| 9 | type MediaRepository struct { |
| 10 | db *sql.DB |
| 11 | } |
| 12 | |
| 13 | func NewMediaRepository(db *sql.DB) *MediaRepository { |
| 14 | return &MediaRepository{db: db} |
| 15 | } |
| 16 | |
| 17 | func (r *MediaRepository) Create(m *models.Media) error { |
| 18 | result, err := r.db.Exec( |
| 19 | `INSERT INTO media (url, title, description, duration, filepath, relative_path, is_audio, has_embedded_thumbnail, info_json_path, playlist_id, playlist_index) |
| 20 | VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, |
| 21 | m.URL, m.Title, m.Description, m.Duration, m.Filepath, m.RelativePath, |
| 22 | boolToInt(m.IsAudio), boolToInt(m.HasEmbeddedThumbnail), m.InfoJSONPath, |
| 23 | m.PlaylistID, m.PlaylistIndex, |
| 24 | ) |
| 25 | if err != nil { |
| 26 | return err |
| 27 | } |
| 28 | m.ID, _ = result.LastInsertId() |
| 29 | return nil |
| 30 | } |
| 31 | |
| 32 | func (r *MediaRepository) GetByID(id int64) (*models.Media, error) { |
| 33 | row := r.db.QueryRow( |
| 34 | `SELECT id, url, title, description, duration, filepath, relative_path, is_audio, has_embedded_thumbnail, info_json_path, playlist_id, playlist_index, created_at |
| 35 | FROM media WHERE id = ?`, id, |
| 36 | ) |
| 37 | return scanMedia(row) |
| 38 | } |
| 39 | |
| 40 | func (r *MediaRepository) GetAll(path string, sortBy, filter string) ([]*models.Media, error) { |
| 41 | query := `SELECT id, url, title, description, duration, filepath, relative_path, is_audio, has_embedded_thumbnail, info_json_path, playlist_id, playlist_index, created_at |
| 42 | FROM media WHERE relative_path LIKE ?` |
| 43 | args := []interface{}{path + "%"} |
| 44 | |
| 45 | // For non-root paths, restrict to immediate items (not nested inside subfolders). |
| 46 | // For root path, only show media not inside any folder. |
| 47 | if path != "" { |
| 48 | query += ` AND relative_path NOT LIKE ?` |
| 49 | args = append(args, path+"%/%") |
| 50 | } else { |
| 51 | query += ` AND relative_path NOT LIKE ?` |
| 52 | args = append(args, "%/%") |
| 53 | } |
| 54 | |
| 55 | if filter != "" { |
| 56 | query += ` AND (title LIKE ? OR description LIKE ?)` |
| 57 | args = append(args, "%"+filter+"%", "%"+filter+"%") |
| 58 | } |
| 59 | |
| 60 | switch sortBy { |
| 61 | case "title": |
| 62 | query += ` ORDER BY title` |
| 63 | case "date": |
| 64 | query += ` ORDER BY created_at DESC` |
| 65 | case "duration": |
| 66 | query += ` ORDER BY duration DESC` |
| 67 | default: |
| 68 | query += ` ORDER BY created_at DESC` |
| 69 | } |
| 70 | |
| 71 | rows, err := r.db.Query(query, args...) |
| 72 | if err != nil { |
| 73 | return nil, err |
| 74 | } |
| 75 | defer rows.Close() |
| 76 | |
| 77 | var media []*models.Media |
| 78 | for rows.Next() { |
| 79 | m, err := scanMedia(rows) |
| 80 | if err != nil { |
| 81 | return nil, err |
| 82 | } |
| 83 | media = append(media, m) |
| 84 | } |
| 85 | return media, rows.Err() |
| 86 | } |
| 87 | |
| 88 | func (r *MediaRepository) GetByRelativePath(relPath string) (*models.Media, error) { |
| 89 | row := r.db.QueryRow( |
| 90 | `SELECT id, url, title, description, duration, filepath, relative_path, is_audio, has_embedded_thumbnail, info_json_path, playlist_id, playlist_index, created_at |
| 91 | FROM media WHERE relative_path = ?`, relPath, |
| 92 | ) |
| 93 | return scanMedia(row) |
| 94 | } |
| 95 | |
| 96 | func (r *MediaRepository) Delete(id int64) error { |
| 97 | _, err := r.db.Exec(`DELETE FROM media WHERE id = ?`, id) |
| 98 | return err |
| 99 | } |
| 100 | |
| 101 | func (r *MediaRepository) GetFolders(parentPath string) ([]string, error) { |
| 102 | rows, err := r.db.Query( |
| 103 | `SELECT relative_path FROM media WHERE relative_path LIKE ?`, |
| 104 | parentPath+"%", |
| 105 | ) |
| 106 | if err != nil { |
| 107 | return nil, err |
| 108 | } |
| 109 | defer rows.Close() |
| 110 | |
| 111 | folders := make(map[string]bool) |
| 112 | for rows.Next() { |
| 113 | var relPath string |
| 114 | if err := rows.Scan(&relPath); err != nil { |
| 115 | return nil, err |
| 116 | } |
| 117 | // Get the folder part after parentPath |
| 118 | remaining := strings.TrimPrefix(relPath, parentPath) |
| 119 | remaining = strings.TrimPrefix(remaining, "/") |
| 120 | if idx := strings.Index(remaining, "/"); idx >= 0 { |
| 121 | folder := remaining[:idx] |
| 122 | if folder != "" { |
| 123 | folders[folder] = true |
| 124 | } |
| 125 | } |
| 126 | } |
| 127 | |
| 128 | var result []string |
| 129 | for f := range folders { |
| 130 | result = append(result, f) |
| 131 | } |
| 132 | return result, rows.Err() |
| 133 | } |
| 134 | |
| 135 | func scanMedia(row interface{ Scan(...interface{}) error }) (*models.Media, error) { |
| 136 | var m models.Media |
| 137 | var isAudio, hasThumb int |
| 138 | err := row.Scan( |
| 139 | &m.ID, &m.URL, &m.Title, &m.Description, &m.Duration, &m.Filepath, |
| 140 | &m.RelativePath, &isAudio, &hasThumb, &m.InfoJSONPath, |
| 141 | &m.PlaylistID, &m.PlaylistIndex, &m.CreatedAt, |
| 142 | ) |
| 143 | if err != nil { |
| 144 | return nil, err |
| 145 | } |
| 146 | m.IsAudio = isAudio == 1 |
| 147 | m.HasEmbeddedThumbnail = hasThumb == 1 |
| 148 | return &m, nil |
| 149 | } |
| 150 |