242 lines
7.8 KiB
Go
242 lines
7.8 KiB
Go
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, 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
|
|
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)
|
|
}
|