Subsonic: browsing endpoints (getMusicFolders, getIndexes, getMusicDirectory, getGenres, getArtists/Artist, getAlbum, getSong, getVideos/VideoInfo, getArtistInfo/2, getAlbumInfo/2, getSimilarSongs/2, getTopSongs)
This commit is contained in:
@@ -3,6 +3,7 @@ package database
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"git.noroutes.net/NoRoutes/vibetunes/internal/idgen"
|
||||
)
|
||||
@@ -128,6 +129,7 @@ type Album struct {
|
||||
MusicBrainzID string
|
||||
PrimaryAlbumArtistID sql.NullString
|
||||
CoverArtID sql.NullString
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// FindOrCreateAlbum resolves an album by (title, primary album artist),
|
||||
@@ -167,7 +169,7 @@ func FindOrCreateAlbum(db *sql.DB, title string, primaryArtistID sql.NullString,
|
||||
return GetAlbum(db, id)
|
||||
}
|
||||
|
||||
const albumSelect = `SELECT id, title, sort_title, year, original_year, record_label, musicbrainz_id, primary_album_artist_id, cover_art_id FROM albums`
|
||||
const albumSelect = `SELECT id, title, sort_title, year, original_year, record_label, musicbrainz_id, primary_album_artist_id, cover_art_id, created_at FROM albums`
|
||||
|
||||
func GetAlbum(db *sql.DB, id string) (*Album, error) {
|
||||
return scanAlbum(db.QueryRow(albumSelect+` WHERE id = ?`, id))
|
||||
@@ -175,13 +177,15 @@ func GetAlbum(db *sql.DB, id string) (*Album, error) {
|
||||
|
||||
func scanAlbum(row *sql.Row) (*Album, error) {
|
||||
var a Album
|
||||
err := row.Scan(&a.ID, &a.Title, &a.SortTitle, &a.Year, &a.OriginalYear, &a.RecordLabel, &a.MusicBrainzID, &a.PrimaryAlbumArtistID, &a.CoverArtID)
|
||||
var createdAt string
|
||||
err := row.Scan(&a.ID, &a.Title, &a.SortTitle, &a.Year, &a.OriginalYear, &a.RecordLabel, &a.MusicBrainzID, &a.PrimaryAlbumArtistID, &a.CoverArtID, &createdAt)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.CreatedAt, _ = parseTime(createdAt)
|
||||
return &a, nil
|
||||
}
|
||||
|
||||
@@ -195,7 +199,7 @@ func ListAlbums(db *sql.DB, limit, offset int) ([]*Album, error) {
|
||||
}
|
||||
|
||||
func ListAlbumsByArtist(db *sql.DB, artistID string) ([]*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
|
||||
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_artists aa ON aa.album_id = al.id
|
||||
WHERE aa.artist_id = ? ORDER BY al.year, al.sort_title COLLATE NOCASE`, artistID)
|
||||
if err != nil {
|
||||
@@ -209,9 +213,11 @@ func scanAlbums(rows *sql.Rows) ([]*Album, error) {
|
||||
var out []*Album
|
||||
for rows.Next() {
|
||||
var a Album
|
||||
if err := rows.Scan(&a.ID, &a.Title, &a.SortTitle, &a.Year, &a.OriginalYear, &a.RecordLabel, &a.MusicBrainzID, &a.PrimaryAlbumArtistID, &a.CoverArtID); err != nil {
|
||||
var createdAt string
|
||||
if err := rows.Scan(&a.ID, &a.Title, &a.SortTitle, &a.Year, &a.OriginalYear, &a.RecordLabel, &a.MusicBrainzID, &a.PrimaryAlbumArtistID, &a.CoverArtID, &createdAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
a.CreatedAt, _ = parseTime(createdAt)
|
||||
out = append(out, &a)
|
||||
}
|
||||
return out, rows.Err()
|
||||
|
||||
@@ -0,0 +1,241 @@
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
package database
|
||||
|
||||
import "database/sql"
|
||||
|
||||
// ListVideoMediaFiles returns every media file the scanner identified as
|
||||
// a video (as opposed to audio) track.
|
||||
func ListVideoMediaFiles(db *sql.DB) ([]*MediaFile, error) {
|
||||
rows, err := db.Query(mediaFileSelect + ` WHERE kind = 'video' AND missing = 0 ORDER BY relative_path`)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanMediaFiles(rows)
|
||||
}
|
||||
|
||||
// FindArtistByName looks up an artist by exact, case-insensitive name —
|
||||
// used by endpoints that take an artist name rather than ID (getTopSongs).
|
||||
func FindArtistByName(db *sql.DB, name string) (*Artist, error) {
|
||||
var id string
|
||||
err := db.QueryRow(`SELECT id FROM artists WHERE name = ? COLLATE NOCASE LIMIT 1`, name).Scan(&id)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, ErrNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return GetArtist(db, id)
|
||||
}
|
||||
|
||||
// TopSongsByArtist returns an artist's songs ordered by total play count
|
||||
// (across all users) descending, falling back to any order for songs
|
||||
// that have never been played, up to limit.
|
||||
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
|
||||
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
|
||||
WHERE sa.artist_id = ?
|
||||
GROUP BY s.id
|
||||
ORDER BY COALESCE(MAX(ph.plays), 0) DESC, s.title COLLATE NOCASE
|
||||
LIMIT ?`, artistID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanSongs(rows)
|
||||
}
|
||||
|
||||
// SimilarSongsByGenre returns up to limit random songs sharing at least
|
||||
// one genre with seedSongID, excluding the seed itself. A simple,
|
||||
// honest local substitute for an external recommendation service.
|
||||
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
|
||||
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 = ?)
|
||||
AND s.id != ?
|
||||
ORDER BY RANDOM() LIMIT ?`, seedSongID, seedSongID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanSongs(rows)
|
||||
}
|
||||
|
||||
// SimilarSongsByArtist returns up to limit random songs by the given
|
||||
// artist (or sharing a genre with them if that artist has few songs),
|
||||
// excluding none in particular — used for getSimilarSongs2's artist
|
||||
// seed.
|
||||
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
|
||||
FROM songs s
|
||||
JOIN song_genres sg ON sg.song_id = s.id
|
||||
WHERE sg.genre_id IN (
|
||||
SELECT DISTINCT sg2.genre_id FROM song_artists sa2
|
||||
JOIN song_genres sg2 ON sg2.song_id = sa2.song_id
|
||||
WHERE sa2.artist_id = ?
|
||||
)
|
||||
ORDER BY RANDOM() LIMIT ?`, artistID, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
return scanSongs(rows)
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
package database
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func setupGenreFixture(t *testing.T) *sql.DB {
|
||||
t.Helper()
|
||||
db, err := Open(filepath.Join(t.TempDir(), "test.db"))
|
||||
if err != nil {
|
||||
t.Fatalf("Open: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { db.Close() })
|
||||
|
||||
folder, err := CreateMusicFolder(db, "Music", "/music", "music")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
artist, err := FindOrCreateArtist(db, "Test Artist", "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
album, err := FindOrCreateAlbum(db, "Test Album", nullStr2(artist.ID), "", nullInt2(2020), nullInt2(0), "", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := LinkAlbumArtist(db, album.ID, artist.ID, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
genre, err := FindOrCreateGenre(db, "Rock")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := LinkAlbumGenre(db, album.ID, genre.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
for i := 1; i <= 3; i++ {
|
||||
mf, err := CreateMediaFile(db, folder.ID, fmt.Sprintf("song%d.mp3", i), 1000, 0, fmt.Sprintf("hash%d", i), "mp3", "song", 180, 320, 44100, 2)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
song, err := UpsertSong(db, UpsertSongInput{MediaFileID: mf.ID, AlbumID: nullStr2(album.ID), Title: "Song"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := LinkSongGenre(db, song.ID, genre.ID); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func nullStr2(s string) sql.NullString { return sql.NullString{String: s, Valid: s != ""} }
|
||||
func nullInt2(n int64) sql.NullInt64 {
|
||||
if n == 0 {
|
||||
return sql.NullInt64{}
|
||||
}
|
||||
return sql.NullInt64{Int64: n, Valid: true}
|
||||
}
|
||||
|
||||
func TestAlbumStats(t *testing.T) {
|
||||
db := setupGenreFixture(t)
|
||||
albums, err := ListAlbums(db, 10, 0)
|
||||
if err != nil || len(albums) != 1 {
|
||||
t.Fatalf("ListAlbums: %v, %v", albums, err)
|
||||
}
|
||||
count, dur, err := AlbumStats(db, albums[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("AlbumStats: %v", err)
|
||||
}
|
||||
if count != 3 {
|
||||
t.Errorf("songCount = %d, want 3", count)
|
||||
}
|
||||
if dur != 540 {
|
||||
t.Errorf("duration = %v, want 540", dur)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListGenreStats(t *testing.T) {
|
||||
db := setupGenreFixture(t)
|
||||
stats, err := ListGenreStats(db)
|
||||
if err != nil {
|
||||
t.Fatalf("ListGenreStats: %v", err)
|
||||
}
|
||||
if len(stats) != 1 {
|
||||
t.Fatalf("stats = %v, want 1 genre", stats)
|
||||
}
|
||||
if stats[0].Name != "Rock" || stats[0].SongCount != 3 || stats[0].AlbumCount != 1 {
|
||||
t.Errorf("stats[0] = %+v, want Rock/3/1", stats[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestSongsByGenre(t *testing.T) {
|
||||
db := setupGenreFixture(t)
|
||||
songs, err := SongsByGenre(db, "Rock", 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("SongsByGenre: %v", err)
|
||||
}
|
||||
if len(songs) != 3 {
|
||||
t.Fatalf("songs = %v, want 3", songs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRandomSongIDsFilteredByGenre(t *testing.T) {
|
||||
db := setupGenreFixture(t)
|
||||
ids, err := RandomSongIDs(db, 10, "Rock", 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("RandomSongIDs: %v", err)
|
||||
}
|
||||
if len(ids) != 3 {
|
||||
t.Fatalf("ids = %v, want 3", ids)
|
||||
}
|
||||
ids2, err := RandomSongIDs(db, 10, "Jazz", 0, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("RandomSongIDs(Jazz): %v", err)
|
||||
}
|
||||
if len(ids2) != 0 {
|
||||
t.Fatalf("ids2 = %v, want 0 for nonexistent genre", ids2)
|
||||
}
|
||||
}
|
||||
|
||||
func TestArtistAlbumCountAndCoverArt(t *testing.T) {
|
||||
db := setupGenreFixture(t)
|
||||
artists, err := ListArtists(db)
|
||||
if err != nil || len(artists) != 1 {
|
||||
t.Fatalf("ListArtists: %v, %v", artists, err)
|
||||
}
|
||||
n, err := ArtistAlbumCount(db, artists[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ArtistAlbumCount: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Errorf("ArtistAlbumCount = %d, want 1", n)
|
||||
}
|
||||
cover, err := ArtistCoverArtID(db, artists[0].ID)
|
||||
if err != nil {
|
||||
t.Fatalf("ArtistCoverArtID: %v", err)
|
||||
}
|
||||
if cover != "" {
|
||||
t.Errorf("ArtistCoverArtID = %q, want empty (no cover art set in fixture)", cover)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAlbumsByYearRange(t *testing.T) {
|
||||
db := setupGenreFixture(t)
|
||||
albums, err := AlbumsByYearRange(db, 2019, 2021, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AlbumsByYearRange: %v", err)
|
||||
}
|
||||
if len(albums) != 1 {
|
||||
t.Fatalf("albums = %v, want 1", albums)
|
||||
}
|
||||
none, err := AlbumsByYearRange(db, 1990, 1991, 10, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("AlbumsByYearRange: %v", err)
|
||||
}
|
||||
if len(none) != 0 {
|
||||
t.Fatalf("albums = %v, want 0 outside range", none)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user