A client starts an upload with the soundtrack's name, sends one MP3 per
request, and finishes it. The server then moves the songs into a new folder
in the music folder and syncs only that folder, so the soundtrack is in the
library, lists and search right away.
- POST /api/v1/soundtracks/uploads, GET/DELETE .../uploads/{id},
PUT .../uploads/{id}/files/{file} and POST .../uploads/{id}/finish.
- Names must work as file names on every system and be free, ignoring case.
File names are plain: clients flatten disc folders.
- Only MP3 files. Size limits from UPLOAD_MAX_FILE_MB (50) and
UPLOAD_MAX_TOTAL_MB (2048).
- Refused while a sync runs, and a sync cannot start while an upload is
finished. Songs equal to an existing soundtrack's are refused, as the sync
would take them for that soundtrack renamed.
- Unfinished uploads live in MUSIC_PATH/.uploads and are removed after a day.
The sync now skips every hidden folder.
Closes #33
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
659 lines
19 KiB
Go
659 lines
19 KiB
Go
// Package fakedb provides an in-memory repository.Querier for tests.
|
|
package fakedb
|
|
|
|
import (
|
|
"cmp"
|
|
"context"
|
|
"math/rand/v2"
|
|
"slices"
|
|
"sync"
|
|
"time"
|
|
|
|
"music-server/internal/db/repository"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
)
|
|
|
|
// Store is an in-memory implementation of the queries used by the application.
|
|
// Calling a query it does not implement panics through the nil embedded interface.
|
|
type Store struct {
|
|
repository.Querier
|
|
|
|
mu sync.Mutex
|
|
nextID int32
|
|
Soundtracks []repository.Soundtrack
|
|
Songs []repository.Song
|
|
Sessions map[string]repository.Session
|
|
Queues []repository.Queue
|
|
QueueSongs []repository.QueueSong
|
|
|
|
// Err, when set, is returned by every implemented query.
|
|
Err error
|
|
}
|
|
|
|
// New returns an empty Store.
|
|
func New() *Store {
|
|
return &Store{Sessions: map[string]repository.Session{}}
|
|
}
|
|
|
|
func (s *Store) lock() func() {
|
|
s.mu.Lock()
|
|
return s.mu.Unlock
|
|
}
|
|
|
|
// AddSoundtrack inserts a soundtrack and returns it with its ID and UUID set.
|
|
func (s *Store) AddSoundtrack(st repository.Soundtrack) repository.Soundtrack {
|
|
defer s.lock()()
|
|
s.nextID++
|
|
st.ID = s.nextID
|
|
if st.Uuid == uuid.Nil {
|
|
st.Uuid = uuid.New()
|
|
}
|
|
s.Soundtracks = append(s.Soundtracks, st)
|
|
return st
|
|
}
|
|
|
|
// AddTestSong inserts a song and returns it with its UUID and soundtrack UUID set.
|
|
func (s *Store) AddTestSong(song repository.Song) repository.Song {
|
|
defer s.lock()()
|
|
if song.Uuid == uuid.Nil {
|
|
song.Uuid = uuid.New()
|
|
}
|
|
if i := s.soundtrackIndex(song.SoundtrackID); song.SoundtrackUuid == uuid.Nil && i >= 0 {
|
|
song.SoundtrackUuid = s.Soundtracks[i].Uuid
|
|
}
|
|
s.Songs = append(s.Songs, song)
|
|
return song
|
|
}
|
|
|
|
// Soundtrack returns the soundtrack with the given name.
|
|
func (s *Store) Soundtrack(name string) (repository.Soundtrack, bool) {
|
|
defer s.lock()()
|
|
for _, st := range s.Soundtracks {
|
|
if st.SoundtrackName == name {
|
|
return st, true
|
|
}
|
|
}
|
|
return repository.Soundtrack{}, false
|
|
}
|
|
|
|
// SongsOf returns the songs of a soundtrack sorted by path.
|
|
func (s *Store) SongsOf(soundtrackID int32) []repository.Song {
|
|
defer s.lock()()
|
|
var songs []repository.Song
|
|
for _, song := range s.Songs {
|
|
if song.SoundtrackID == soundtrackID {
|
|
songs = append(songs, song)
|
|
}
|
|
}
|
|
slices.SortFunc(songs, func(a, b repository.Song) int { return cmp.Compare(a.Path, b.Path) })
|
|
return songs
|
|
}
|
|
|
|
func (s *Store) soundtrackIndex(id int32) int {
|
|
return slices.IndexFunc(s.Soundtracks, func(st repository.Soundtrack) bool { return st.ID == id })
|
|
}
|
|
|
|
func (s *Store) updateSoundtrack(id int32, f func(*repository.Soundtrack)) error {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return s.Err
|
|
}
|
|
if i := s.soundtrackIndex(id); i >= 0 {
|
|
f(&s.Soundtracks[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func sortedByName(soundtracks []repository.Soundtrack) []repository.Soundtrack {
|
|
slices.SortFunc(soundtracks, func(a, b repository.Soundtrack) int { return cmp.Compare(a.SoundtrackName, b.SoundtrackName) })
|
|
return soundtracks
|
|
}
|
|
|
|
func (s *Store) FindAllSoundtracks(context.Context) ([]repository.Soundtrack, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
var active []repository.Soundtrack
|
|
for _, st := range s.Soundtracks {
|
|
if st.Deleted == nil {
|
|
active = append(active, st)
|
|
}
|
|
}
|
|
return sortedByName(active), nil
|
|
}
|
|
|
|
func (s *Store) GetAllSoundtracksIncludingDeleted(context.Context) ([]repository.Soundtrack, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
return sortedByName(slices.Clone(s.Soundtracks)), nil
|
|
}
|
|
|
|
func (s *Store) InsertSoundtrack(_ context.Context, arg repository.InsertSoundtrackParams) (repository.InsertSoundtrackRow, error) {
|
|
if s.Err != nil {
|
|
return repository.InsertSoundtrackRow{}, s.Err
|
|
}
|
|
st := s.AddSoundtrack(repository.Soundtrack{
|
|
SoundtrackName: arg.SoundtrackName,
|
|
Path: arg.Path,
|
|
Hash: arg.Hash,
|
|
Uuid: arg.Uuid,
|
|
Added: time.Now(),
|
|
})
|
|
return repository.InsertSoundtrackRow{ID: st.ID, Uuid: st.Uuid}, nil
|
|
}
|
|
|
|
func (s *Store) ResetSoundtrackIdSeq(context.Context) (int64, error) {
|
|
return int64(s.nextID), s.Err
|
|
}
|
|
|
|
func (s *Store) UpdateSoundtrackHash(_ context.Context, arg repository.UpdateSoundtrackHashParams) error {
|
|
return s.updateSoundtrack(arg.ID, func(st *repository.Soundtrack) { st.Hash = arg.Hash })
|
|
}
|
|
|
|
func (s *Store) UpdateSoundtrackName(_ context.Context, arg repository.UpdateSoundtrackNameParams) error {
|
|
return s.updateSoundtrack(arg.ID, func(st *repository.Soundtrack) { st.SoundtrackName, st.Path = arg.Name, arg.Path })
|
|
}
|
|
|
|
func (s *Store) SetSoundtrackFingerprint(_ context.Context, arg repository.SetSoundtrackFingerprintParams) error {
|
|
return s.updateSoundtrack(arg.ID, func(st *repository.Soundtrack) { st.Fingerprint = arg.Fingerprint })
|
|
}
|
|
|
|
func (s *Store) RemoveSoundtrackDeletionDate(_ context.Context, id int32) error {
|
|
return s.updateSoundtrack(id, func(st *repository.Soundtrack) { st.Deleted = nil })
|
|
}
|
|
|
|
func (s *Store) AddSoundtrackPlayedByUUID(_ context.Context, id uuid.UUID) error {
|
|
defer s.lock()()
|
|
now := time.Now()
|
|
for i := range s.Soundtracks {
|
|
if s.Soundtracks[i].Uuid == id {
|
|
s.Soundtracks[i].TimesPlayed++
|
|
s.Soundtracks[i].LastPlayed = &now
|
|
}
|
|
}
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) GetSoundtrackById(_ context.Context, id int32) (repository.Soundtrack, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Soundtrack{}, s.Err
|
|
}
|
|
if i := s.soundtrackIndex(id); i >= 0 && s.Soundtracks[i].Deleted == nil {
|
|
return s.Soundtracks[i], nil
|
|
}
|
|
return repository.Soundtrack{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (s *Store) GetSoundtrackByUUID(_ context.Context, id uuid.UUID) (repository.Soundtrack, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Soundtrack{}, s.Err
|
|
}
|
|
for _, st := range s.Soundtracks {
|
|
if st.Uuid == id && st.Deleted == nil {
|
|
return st, nil
|
|
}
|
|
}
|
|
return repository.Soundtrack{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (s *Store) soundtrackByUUID(id uuid.UUID) int {
|
|
return slices.IndexFunc(s.Soundtracks, func(st repository.Soundtrack) bool { return st.Uuid == id })
|
|
}
|
|
|
|
func (s *Store) updateSoundtrackByUUID(id uuid.UUID, f func(*repository.Soundtrack)) error {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return s.Err
|
|
}
|
|
if i := s.soundtrackByUUID(id); i >= 0 {
|
|
f(&s.Soundtracks[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) SetAlternativeNames(_ context.Context, arg repository.SetAlternativeNamesParams) (repository.Soundtrack, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Soundtrack{}, s.Err
|
|
}
|
|
i := s.soundtrackByUUID(arg.Uuid)
|
|
if i < 0 || s.Soundtracks[i].Deleted != nil {
|
|
return repository.Soundtrack{}, pgx.ErrNoRows
|
|
}
|
|
st := &s.Soundtracks[i]
|
|
st.AlternativeNames = slices.Clone(arg.Names)
|
|
if st.CoverFile == nil {
|
|
st.CoverChecked = pgtype.Timestamptz{}
|
|
}
|
|
return *st, nil
|
|
}
|
|
|
|
func (s *Store) ListSoundtracksNeedingCover(
|
|
_ context.Context, checkedBefore pgtype.Timestamptz,
|
|
) ([]repository.ListSoundtracksNeedingCoverRow, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
var rows []repository.ListSoundtracksNeedingCoverRow
|
|
for _, st := range sortedByName(slices.Clone(s.Soundtracks)) {
|
|
if st.Deleted != nil || st.CoverFile != nil || st.CoverManual ||
|
|
(st.CoverChecked.Valid && !st.CoverChecked.Time.Before(checkedBefore.Time)) {
|
|
continue
|
|
}
|
|
rows = append(rows, repository.ListSoundtracksNeedingCoverRow{
|
|
Uuid: st.Uuid, SoundtrackName: st.SoundtrackName, AlternativeNames: st.AlternativeNames,
|
|
})
|
|
}
|
|
return rows, nil
|
|
}
|
|
|
|
func (s *Store) SetCover(_ context.Context, arg repository.SetCoverParams) error {
|
|
return s.updateSoundtrackByUUID(arg.Uuid, func(st *repository.Soundtrack) {
|
|
now := pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
|
st.CoverFile, st.CoverSource, st.CoverExternalID = arg.File, arg.Source, arg.ExternalID
|
|
st.CoverManual, st.CoverModified, st.CoverChecked = arg.Manual, now, now
|
|
})
|
|
}
|
|
|
|
func (s *Store) MarkCoverChecked(_ context.Context, id uuid.UUID) error {
|
|
return s.updateSoundtrackByUUID(id, func(st *repository.Soundtrack) {
|
|
st.CoverChecked = pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
|
})
|
|
}
|
|
|
|
func (s *Store) ClearCover(_ context.Context, id uuid.UUID) error {
|
|
return s.updateSoundtrackByUUID(id, func(st *repository.Soundtrack) {
|
|
st.CoverFile, st.CoverSource, st.CoverExternalID, st.CoverManual = nil, nil, nil, false
|
|
st.CoverModified, st.CoverChecked = pgtype.Timestamptz{}, pgtype.Timestamptz{}
|
|
})
|
|
}
|
|
|
|
func (s *Store) ListCoverFiles(context.Context) ([]string, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
var files []string
|
|
for _, st := range s.Soundtracks {
|
|
if st.CoverFile != nil {
|
|
files = append(files, *st.CoverFile)
|
|
}
|
|
}
|
|
return files, nil
|
|
}
|
|
|
|
func (s *Store) MarkSoundtracksDeletedExcept(_ context.Context, ids []int32) error {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return s.Err
|
|
}
|
|
now := time.Now()
|
|
for i := range s.Soundtracks {
|
|
if s.Soundtracks[i].Deleted == nil && !slices.Contains(ids, s.Soundtracks[i].ID) {
|
|
s.Soundtracks[i].Deleted = &now
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ClearSoundtracks(context.Context) error {
|
|
defer s.lock()()
|
|
s.Soundtracks = nil
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) ClearSongs(context.Context) error {
|
|
defer s.lock()()
|
|
s.Songs = nil
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) FindSongsFromSoundtrack(_ context.Context, soundtrackID int32) ([]repository.Song, error) {
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
return s.SongsOf(soundtrackID), nil
|
|
}
|
|
|
|
func (s *Store) FetchAllSongs(context.Context) ([]repository.Song, error) {
|
|
defer s.lock()()
|
|
return slices.Clone(s.Songs), s.Err
|
|
}
|
|
|
|
func (s *Store) AddSong(_ context.Context, arg repository.AddSongParams) error {
|
|
if s.Err != nil {
|
|
return s.Err
|
|
}
|
|
s.AddTestSong(repository.Song{
|
|
SoundtrackID: arg.SoundtrackID,
|
|
SongName: arg.SongName,
|
|
Path: arg.Path,
|
|
FileName: arg.FileName,
|
|
Hash: arg.Hash,
|
|
Uuid: arg.Uuid,
|
|
SoundtrackUuid: arg.SoundtrackUuid,
|
|
})
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) UpdateSongByUUID(_ context.Context, arg repository.UpdateSongByUUIDParams) error {
|
|
defer s.lock()()
|
|
for i := range s.Songs {
|
|
if s.Songs[i].Uuid == arg.Uuid {
|
|
s.Songs[i].SongName, s.Songs[i].FileName, s.Songs[i].Path, s.Songs[i].Hash = arg.SongName, arg.FileName, arg.Path, arg.Hash
|
|
}
|
|
}
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) AddSongPlayedByUUID(_ context.Context, id uuid.UUID) error {
|
|
defer s.lock()()
|
|
for i := range s.Songs {
|
|
if s.Songs[i].Uuid == id {
|
|
s.Songs[i].TimesPlayed++
|
|
}
|
|
}
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) GetSongsBySoundtrackUUID(_ context.Context, id uuid.UUID) ([]repository.Song, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
var songs []repository.Song
|
|
for _, song := range s.Songs {
|
|
if song.SoundtrackUuid == id {
|
|
songs = append(songs, song)
|
|
}
|
|
}
|
|
slices.SortFunc(songs, func(a, b repository.Song) int { return cmp.Compare(a.SongName, b.SongName) })
|
|
return songs, nil
|
|
}
|
|
|
|
func (s *Store) GetActiveSongByUUID(_ context.Context, id uuid.UUID) (repository.GetActiveSongByUUIDRow, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.GetActiveSongByUUIDRow{}, s.Err
|
|
}
|
|
for _, song := range s.Songs {
|
|
if song.Uuid != id {
|
|
continue
|
|
}
|
|
if i := s.soundtrackIndex(song.SoundtrackID); i >= 0 && s.Soundtracks[i].Deleted == nil {
|
|
st := s.Soundtracks[i]
|
|
return repository.GetActiveSongByUUIDRow{Song: song, SoundtrackName: st.SoundtrackName, SoundtrackPath: st.Path}, nil
|
|
}
|
|
}
|
|
return repository.GetActiveSongByUUIDRow{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func (s *Store) RemoveBrokenSongs(_ context.Context, uuids []uuid.UUID) error {
|
|
defer s.lock()()
|
|
s.Songs = slices.DeleteFunc(s.Songs, func(song repository.Song) bool { return slices.Contains(uuids, song.Uuid) })
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) GetRandomActiveSong(context.Context) (repository.GetRandomActiveSongRow, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.GetRandomActiveSongRow{}, s.Err
|
|
}
|
|
var rows []repository.GetRandomActiveSongRow
|
|
for _, song := range s.Songs {
|
|
if i := s.soundtrackIndex(song.SoundtrackID); i >= 0 && s.Soundtracks[i].Deleted == nil {
|
|
rows = append(rows, repository.GetRandomActiveSongRow{Song: song, SoundtrackPath: s.Soundtracks[i].Path})
|
|
}
|
|
}
|
|
if len(rows) == 0 {
|
|
return repository.GetRandomActiveSongRow{}, pgx.ErrNoRows
|
|
}
|
|
return rows[rand.IntN(len(rows))], nil
|
|
}
|
|
|
|
func (s *Store) CreateSession(_ context.Context, arg repository.CreateSessionParams) (repository.Session, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Session{}, s.Err
|
|
}
|
|
session := repository.Session{
|
|
Token: arg.Token,
|
|
IpAddress: arg.IpAddress,
|
|
UserAgent: arg.UserAgent,
|
|
ClientType: arg.ClientType,
|
|
ExpiresAt: arg.ExpiresAt,
|
|
}
|
|
s.Sessions[arg.Token] = session
|
|
return session, nil
|
|
}
|
|
|
|
func (s *Store) GetSession(_ context.Context, token string) (repository.Session, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Session{}, s.Err
|
|
}
|
|
session, ok := s.Sessions[token]
|
|
if !ok {
|
|
return repository.Session{}, pgx.ErrNoRows
|
|
}
|
|
return session, nil
|
|
}
|
|
|
|
func (s *Store) DeleteSession(ctx context.Context, token string) error {
|
|
if err := ctx.Err(); err != nil {
|
|
return err
|
|
}
|
|
defer s.lock()()
|
|
delete(s.Sessions, token)
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) DeleteExpiredSessions(context.Context) error {
|
|
defer s.lock()()
|
|
for token, session := range s.Sessions {
|
|
if session.ExpiresAt.Time.Before(time.Now()) {
|
|
delete(s.Sessions, token)
|
|
}
|
|
}
|
|
return s.Err
|
|
}
|
|
|
|
// HasSession reports whether a session with the given token exists.
|
|
func (s *Store) HasSession(token string) bool {
|
|
defer s.lock()()
|
|
_, ok := s.Sessions[token]
|
|
return ok
|
|
}
|
|
|
|
func (s *Store) queueIndex(id uuid.UUID) int {
|
|
return slices.IndexFunc(s.Queues, func(q repository.Queue) bool { return q.Uuid == id })
|
|
}
|
|
|
|
func (s *Store) updateQueue(id uuid.UUID, f func(*repository.Queue)) error {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return s.Err
|
|
}
|
|
if i := s.queueIndex(id); i >= 0 {
|
|
f(&s.Queues[i])
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) CreateQueue(context.Context) (repository.Queue, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Queue{}, s.Err
|
|
}
|
|
queue := repository.Queue{Uuid: uuid.New(), CurrentSong: -1, StartedAt: pgtype.Timestamptz{Time: time.Now(), Valid: true}}
|
|
s.Queues = append(s.Queues, queue)
|
|
return queue, nil
|
|
}
|
|
|
|
func (s *Store) GetQueue(_ context.Context, id uuid.UUID) (repository.Queue, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.Queue{}, s.Err
|
|
}
|
|
if i := s.queueIndex(id); i >= 0 {
|
|
return s.Queues[i], nil
|
|
}
|
|
return repository.Queue{}, pgx.ErrNoRows
|
|
}
|
|
|
|
func endQueue(q *repository.Queue) {
|
|
if !q.EndedAt.Valid {
|
|
q.EndedAt = pgtype.Timestamptz{Time: time.Now(), Valid: true}
|
|
}
|
|
}
|
|
|
|
func (s *Store) EndQueue(_ context.Context, id uuid.UUID) error {
|
|
return s.updateQueue(id, endQueue)
|
|
}
|
|
|
|
func (s *Store) EndActiveQueues(context.Context) error {
|
|
defer s.lock()()
|
|
for i := range s.Queues {
|
|
endQueue(&s.Queues[i])
|
|
}
|
|
return s.Err
|
|
}
|
|
|
|
func (s *Store) SetQueueLastFetched(_ context.Context, arg repository.SetQueueLastFetchedParams) error {
|
|
return s.updateQueue(arg.Uuid, func(q *repository.Queue) {
|
|
q.LastFetchedSongUuid, q.LastFetchedMode = arg.LastFetchedSongUuid, arg.LastFetchedMode
|
|
})
|
|
}
|
|
|
|
func (s *Store) SetQueueCurrentSong(_ context.Context, arg repository.SetQueueCurrentSongParams) error {
|
|
return s.updateQueue(arg.Uuid, func(q *repository.Queue) { q.CurrentSong = arg.CurrentSong })
|
|
}
|
|
|
|
func (s *Store) AddLastFetchedToQueue(_ context.Context, id uuid.UUID) error {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return s.Err
|
|
}
|
|
i := s.queueIndex(id)
|
|
if i < 0 {
|
|
return nil
|
|
}
|
|
queue := &s.Queues[i]
|
|
songIndex := slices.IndexFunc(s.Songs, func(song repository.Song) bool { return song.Uuid == queue.LastFetchedSongUuid })
|
|
if songIndex < 0 {
|
|
return nil
|
|
}
|
|
song := s.Songs[songIndex]
|
|
var soundtrackName string
|
|
if st := s.soundtrackIndex(song.SoundtrackID); st >= 0 {
|
|
soundtrackName = s.Soundtracks[st].SoundtrackName
|
|
}
|
|
mode := "unknown"
|
|
if queue.LastFetchedMode != nil {
|
|
mode = *queue.LastFetchedMode
|
|
}
|
|
songNo := int32(0)
|
|
for _, qs := range s.QueueSongs {
|
|
if qs.QueueUuid == id {
|
|
songNo = max(songNo, qs.SongNo+1)
|
|
}
|
|
}
|
|
s.QueueSongs = append(s.QueueSongs, repository.QueueSong{
|
|
QueueUuid: id, SongNo: songNo, SongUuid: song.Uuid, SoundtrackName: soundtrackName, SongName: song.SongName,
|
|
Mode: mode,
|
|
})
|
|
queue.CurrentSong = songNo
|
|
queue.LastFetchedSongUuid, queue.LastFetchedMode = uuid.Nil, nil
|
|
return nil
|
|
}
|
|
|
|
func (s *Store) ListQueueSongs(_ context.Context, id uuid.UUID) ([]repository.ListQueueSongsRow, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return nil, s.Err
|
|
}
|
|
var rows []repository.ListQueueSongsRow
|
|
for _, qs := range s.QueueSongs {
|
|
if qs.QueueUuid != id {
|
|
continue
|
|
}
|
|
row := repository.ListQueueSongsRow{
|
|
SongNo: qs.SongNo, SongUuid: qs.SongUuid, SoundtrackName: qs.SoundtrackName, SongName: qs.SongName, Mode: qs.Mode,
|
|
}
|
|
if i := slices.IndexFunc(s.Songs, func(song repository.Song) bool { return song.Uuid == qs.SongUuid }); i >= 0 {
|
|
song := s.Songs[i]
|
|
row.Path, row.TimesPlayed, row.SoundtrackUuid = &song.Path, &song.TimesPlayed, song.SoundtrackUuid
|
|
}
|
|
rows = append(rows, row)
|
|
}
|
|
slices.SortFunc(rows, func(a, b repository.ListQueueSongsRow) int { return cmp.Compare(a.SongNo, b.SongNo) })
|
|
return rows, nil
|
|
}
|
|
|
|
func (s *Store) CountLibrary(context.Context) (repository.CountLibraryRow, error) {
|
|
defer s.lock()()
|
|
if s.Err != nil {
|
|
return repository.CountLibraryRow{}, s.Err
|
|
}
|
|
var count repository.CountLibraryRow
|
|
for _, st := range s.Soundtracks {
|
|
if st.Deleted != nil {
|
|
continue
|
|
}
|
|
count.Soundtracks++
|
|
for _, song := range s.Songs {
|
|
if song.SoundtrackID == st.ID {
|
|
count.Songs++
|
|
}
|
|
}
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
// Statistics queries return empty results; tests that need data use their own fakes.
|
|
|
|
func (s *Store) GetMostPlayedSoundtracksWithSongs(context.Context, int32) ([]repository.GetMostPlayedSoundtracksWithSongsRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetLeastPlayedSoundtracksWithSongs(context.Context, int32) ([]repository.GetLeastPlayedSoundtracksWithSongsRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetLastPlayedSoundtracks(context.Context, int32) ([]repository.GetLastPlayedSoundtracksRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetOldestPlayedSoundtracks(context.Context, int32) ([]repository.GetOldestPlayedSoundtracksRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetAllSoundtracksWithSongs(context.Context) ([]repository.GetAllSoundtracksWithSongsRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetNeverPlayedSoundtracks(context.Context) ([]repository.GetNeverPlayedSoundtracksRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetMostPlayedSongsWithSoundtrack(context.Context, int32) ([]repository.GetMostPlayedSongsWithSoundtrackRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetLeastPlayedSongsWithSoundtrack(context.Context, int32) ([]repository.GetLeastPlayedSongsWithSoundtrackRow, error) {
|
|
return nil, s.Err
|
|
}
|
|
|
|
func (s *Store) GetStatisticsSummary(context.Context) (repository.GetStatisticsSummaryRow, error) {
|
|
return repository.GetStatisticsSummaryRow{}, s.Err
|
|
}
|