weee
This commit is contained in:
@@ -11,6 +11,7 @@ type CoverArt struct {
|
||||
ID string
|
||||
AlbumID sql.NullString
|
||||
MediaFileID sql.NullString
|
||||
ArtistID sql.NullString
|
||||
Source string
|
||||
SourcePath string
|
||||
MimeType string
|
||||
@@ -19,7 +20,7 @@ type CoverArt struct {
|
||||
Height int
|
||||
}
|
||||
|
||||
const coverArtSelect = `SELECT id, album_id, media_file_id, source, source_path, mime_type, content_hash, width, height FROM cover_art`
|
||||
const coverArtSelect = `SELECT id, album_id, media_file_id, artist_id, source, source_path, mime_type, content_hash, width, height FROM cover_art`
|
||||
|
||||
func GetCoverArt(db *sql.DB, id string) (*CoverArt, error) {
|
||||
return scanCoverArt(db.QueryRow(coverArtSelect+` WHERE id = ?`, id))
|
||||
@@ -31,7 +32,7 @@ func GetCoverArtForAlbum(db *sql.DB, albumID string) (*CoverArt, error) {
|
||||
|
||||
func scanCoverArt(row *sql.Row) (*CoverArt, error) {
|
||||
var c CoverArt
|
||||
err := row.Scan(&c.ID, &c.AlbumID, &c.MediaFileID, &c.Source, &c.SourcePath, &c.MimeType, &c.ContentHash, &c.Width, &c.Height)
|
||||
err := row.Scan(&c.ID, &c.AlbumID, &c.MediaFileID, &c.ArtistID, &c.Source, &c.SourcePath, &c.MimeType, &c.ContentHash, &c.Width, &c.Height)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -71,6 +72,36 @@ func UpsertAlbumCoverArt(db *sql.DB, albumID, source, sourcePath, mediaFileID, m
|
||||
return GetCoverArt(db, id)
|
||||
}
|
||||
|
||||
// UpsertArtistCoverArt records (or replaces) an artist's photo and links
|
||||
// it as the artist's cover art. manual=true marks it as an admin
|
||||
// override (see database.SetArtistCoverArt); manual=false is the
|
||||
// auto-fetch path, which never overwrites an existing manual override.
|
||||
func UpsertArtistCoverArt(db *sql.DB, artistID, source, sourcePath, mimeType, contentHash string, width, height int, manual bool) (*CoverArt, error) {
|
||||
var id string
|
||||
err := db.QueryRow(`SELECT id FROM cover_art WHERE artist_id = ?`, artistID).Scan(&id)
|
||||
switch {
|
||||
case err == nil:
|
||||
_, err = db.Exec(`UPDATE cover_art SET source=?, source_path=?, mime_type=?, content_hash=?, width=?, height=?, updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
|
||||
source, sourcePath, mimeType, contentHash, width, height, id)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
case errors.Is(err, sql.ErrNoRows):
|
||||
id = idgen.New("ca")
|
||||
_, err = db.Exec(`INSERT INTO cover_art (id, artist_id, source, source_path, mime_type, content_hash, width, height) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
id, artistID, source, sourcePath, mimeType, contentHash, width, height)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
default:
|
||||
return nil, err
|
||||
}
|
||||
if err := SetArtistCoverArt(db, artistID, sql.NullString{String: id, Valid: true}, manual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetCoverArt(db, id)
|
||||
}
|
||||
|
||||
func nullEmptyStr(s string) any {
|
||||
if s == "" {
|
||||
return nil
|
||||
|
||||
@@ -15,6 +15,23 @@ type Artist struct {
|
||||
Name string
|
||||
SortName string
|
||||
MusicBrainzID string
|
||||
Bio string
|
||||
CoverArtID sql.NullString
|
||||
PhotoManual bool
|
||||
}
|
||||
|
||||
const artistSelect = `SELECT id, name, sort_name, musicbrainz_id, bio, cover_art_id, photo_manual FROM artists`
|
||||
|
||||
func scanArtist(row *sql.Row) (*Artist, error) {
|
||||
var a Artist
|
||||
err := row.Scan(&a.ID, &a.Name, &a.SortName, &a.MusicBrainzID, &a.Bio, &a.CoverArtID, &a.PhotoManual)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
// FindOrCreateArtist looks up an artist by (name, musicbrainz_id) and
|
||||
@@ -43,19 +60,11 @@ func FindOrCreateArtist(db *sql.DB, name, sortName, musicBrainzID string) (*Arti
|
||||
}
|
||||
|
||||
func GetArtist(db *sql.DB, id string) (*Artist, error) {
|
||||
var a Artist
|
||||
err := db.QueryRow(`SELECT id, name, sort_name, musicbrainz_id FROM artists WHERE id = ?`, id).Scan(&a.ID, &a.Name, &a.SortName, &a.MusicBrainzID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &a, nil
|
||||
return scanArtist(db.QueryRow(artistSelect+` WHERE id = ?`, id))
|
||||
}
|
||||
|
||||
func ListArtists(db *sql.DB) ([]*Artist, error) {
|
||||
rows, err := db.Query(`SELECT DISTINCT a.id, a.name, a.sort_name, a.musicbrainz_id FROM artists a
|
||||
rows, err := db.Query(`SELECT DISTINCT a.id, a.name, a.sort_name, a.musicbrainz_id, a.bio, a.cover_art_id, a.photo_manual FROM artists a
|
||||
JOIN album_artists aa ON aa.artist_id = a.id
|
||||
ORDER BY COALESCE(NULLIF(a.sort_name, ''), a.name) COLLATE NOCASE`)
|
||||
if err != nil {
|
||||
@@ -65,7 +74,7 @@ func ListArtists(db *sql.DB) ([]*Artist, error) {
|
||||
var out []*Artist
|
||||
for rows.Next() {
|
||||
var a Artist
|
||||
if err := rows.Scan(&a.ID, &a.Name, &a.SortName, &a.MusicBrainzID); err != nil {
|
||||
if err := rows.Scan(&a.ID, &a.Name, &a.SortName, &a.MusicBrainzID, &a.Bio, &a.CoverArtID, &a.PhotoManual); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, &a)
|
||||
@@ -73,6 +82,25 @@ func ListArtists(db *sql.DB) ([]*Artist, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateArtist overwrites an artist's name, sort name, and bio. It never
|
||||
// touches the artist's photo (see SetArtistCoverArt).
|
||||
func UpdateArtist(db *sql.DB, id, name, sortName, bio string) error {
|
||||
_, err := db.Exec(`UPDATE artists SET name = ?, sort_name = ?, bio = ? WHERE id = ?`, name, sortName, bio, id)
|
||||
return err
|
||||
}
|
||||
|
||||
// SetArtistCoverArt links (or unlinks) an artist's cover_art row.
|
||||
// manual=true records an admin override; manual=false is the auto-fetch
|
||||
// path, which is guarded to never overwrite an existing manual override.
|
||||
func SetArtistCoverArt(db *sql.DB, artistID string, coverArtID sql.NullString, manual bool) error {
|
||||
if manual {
|
||||
_, err := db.Exec(`UPDATE artists SET cover_art_id = ?, photo_manual = 1 WHERE id = ?`, nullStr(coverArtID), artistID)
|
||||
return err
|
||||
}
|
||||
_, err := db.Exec(`UPDATE artists SET cover_art_id = ?, photo_manual = 0 WHERE id = ? AND photo_manual = 0`, nullStr(coverArtID), artistID)
|
||||
return err
|
||||
}
|
||||
|
||||
// --- Genres ---
|
||||
|
||||
type Genre struct {
|
||||
@@ -223,6 +251,14 @@ func scanAlbums(rows *sql.Rows) ([]*Album, error) {
|
||||
return out, rows.Err()
|
||||
}
|
||||
|
||||
// UpdateAlbum overwrites an album's editable metadata fields. It never
|
||||
// touches the album's cover art (see SetAlbumCoverArt).
|
||||
func UpdateAlbum(db *sql.DB, id, title, sortTitle string, year, originalYear sql.NullInt64, label string) error {
|
||||
_, err := db.Exec(`UPDATE albums SET title = ?, sort_title = ?, year = ?, original_year = ?, record_label = ? WHERE id = ?`,
|
||||
title, sortTitle, nullInt(year), nullInt(originalYear), label, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func SetAlbumCoverArt(db *sql.DB, albumID, coverArtID string) error {
|
||||
_, err := db.Exec(`UPDATE albums SET cover_art_id = ? WHERE id = ?`, coverArtID, albumID)
|
||||
return err
|
||||
@@ -260,11 +296,12 @@ type Song struct {
|
||||
ReplayGainTrackPeak sql.NullFloat64
|
||||
ReplayGainAlbumGainDB sql.NullFloat64
|
||||
ReplayGainAlbumPeak sql.NullFloat64
|
||||
MetadataLocked bool
|
||||
}
|
||||
|
||||
const songSelect = `SELECT id, media_file_id, album_id, title, sort_title, track_number, track_total, disc_number, disc_total,
|
||||
composer, conductor, isrc, bpm, musicbrainz_track_id, musicbrainz_recording_id,
|
||||
replaygain_track_gain_db, replaygain_track_peak, replaygain_album_gain_db, replaygain_album_peak FROM songs`
|
||||
replaygain_track_gain_db, replaygain_track_peak, replaygain_album_gain_db, replaygain_album_peak, metadata_locked FROM songs`
|
||||
|
||||
func GetSong(db *sql.DB, id string) (*Song, error) {
|
||||
return scanSong(db.QueryRow(songSelect+` WHERE id = ?`, id))
|
||||
@@ -278,7 +315,7 @@ func scanSong(row *sql.Row) (*Song, error) {
|
||||
var s Song
|
||||
err := row.Scan(&s.ID, &s.MediaFileID, &s.AlbumID, &s.Title, &s.SortTitle, &s.TrackNumber, &s.TrackTotal, &s.DiscNumber, &s.DiscTotal,
|
||||
&s.Composer, &s.Conductor, &s.ISRC, &s.BPM, &s.MusicBrainzTrackID, &s.MusicBrainzRecordingID,
|
||||
&s.ReplayGainTrackGainDB, &s.ReplayGainTrackPeak, &s.ReplayGainAlbumGainDB, &s.ReplayGainAlbumPeak)
|
||||
&s.ReplayGainTrackGainDB, &s.ReplayGainTrackPeak, &s.ReplayGainAlbumGainDB, &s.ReplayGainAlbumPeak, &s.MetadataLocked)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
@@ -312,7 +349,7 @@ func scanSongs(rows *sql.Rows) ([]*Song, error) {
|
||||
var s Song
|
||||
if err := rows.Scan(&s.ID, &s.MediaFileID, &s.AlbumID, &s.Title, &s.SortTitle, &s.TrackNumber, &s.TrackTotal, &s.DiscNumber, &s.DiscTotal,
|
||||
&s.Composer, &s.Conductor, &s.ISRC, &s.BPM, &s.MusicBrainzTrackID, &s.MusicBrainzRecordingID,
|
||||
&s.ReplayGainTrackGainDB, &s.ReplayGainTrackPeak, &s.ReplayGainAlbumGainDB, &s.ReplayGainAlbumPeak); err != nil {
|
||||
&s.ReplayGainTrackGainDB, &s.ReplayGainTrackPeak, &s.ReplayGainAlbumGainDB, &s.ReplayGainAlbumPeak, &s.MetadataLocked); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out = append(out, &s)
|
||||
@@ -354,14 +391,29 @@ func UpsertSong(db *sql.DB, in UpsertSongInput) (*Song, error) {
|
||||
return nil, err
|
||||
}
|
||||
if existing != nil {
|
||||
_, err = db.Exec(`UPDATE songs SET album_id=?, title=?, sort_title=?, track_number=?, track_total=?, disc_number=?, disc_total=?, disc_subtitle=?,
|
||||
composer=?, conductor=?, comment=?, isrc=?, bpm=?, musicbrainz_track_id=?, musicbrainz_recording_id=?,
|
||||
replaygain_track_gain_db=?, replaygain_track_peak=?, replaygain_album_gain_db=?, replaygain_album_peak=?,
|
||||
updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
|
||||
nullStr(in.AlbumID), in.Title, in.SortTitle, nullInt(in.TrackNumber), nullInt(in.TrackTotal), nullInt(in.DiscNumber), nullInt(in.DiscTotal), in.DiscSubtitle,
|
||||
in.Composer, in.Conductor, in.Comment, in.ISRC, nullInt(in.BPM), in.MusicBrainzTrackID, in.MusicBrainzRecordingID,
|
||||
nullFloat(in.ReplayGainTrackGainDB), nullFloat(in.ReplayGainTrackPeak), nullFloat(in.ReplayGainAlbumGainDB), nullFloat(in.ReplayGainAlbumPeak),
|
||||
existing.ID)
|
||||
if existing.MetadataLocked {
|
||||
// An admin has hand-edited title/sort_title/track_number/
|
||||
// disc_number/composer/conductor for this song; leave those
|
||||
// alone and only refresh the fields that come straight off
|
||||
// the file itself and were never exposed for editing.
|
||||
_, err = db.Exec(`UPDATE songs SET album_id=?, track_total=?, disc_total=?, disc_subtitle=?,
|
||||
comment=?, isrc=?, bpm=?, musicbrainz_track_id=?, musicbrainz_recording_id=?,
|
||||
replaygain_track_gain_db=?, replaygain_track_peak=?, replaygain_album_gain_db=?, replaygain_album_peak=?,
|
||||
updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
|
||||
nullStr(in.AlbumID), nullInt(in.TrackTotal), nullInt(in.DiscTotal), in.DiscSubtitle,
|
||||
in.Comment, in.ISRC, nullInt(in.BPM), in.MusicBrainzTrackID, in.MusicBrainzRecordingID,
|
||||
nullFloat(in.ReplayGainTrackGainDB), nullFloat(in.ReplayGainTrackPeak), nullFloat(in.ReplayGainAlbumGainDB), nullFloat(in.ReplayGainAlbumPeak),
|
||||
existing.ID)
|
||||
} else {
|
||||
_, err = db.Exec(`UPDATE songs SET album_id=?, title=?, sort_title=?, track_number=?, track_total=?, disc_number=?, disc_total=?, disc_subtitle=?,
|
||||
composer=?, conductor=?, comment=?, isrc=?, bpm=?, musicbrainz_track_id=?, musicbrainz_recording_id=?,
|
||||
replaygain_track_gain_db=?, replaygain_track_peak=?, replaygain_album_gain_db=?, replaygain_album_peak=?,
|
||||
updated_at=strftime('%Y-%m-%dT%H:%M:%fZ','now') WHERE id=?`,
|
||||
nullStr(in.AlbumID), in.Title, in.SortTitle, nullInt(in.TrackNumber), nullInt(in.TrackTotal), nullInt(in.DiscNumber), nullInt(in.DiscTotal), in.DiscSubtitle,
|
||||
in.Composer, in.Conductor, in.Comment, in.ISRC, nullInt(in.BPM), in.MusicBrainzTrackID, in.MusicBrainzRecordingID,
|
||||
nullFloat(in.ReplayGainTrackGainDB), nullFloat(in.ReplayGainTrackPeak), nullFloat(in.ReplayGainAlbumGainDB), nullFloat(in.ReplayGainAlbumPeak),
|
||||
existing.ID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -382,6 +434,15 @@ func UpsertSong(db *sql.DB, in UpsertSongInput) (*Song, error) {
|
||||
return GetSong(db, id)
|
||||
}
|
||||
|
||||
// UpdateSongMetadata overwrites a song's editable fields. locked=true
|
||||
// sets metadata_locked so a later library rescan (UpsertSong) leaves
|
||||
// these fields alone instead of overwriting them from the file's tags.
|
||||
func UpdateSongMetadata(db *sql.DB, id, title, sortTitle string, trackNumber, discNumber sql.NullInt64, composer, conductor string, locked bool) error {
|
||||
_, err := db.Exec(`UPDATE songs SET title=?, sort_title=?, track_number=?, disc_number=?, composer=?, conductor=?, metadata_locked=? WHERE id=?`,
|
||||
title, sortTitle, nullInt(trackNumber), nullInt(discNumber), composer, conductor, locked, id)
|
||||
return err
|
||||
}
|
||||
|
||||
func ClearSongArtists(db *sql.DB, songID string) error {
|
||||
_, err := db.Exec(`DELETE FROM song_artists WHERE song_id = ?`, songID)
|
||||
return err
|
||||
|
||||
@@ -111,7 +111,7 @@ func ListGenreStats(db *sql.DB) ([]GenreStat, error) {
|
||||
func SongsByGenre(db *sql.DB, genreName string, limit, offset int) ([]*Song, error) {
|
||||
rows, err := db.Query(`SELECT s.id, s.media_file_id, s.album_id, s.title, s.sort_title, s.track_number, s.track_total, s.disc_number, s.disc_total,
|
||||
s.composer, s.conductor, s.isrc, s.bpm, s.musicbrainz_track_id, s.musicbrainz_recording_id,
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak, s.metadata_locked
|
||||
FROM songs s
|
||||
JOIN song_genres sg ON sg.song_id = s.id
|
||||
JOIN genres g ON g.id = sg.genre_id
|
||||
|
||||
@@ -37,7 +37,7 @@ func FindArtistByName(db *sql.DB, name string) (*Artist, error) {
|
||||
func TopSongsByArtist(db *sql.DB, artistID string, limit int) ([]*Song, error) {
|
||||
rows, err := db.Query(`SELECT s.id, s.media_file_id, s.album_id, s.title, s.sort_title, s.track_number, s.track_total, s.disc_number, s.disc_total,
|
||||
s.composer, s.conductor, s.isrc, s.bpm, s.musicbrainz_track_id, s.musicbrainz_recording_id,
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak, s.metadata_locked
|
||||
FROM songs s
|
||||
JOIN song_artists sa ON sa.song_id = s.id
|
||||
LEFT JOIN (SELECT song_id, COUNT(*) AS plays FROM playback_history GROUP BY song_id) ph ON ph.song_id = s.id
|
||||
@@ -58,7 +58,7 @@ func TopSongsByArtist(db *sql.DB, artistID string, limit int) ([]*Song, error) {
|
||||
func SimilarSongsByGenre(db *sql.DB, seedSongID string, limit int) ([]*Song, error) {
|
||||
rows, err := db.Query(`SELECT DISTINCT s.id, s.media_file_id, s.album_id, s.title, s.sort_title, s.track_number, s.track_total, s.disc_number, s.disc_total,
|
||||
s.composer, s.conductor, s.isrc, s.bpm, s.musicbrainz_track_id, s.musicbrainz_recording_id,
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak, s.metadata_locked
|
||||
FROM songs s
|
||||
JOIN song_genres sg ON sg.song_id = s.id
|
||||
WHERE sg.genre_id IN (SELECT genre_id FROM song_genres WHERE song_id = ?)
|
||||
@@ -78,7 +78,7 @@ func SimilarSongsByGenre(db *sql.DB, seedSongID string, limit int) ([]*Song, err
|
||||
func SimilarSongsByArtist(db *sql.DB, artistID string, limit int) ([]*Song, error) {
|
||||
rows, err := db.Query(`SELECT DISTINCT s.id, s.media_file_id, s.album_id, s.title, s.sort_title, s.track_number, s.track_total, s.disc_number, s.disc_total,
|
||||
s.composer, s.conductor, s.isrc, s.bpm, s.musicbrainz_track_id, s.musicbrainz_recording_id,
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak
|
||||
s.replaygain_track_gain_db, s.replaygain_track_peak, s.replaygain_album_gain_db, s.replaygain_album_peak, s.metadata_locked
|
||||
FROM songs s
|
||||
JOIN song_genres sg ON sg.song_id = s.id
|
||||
WHERE sg.genre_id IN (
|
||||
|
||||
Reference in New Issue
Block a user