package database import "database/sql" // This file collects small aggregate/lookup queries specific to shaping // data for the Subsonic REST API (internal/subsonic), which needs // things like song counts and "primary genre" that the native web UI // does not. // AlbumStats returns an album's song count and total duration in // seconds. func AlbumStats(db *sql.DB, albumID string) (songCount int, totalDuration float64, err error) { err = db.QueryRow(`SELECT COUNT(*), COALESCE(SUM(mf.duration_seconds), 0) FROM songs s JOIN media_files mf ON mf.id = s.media_file_id WHERE s.album_id = ?`, albumID).Scan(&songCount, &totalDuration) return } // ArtistAlbumCount returns how many albums an artist is credited on. func ArtistAlbumCount(db *sql.DB, artistID string) (int, error) { var n int err := db.QueryRow(`SELECT COUNT(*) FROM album_artists WHERE artist_id = ?`, artistID).Scan(&n) return n, err } // ArtistCoverArtID returns the cover_art id of any one album by the // artist that has cover art, or "" if none do. func ArtistCoverArtID(db *sql.DB, artistID string) (string, error) { var id string err := db.QueryRow(`SELECT al.cover_art_id FROM albums al JOIN album_artists aa ON aa.album_id = al.id WHERE aa.artist_id = ? AND al.cover_art_id IS NOT NULL LIMIT 1`, artistID).Scan(&id) if err == sql.ErrNoRows { return "", nil } return id, err } // FirstGenreForSong returns one genre name for a song, or "" if it has // none. func FirstGenreForSong(db *sql.DB, songID string) (string, error) { var name string err := db.QueryRow(`SELECT g.name FROM song_genres sg JOIN genres g ON g.id = sg.genre_id WHERE sg.song_id = ? ORDER BY g.name LIMIT 1`, songID).Scan(&name) if err == sql.ErrNoRows { return "", nil } return name, err } // FirstGenreForAlbum returns one genre name for an album, or "" if it // has none directly tagged (callers typically fall back to a song's // genre). func FirstGenreForAlbum(db *sql.DB, albumID string) (string, error) { var name string err := db.QueryRow(`SELECT g.name FROM album_genres ag JOIN genres g ON g.id = ag.genre_id WHERE ag.album_id = ? ORDER BY g.name LIMIT 1`, albumID).Scan(&name) if err == sql.ErrNoRows { return "", nil } return name, err } // AlbumArtistName returns the display name for an album: its primary // album artist if set, otherwise "" (a "Various Artists"-style album // with no single primary artist). func AlbumArtistName(db *sql.DB, album *Album) (string, error) { if !album.PrimaryAlbumArtistID.Valid { return "", nil } artist, err := GetArtist(db, album.PrimaryAlbumArtistID.String) if err != nil { if err == ErrNotFound { return "", nil } return "", err } return artist.Name, nil } // GenreStats aggregates per-genre song and album counts for // getGenres. type GenreStat struct { Name string SongCount int AlbumCount int } func ListGenreStats(db *sql.DB) ([]GenreStat, error) { rows, err := db.Query(`SELECT g.name, (SELECT COUNT(*) FROM song_genres sg WHERE sg.genre_id = g.id) AS song_count, (SELECT COUNT(*) FROM album_genres ag WHERE ag.genre_id = g.id) AS album_count FROM genres g ORDER BY g.name COLLATE NOCASE`) if err != nil { return nil, err } defer rows.Close() var out []GenreStat for rows.Next() { var g GenreStat if err := rows.Scan(&g.Name, &g.SongCount, &g.AlbumCount); err != nil { return nil, err } out = append(out, g) } return out, rows.Err() } // SongsByGenre returns up to limit songs tagged with the given genre // name, offset for pagination. 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 FROM songs s JOIN song_genres sg ON sg.song_id = s.id JOIN genres g ON g.id = sg.genre_id WHERE g.name = ? ORDER BY s.id LIMIT ? OFFSET ?`, genreName, limit, offset) if err != nil { return nil, err } defer rows.Close() return scanSongs(rows) } // RandomSongIDs returns up to limit random song IDs, optionally // restricted to a genre and/or year range. func RandomSongIDs(db *sql.DB, limit int, genre string, fromYear, toYear int) ([]string, error) { query := `SELECT DISTINCT s.id FROM songs s` var args []any var where []string if genre != "" { query += ` JOIN song_genres sg ON sg.song_id = s.id JOIN genres g ON g.id = sg.genre_id` where = append(where, `g.name = ?`) args = append(args, genre) } if fromYear != 0 || toYear != 0 { query += ` JOIN albums al ON al.id = s.album_id` if fromYear != 0 { where = append(where, `al.year >= ?`) args = append(args, fromYear) } if toYear != 0 { where = append(where, `al.year <= ?`) args = append(args, toYear) } } if len(where) > 0 { query += ` WHERE ` for i, w := range where { if i > 0 { query += ` AND ` } query += w } } query += ` ORDER BY RANDOM() LIMIT ?` args = append(args, limit) rows, err := db.Query(query, args...) if err != nil { return nil, err } defer rows.Close() var out []string for rows.Next() { var id string if err := rows.Scan(&id); err != nil { return nil, err } out = append(out, id) } return out, rows.Err() } // AlbumsByYearRange lists albums sorted by year for getAlbumList's // byYear type. func AlbumsByYearRange(db *sql.DB, fromYear, toYear, limit, offset int) ([]*Album, error) { order := "ASC" from, to := fromYear, toYear if from > to { from, to = to, from order = "DESC" } rows, err := db.Query(albumSelect+` WHERE year >= ? AND year <= ? ORDER BY year `+order+`, sort_title LIMIT ? OFFSET ?`, from, to, limit, offset) if err != nil { return nil, err } defer rows.Close() return scanAlbums(rows) } // AlbumsByGenre lists albums tagged with the given genre. func AlbumsByGenre(db *sql.DB, genre string, limit, offset int) ([]*Album, error) { rows, err := db.Query(`SELECT DISTINCT al.id, al.title, al.sort_title, al.year, al.original_year, al.record_label, al.musicbrainz_id, al.primary_album_artist_id, al.cover_art_id, al.created_at FROM albums al JOIN album_genres ag ON ag.album_id = al.id JOIN genres g ON g.id = ag.genre_id WHERE g.name = ? ORDER BY al.sort_title COLLATE NOCASE LIMIT ? OFFSET ?`, genre, limit, offset) if err != nil { return nil, err } defer rows.Close() return scanAlbums(rows) } // RandomAlbums returns up to limit random albums. func RandomAlbums(db *sql.DB, limit int) ([]*Album, error) { rows, err := db.Query(albumSelect+` ORDER BY RANDOM() LIMIT ?`, limit) if err != nil { return nil, err } defer rows.Close() return scanAlbums(rows) } // RecentlyAddedAlbums lists albums by creation order (newest first). func RecentlyAddedAlbums(db *sql.DB, limit, offset int) ([]*Album, error) { rows, err := db.Query(albumSelect+` ORDER BY created_at DESC LIMIT ? OFFSET ?`, limit, offset) if err != nil { return nil, err } defer rows.Close() return scanAlbums(rows) } // AlbumsAlphabetical lists albums by title or artist name. func AlbumsAlphabetical(db *sql.DB, byArtist bool, limit, offset int) ([]*Album, error) { order := "al.sort_title COLLATE NOCASE" join := "" if byArtist { join = "LEFT JOIN artists ar ON ar.id = al.primary_album_artist_id" order = "COALESCE(ar.sort_name, ar.name, al.sort_title) COLLATE NOCASE, al.sort_title COLLATE NOCASE" } rows, err := db.Query(`SELECT al.id, al.title, al.sort_title, al.year, al.original_year, al.record_label, al.musicbrainz_id, al.primary_album_artist_id, al.cover_art_id, al.created_at FROM albums al `+join+` ORDER BY `+order+` LIMIT ? OFFSET ?`, limit, offset) if err != nil { return nil, err } defer rows.Close() return scanAlbums(rows) }