Bootstrap SQLite on server startup with embedded migrations and development seed data. Replace placeholder auth and library responses with database-backed services, bearer sessions, and repository-driven API handlers.
163 lines
3.8 KiB
Go
163 lines
3.8 KiB
Go
package library
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"fmt"
|
|
)
|
|
|
|
type Artist struct {
|
|
ID string `json:"id"`
|
|
Name string `json:"name"`
|
|
AlbumCount int `json:"albumCount"`
|
|
}
|
|
|
|
type Album struct {
|
|
ID string `json:"id"`
|
|
ArtistID string `json:"artistId"`
|
|
ArtistName string `json:"artistName"`
|
|
Title string `json:"title"`
|
|
Year int `json:"year"`
|
|
TrackCount int `json:"trackCount"`
|
|
}
|
|
|
|
type Track struct {
|
|
ID string `json:"id"`
|
|
AlbumID string `json:"albumId"`
|
|
ArtistID string `json:"artistId"`
|
|
Title string `json:"title"`
|
|
ArtistName string `json:"artistName"`
|
|
AlbumTitle string `json:"albumTitle"`
|
|
TrackNumber int `json:"trackNumber"`
|
|
DurationSecs int `json:"durationSeconds"`
|
|
FilePath string `json:"filePath"`
|
|
ContentType string `json:"contentType"`
|
|
}
|
|
|
|
type HomePayload struct {
|
|
RecentAlbums []Album `json:"recentAlbums"`
|
|
Artists []Artist `json:"artists"`
|
|
}
|
|
|
|
type Service struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
func NewService(db *sql.DB) *Service {
|
|
return &Service{db: db}
|
|
}
|
|
|
|
func (s *Service) Home(ctx context.Context) (HomePayload, error) {
|
|
albums, err := s.RecentAlbums(ctx, 6)
|
|
if err != nil {
|
|
return HomePayload{}, err
|
|
}
|
|
|
|
artists, err := s.Artists(ctx, 12)
|
|
if err != nil {
|
|
return HomePayload{}, err
|
|
}
|
|
|
|
return HomePayload{
|
|
RecentAlbums: albums,
|
|
Artists: artists,
|
|
}, nil
|
|
}
|
|
|
|
func (s *Service) Artists(ctx context.Context, limit int) ([]Artist, error) {
|
|
rows, err := s.db.QueryContext(
|
|
ctx,
|
|
`SELECT a.id, a.name, COUNT(al.id) AS album_count
|
|
FROM artists a
|
|
LEFT JOIN albums al ON al.artist_id = a.id
|
|
GROUP BY a.id, a.name
|
|
ORDER BY a.name ASC
|
|
LIMIT ?`,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query artists: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var artists []Artist
|
|
for rows.Next() {
|
|
var artist Artist
|
|
if err := rows.Scan(&artist.ID, &artist.Name, &artist.AlbumCount); err != nil {
|
|
return nil, fmt.Errorf("scan artist: %w", err)
|
|
}
|
|
artists = append(artists, artist)
|
|
}
|
|
|
|
return artists, rows.Err()
|
|
}
|
|
|
|
func (s *Service) RecentAlbums(ctx context.Context, limit int) ([]Album, error) {
|
|
rows, err := s.db.QueryContext(
|
|
ctx,
|
|
`SELECT al.id, al.artist_id, a.name, al.title, COALESCE(al.year, 0), COUNT(t.id) AS track_count
|
|
FROM albums al
|
|
JOIN artists a ON a.id = al.artist_id
|
|
LEFT JOIN tracks t ON t.album_id = al.id
|
|
GROUP BY al.id, al.artist_id, a.name, al.title, al.year
|
|
ORDER BY al.updated_at DESC, al.title ASC
|
|
LIMIT ?`,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query albums: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var albums []Album
|
|
for rows.Next() {
|
|
var album Album
|
|
if err := rows.Scan(&album.ID, &album.ArtistID, &album.ArtistName, &album.Title, &album.Year, &album.TrackCount); err != nil {
|
|
return nil, fmt.Errorf("scan album: %w", err)
|
|
}
|
|
albums = append(albums, album)
|
|
}
|
|
|
|
return albums, rows.Err()
|
|
}
|
|
|
|
func (s *Service) Tracks(ctx context.Context, limit int) ([]Track, error) {
|
|
rows, err := s.db.QueryContext(
|
|
ctx,
|
|
`SELECT t.id, t.album_id, t.artist_id, t.title, a.name, al.title, COALESCE(t.track_number, 0),
|
|
COALESCE(t.duration_seconds, 0), t.file_path, COALESCE(t.content_type, '')
|
|
FROM tracks t
|
|
JOIN artists a ON a.id = t.artist_id
|
|
JOIN albums al ON al.id = t.album_id
|
|
ORDER BY a.name ASC, al.year DESC, al.title ASC, t.disc_number ASC, t.track_number ASC
|
|
LIMIT ?`,
|
|
limit,
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("query tracks: %w", err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var tracks []Track
|
|
for rows.Next() {
|
|
var track Track
|
|
if err := rows.Scan(
|
|
&track.ID,
|
|
&track.AlbumID,
|
|
&track.ArtistID,
|
|
&track.Title,
|
|
&track.ArtistName,
|
|
&track.AlbumTitle,
|
|
&track.TrackNumber,
|
|
&track.DurationSecs,
|
|
&track.FilePath,
|
|
&track.ContentType,
|
|
); err != nil {
|
|
return nil, fmt.Errorf("scan track: %w", err)
|
|
}
|
|
tracks = append(tracks, track)
|
|
}
|
|
|
|
return tracks, rows.Err()
|
|
}
|