The other soundtrack statistics are capped at 100, so a client that shows every soundtrack (the MusicPlayer statistics tab) had no way to get them all. This one has no limit and is sorted by name.
413 lines
12 KiB
Go
413 lines
12 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"
|
|
)
|
|
|
|
// 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
|
|
|
|
// 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) 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) 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
|
|
}
|
|
|
|
// 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
|
|
}
|