283 lines
6.2 KiB
Go
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
|
|
}
|