Files
TermorServer/internal/library/service.go
benya 46c2c3fb28 feat: import library from media root and stream tracks
Add a filesystem scanner that ingests supported audio files from MEDIA_ROOT into SQLite using embedded tags with filename fallbacks. Wire startup scanning, manual rescan, and authenticated audio streaming into the backend, then connect the web player to the real stream endpoint.
2026-04-02 22:29:04 +03:00

194 lines
4.6 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()
}
func (s *Service) TrackByID(ctx context.Context, id string) (Track, error) {
var track Track
err := s.db.QueryRowContext(
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
WHERE t.id = ?`,
id,
).Scan(
&track.ID,
&track.AlbumID,
&track.ArtistID,
&track.Title,
&track.ArtistName,
&track.AlbumTitle,
&track.TrackNumber,
&track.DurationSecs,
&track.FilePath,
&track.ContentType,
)
if err != nil {
return Track{}, fmt.Errorf("query track by id: %w", err)
}
return track, nil
}