Files
yellowjacket/backend/library/query.go
T

283 lines
6.2 KiB
Go

package library
import (
"errors"
"fmt"
"path/filepath"
"strconv"
"strings"
)
// Sentinel errors for library queries.
var (
errNoTracksInLibrary = errors.New("no tracks in library")
errNoTracksForAlbum = errors.New("no tracks found for album")
)
// Track represents a playable audio file in the library.
type Track struct {
TrackName string
ArtistName string
TrackLength string
FilePath string
TrackNumber int64
DiscNumber int64
Album string
Genre []string
Year int64
Composer string
FileType string
SampleRate int64
BitDepth int64
Channels int64
Bitrate int64
FileSize int64
}
// genreDelimiter is the separator used by GROUP_CONCAT in the
// GetAllTracksWithFullMetadata query.
const genreDelimiter = "||"
// splitGenres splits a GROUP_CONCAT genre string into individual
// genre names. An empty string returns nil.
func splitGenres(concatenated string) []string {
if concatenated == "" {
return nil
}
return strings.Split(concatenated, genreDelimiter)
}
// Artist represents an artist in the library.
type Artist struct {
ID int64
Name string
}
// Album represents an album for the cover grid display.
type Album struct {
ID int64
Name string
ArtistName string
CoverArtPath string
CoverArtSmall string
CoverArtMedium string
CoverArtLarge string
Year int64
}
// GetAllTracks returns an array of track structs of every file in the library.
func (l *Library) GetAllTracks() ([]Track, error) {
rows, err := l.db.Queries.GetAllTracksWithFullMetadata(
l.ctx,
)
if err != nil {
l.logger.Error(
"could not retrieve audio files",
"error", err,
)
return nil, err
}
l.logger.Info("audio file list", "count", len(rows))
if len(rows) == 0 {
l.logger.Error("no tracks in library")
return nil, errNoTracksInLibrary
}
tracks := make([]Track, 0, len(rows))
for _, row := range rows {
track := Track{
TrackName: row.Title,
ArtistName: row.ArtistName,
TrackLength: strconv.FormatInt(
row.LengthMilliseconds, 10,
),
FilePath: row.FilePath,
TrackNumber: row.TrackNumber.Int64,
DiscNumber: row.DiscNumber.Int64,
Album: row.Album,
Genre: splitGenres(row.Genre),
Year: row.Year,
Composer: row.Composer,
FileType: row.FileType,
SampleRate: row.SampleRate,
BitDepth: row.BitDepth,
Channels: row.Channels,
Bitrate: row.Bitrate,
FileSize: row.FileSize,
}
tracks = append(tracks, track)
}
l.logger.Info("formatted tracks", "count", len(tracks))
return tracks, nil
}
// GetAlbumTracks returns all tracks for a given album (release group), ordered by disc and track number.
func (l *Library) GetAlbumTracks(albumID int64) ([]Track, error) {
rows, err := l.db.Queries.GetAudioFilesByReleaseGroup(l.ctx, albumID)
if err != nil {
l.logger.Error("could not retrieve album tracks", "albumID", albumID, "error", err)
return nil, fmt.Errorf("could not get album tracks: %w", err)
}
if len(rows) == 0 {
return nil, fmt.Errorf("%w %d", errNoTracksForAlbum, albumID)
}
tracks := make([]Track, 0, len(rows))
for _, row := range rows {
tracks = append(tracks, Track{
TrackName: row.Title,
ArtistName: row.ArtistName,
TrackLength: strconv.FormatInt(
row.LengthMilliseconds,
10,
),
FilePath: row.FilePath,
TrackNumber: row.TrackNumber.Int64,
DiscNumber: row.DiscNumber.Int64,
})
}
return tracks, nil
}
// GetAllAlbums returns all albums with cover art and artist info for the cover grid.
func (l *Library) GetAllAlbums() ([]Album, error) {
rows, err := l.db.Queries.GetAllAlbumsWithDetails(l.ctx)
if err != nil {
l.logger.Error("could not retrieve albums", "error", err)
return nil, fmt.Errorf("could not get albums: %w", err)
}
l.logger.Info("album list", "count", len(rows))
albums := make([]Album, 0, len(rows))
for _, row := range rows {
album := Album{
ID: row.ID,
Name: row.Name,
ArtistName: row.ArtistName,
}
if row.Year.Valid {
album.Year = row.Year.Int64
}
// Convert filesystem path to URL path for the asset handler.
if row.CoverArtPath != "" {
base := filepath.Base(row.CoverArtPath)
album.CoverArtPath = "/covers/" + base
album.CoverArtSmall = "/covers/" +
SizedFilename(base, "_sm")
album.CoverArtMedium = "/covers/" +
SizedFilename(base, "_md")
album.CoverArtLarge = "/covers/" +
SizedFilename(base, "_lg")
}
albums = append(albums, album)
}
return albums, nil
}
// GetAllArtists returns artists that are credited as album artists, ordered by name.
func (l *Library) GetAllArtists() ([]Artist, error) {
rows, err := l.db.Queries.GetAlbumArtists(l.ctx)
if err != nil {
l.logger.Error(
"could not retrieve artists",
"error", err,
)
return nil, fmt.Errorf(
"could not get artists: %w",
err,
)
}
l.logger.Info("artist list", "count", len(rows))
artists := make([]Artist, 0, len(rows))
for _, row := range rows {
artists = append(artists, Artist{
ID: row.ID,
Name: row.Name,
})
}
return artists, nil
}
// GetAlbumsByArtist returns all albums where the given artist is the album artist.
func (l *Library) GetAlbumsByArtist(
artistID int64,
) ([]Album, error) {
rows, err := l.db.Queries.GetAlbumsByArtist(
l.ctx,
artistID,
)
if err != nil {
l.logger.Error(
"could not retrieve albums for artist",
"artistID", artistID,
"error", err,
)
return nil, fmt.Errorf(
"could not get albums for artist: %w",
err,
)
}
l.logger.Info(
"albums for artist",
"artistID", artistID,
"count", len(rows),
)
albums := make([]Album, 0, len(rows))
for _, row := range rows {
album := Album{
ID: row.ID,
Name: row.Name,
ArtistName: row.ArtistName,
}
if row.Year.Valid {
album.Year = row.Year.Int64
}
// Convert filesystem path to URL path for the asset handler.
if row.CoverArtPath != "" {
base := filepath.Base(row.CoverArtPath)
album.CoverArtPath = "/covers/" + base
album.CoverArtSmall = "/covers/" +
SizedFilename(base, "_sm")
album.CoverArtMedium = "/covers/" +
SizedFilename(base, "_md")
album.CoverArtLarge = "/covers/" +
SizedFilename(base, "_lg")
}
albums = append(albums, album)
}
return albums, nil
}