Upload new soundtracks through the API
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>
This commit is contained in:
1 parent
439ffede18
commit
a7e2280969
16 files changed
+1135
-5
No files matched your search
@@ -35,8 +35,15 @@ const (
|
||||
brokenWorkers = 50
|
||||
)
|
||||
|
||||
// foldersToSkip are directories in the music path that are never synced.
|
||||
var foldersToSkip = map[string]struct{}{".sync": {}, "characters": {}, "dist": {}, "old": {}}
|
||||
// foldersToSkip are directories in the music path that are never synced, besides hidden
|
||||
// ones like .sync and .uploads.
|
||||
var foldersToSkip = map[string]struct{}{"characters": {}, "dist": {}, "old": {}}
|
||||
|
||||
// skipFolder reports whether a directory in the music path is not a soundtrack.
|
||||
func skipFolder(name string) bool {
|
||||
_, skip := foldersToSkip[name]
|
||||
return skip || strings.HasPrefix(name, ".")
|
||||
}
|
||||
|
||||
type SyncResponse struct {
|
||||
SoundtracksAdded []string `json:"soundtracks_added"`
|
||||
@@ -117,6 +124,37 @@ func (s *SyncService) Start(full bool) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// SyncFolder runs prepare and then syncs the soundtrack folder name, while no other sync
|
||||
// can run. It returns ErrSyncing when a sync is running. prepare can put the folder in
|
||||
// place; when it fails, nothing is synced. onComplete is not called, as only one
|
||||
// soundtrack changes.
|
||||
func (s *SyncService) SyncFolder(ctx context.Context, name string, prepare func() error) (repository.Soundtrack, error) {
|
||||
if !s.running.CompareAndSwap(false, true) {
|
||||
return repository.Soundtrack{}, ErrSyncing
|
||||
}
|
||||
defer s.running.Store(false)
|
||||
|
||||
existing, err := s.q.GetAllSoundtracksIncludingDeleted(ctx)
|
||||
if err != nil {
|
||||
return repository.Soundtrack{}, err
|
||||
}
|
||||
if err := prepare(); err != nil {
|
||||
return repository.Soundtrack{}, err
|
||||
}
|
||||
run := &syncRun{q: s.q, musicPath: s.musicPath, existing: existing, progress: func() {}}
|
||||
run.syncSoundtrack(ctx, name)
|
||||
if len(run.errs) > 0 {
|
||||
return repository.Soundtrack{}, fmt.Errorf("syncing %q: %s", name, strings.Join(run.errs, "; "))
|
||||
}
|
||||
if len(run.foundIDs) != 1 {
|
||||
return repository.Soundtrack{}, fmt.Errorf("syncing %q found %d soundtracks", name, len(run.foundIDs))
|
||||
}
|
||||
if err := s.UpdateLibraryMetrics(ctx); err != nil {
|
||||
logging.GetLogger().Error("Failed to count the library", zap.Error(err))
|
||||
}
|
||||
return s.q.GetSoundtrackById(ctx, run.foundIDs[0])
|
||||
}
|
||||
|
||||
// Wait blocks until the running sync, if any, has finished.
|
||||
func (s *SyncService) Wait() {
|
||||
s.wg.Wait()
|
||||
@@ -276,7 +314,7 @@ func (r *syncRun) execute(ctx context.Context, setTotal func(int)) SyncResponse
|
||||
for _, dir := range directories {
|
||||
g.Go(func() error {
|
||||
defer r.progress()
|
||||
if _, skip := foldersToSkip[dir.Name()]; dir.IsDir() && !skip {
|
||||
if dir.IsDir() && !skipFolder(dir.Name()) {
|
||||
r.syncSoundtrack(ctx, dir.Name())
|
||||
}
|
||||
return nil
|
||||
|
||||
Reference in new issue
Block a user