package repository import ( "butterfliu/internal/model" "database/sql" ) // SongDetail is a DTO for API responses with joined artist/album names. // Kept here temporarily for backward compatibility with service layer. // Consider moving to model package in future refactoring. type SongDetail = model.SongDetail type LibraryRepository struct { db *sql.DB } func NewLibraryRepository(db *sql.DB) *LibraryRepository { return &LibraryRepository{db: db} } func (r *LibraryRepository) GetAll() ([]model.Library, error) { rows, err := r.db.Query("SELECT id, name, path, COALESCE(created_at, '1970-01-01T00:00:00Z'), COALESCE(updated_at, '1970-01-01T00:00:00Z') FROM libraries") if err != nil { return nil, err } defer rows.Close() libraries := []model.Library{} for rows.Next() { var lib model.Library if err := rows.Scan(&lib.ID, &lib.Name, &lib.Path, &lib.CreatedAt, &lib.UpdatedAt); err != nil { return nil, err } libraries = append(libraries, lib) } return libraries, nil } func (r *LibraryRepository) GetByID(id int) (model.Library, error) { row := r.db.QueryRow("SELECT id, name, path, COALESCE(created_at, '1970-01-01T00:00:00Z'), COALESCE(updated_at, '1970-01-01T00:00:00Z') FROM libraries WHERE id = ?", id) var lib model.Library if err := row.Scan(&lib.ID, &lib.Name, &lib.Path, &lib.CreatedAt, &lib.UpdatedAt); err != nil { return model.Library{}, err } return lib, nil } func (r *LibraryRepository) Create(name, path string) (model.Library, error) { result, err := r.db.Exec("INSERT INTO libraries (name, path) VALUES (?, ?)", name, path) if err != nil { return model.Library{}, err } id, err := result.LastInsertId() if err != nil { return model.Library{}, err } return model.Library{ ID: int(id), Name: name, Path: path, }, nil } func (r *LibraryRepository) UpdateName(id int, name string) error { _, err := r.db.Exec("UPDATE libraries SET name = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", name, id) return err } func (r *LibraryRepository) UpdatePath(id int, path string) error { _, err := r.db.Exec("UPDATE libraries SET path = ?, updated_at = CURRENT_TIMESTAMP WHERE id = ?", path, id) return err } func (r *LibraryRepository) Delete(id int) error { tx, err := r.db.Begin() if err != nil { return err } defer tx.Rollback() // Delete the library — CASCADE removes media_files and songs automatically _, err = tx.Exec("DELETE FROM libraries WHERE id = ?", id) if err != nil { return err } // Clean up orphan albums (no songs reference them) _, err = tx.Exec(` DELETE FROM albums WHERE id NOT IN ( SELECT DISTINCT album_id FROM songs WHERE album_id IS NOT NULL ) `) if err != nil { return err } // Clean up orphan artists (no songs and no albums reference them) _, err = tx.Exec(` DELETE FROM artists WHERE id NOT IN ( SELECT DISTINCT artist_id FROM songs WHERE artist_id IS NOT NULL UNION SELECT DISTINCT artist_id FROM albums WHERE artist_id IS NOT NULL ) `) if err != nil { return err } return tx.Commit() } func (r *LibraryRepository) GetSongsByLibraryWithDetails(libraryID int) ([]SongDetail, error) { rows, err := r.db.Query(` SELECT s.id, s.title, a.name as artist_name, al.title as album_title, s.duration, mf.path FROM songs s INNER JOIN media_files mf ON s.media_file_id = mf.id INNER JOIN artists a ON s.artist_id = a.id INNER JOIN albums al ON s.album_id = al.id WHERE mf.library_id = ? ORDER BY s.title `, libraryID) if err != nil { return nil, err } defer rows.Close() songs := []SongDetail{} for rows.Next() { var song SongDetail if err := rows.Scan(&song.ID, &song.Title, &song.Artist, &song.Album, &song.Duration, &song.Path); err != nil { return nil, err } songs = append(songs, song) } return songs, nil } func (r *LibraryRepository) GetMediaFileByPath(path string) (model.MediaFile, error) { row := r.db.QueryRow(` SELECT id, path, library_id, COALESCE(file_size, 0), COALESCE(format, ''), COALESCE(bit_rate, 0), COALESCE(sample_rate, 0), COALESCE(created_at, '1970-01-01T00:00:00Z'), COALESCE(updated_at, '1970-01-01T00:00:00Z') FROM media_files WHERE path = ? `) var mediaFile model.MediaFile if err := row.Scan( &mediaFile.ID, &mediaFile.Path, &mediaFile.LibraryID, &mediaFile.FileSize, &mediaFile.Format, &mediaFile.BitRate, &mediaFile.SampleRate, &mediaFile.CreatedAt, &mediaFile.UpdatedAt, ); err != nil { return model.MediaFile{}, err } return mediaFile, nil } func (r *LibraryRepository) CreateMediaFile(path string, libraryID int) (model.MediaFile, error) { result, err := r.db.Exec("INSERT INTO media_files (path, library_id) VALUES (?, ?)", path, libraryID) if err != nil { return model.MediaFile{}, err } id, err := result.LastInsertId() if err != nil { return model.MediaFile{}, err } return model.MediaFile{ ID: int(id), Path: path, LibraryID: libraryID, }, nil } func (r *LibraryRepository) GetArtistByName(name string) (model.Artist, error) { row := r.db.QueryRow("SELECT id, name FROM artists WHERE name = ?", name) var artist model.Artist if err := row.Scan(&artist.ID, &artist.Name); err != nil { return model.Artist{}, err } return artist, nil } func (r *LibraryRepository) CreateArtist(name string) (model.Artist, error) { result, err := r.db.Exec("INSERT INTO artists (name) VALUES (?)", name) if err != nil { return model.Artist{}, err } id, err := result.LastInsertId() if err != nil { return model.Artist{}, err } return model.Artist{ ID: int(id), Name: name, }, nil } func (r *LibraryRepository) GetAlbumByTitleAndArtist(title string, artistID int) (model.Album, error) { row := r.db.QueryRow(` SELECT id, title, artist_id, COALESCE(cover, ''), COALESCE(year, 0) FROM albums WHERE title = ? AND artist_id = ? `) var album model.Album if err := row.Scan(&album.ID, &album.Title, &album.ArtistID, &album.Cover, &album.Year); err != nil { return model.Album{}, err } return album, nil } func (r *LibraryRepository) CreateAlbum(title string, artistID int) (model.Album, error) { result, err := r.db.Exec("INSERT INTO albums (title, artist_id) VALUES (?, ?)", title, artistID) if err != nil { return model.Album{}, err } id, err := result.LastInsertId() if err != nil { return model.Album{}, err } return model.Album{ ID: int(id), Title: title, ArtistID: artistID, }, nil } func (r *LibraryRepository) CreateSong(title string, artistID, albumID, duration, mediaFileID int) (model.Song, error) { result, err := r.db.Exec( "INSERT INTO songs (title, artist_id, album_id, duration, media_file_id) VALUES (?, ?, ?, ?, ?)", title, artistID, albumID, duration, mediaFileID, ) if err != nil { return model.Song{}, err } id, err := result.LastInsertId() if err != nil { return model.Song{}, err } return model.Song{ ID: int(id), Title: title, ArtistID: artistID, AlbumID: albumID, Duration: duration, MediaFileID: mediaFileID, }, nil } func (r *LibraryRepository) GetArtists() ([]model.Artist, error) { rows, err := r.db.Query("SELECT id, name FROM artists") if err != nil { return nil, err } defer rows.Close() artists := []model.Artist{} for rows.Next() { var artist model.Artist if err := rows.Scan(&artist.ID, &artist.Name); err != nil { return nil, err } artists = append(artists, artist) } return artists, nil } func (r *LibraryRepository) GetAlbums() ([]model.Album, error) { rows, err := r.db.Query("SELECT id, title, artist_id FROM albums") if err != nil { return nil, err } defer rows.Close() albums := []model.Album{} for rows.Next() { var album model.Album if err := rows.Scan(&album.ID, &album.Title, &album.ArtistID); err != nil { return nil, err } albums = append(albums, album) } return albums, nil }