diff --git a/backend/app.go b/backend/app.go index b63bf38..af3af30 100644 --- a/backend/app.go +++ b/backend/app.go @@ -44,6 +44,7 @@ type App struct { Config *Config ServerManager *ServerManager ImageManager *ImageManager + AudioCache *AudioCache PlaybackManager *PlaybackManager LocalPlayer *mpv.Player UpdateChecker UpdateChecker @@ -142,8 +143,13 @@ func StartupApp(appName, displayAppName, appVersion, appVersionTag, latestReleas } a.ServerManager = NewServerManager(appName, appVersion, a.Config, !portableMode && a.Config.Application.EnablePasswordStorage) - a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.LocalPlayer, &a.Config.Playback, &a.Config.Scrobbling, &a.Config.Transcoding, &a.Config.Application) a.ImageManager = NewImageManager(a.bgrndCtx, a.ServerManager, cacheDir) + ac, err := NewAudioCache(a.bgrndCtx, a.ServerManager, filepath.Join(cacheDir, "audio")) + if err != nil { + log.Printf("failed to create audio cache: %s", err.Error()) + } + a.AudioCache = ac + a.PlaybackManager = NewPlaybackManager(a.bgrndCtx, a.ServerManager, a.LocalPlayer, &a.Config.Playback, &a.Config.Scrobbling, &a.Config.Transcoding, &a.Config.Application) a.Config.Application.MaxImageCacheSizeMB = clamp(a.Config.Application.MaxImageCacheSizeMB, 1, 500) a.ImageManager.SetMaxOnDiskCacheSizeBytes(int64(a.Config.Application.MaxImageCacheSizeMB) * 1_048_576) a.ServerManager.SetPrefetchAlbumCoverCallback(func(coverID string) { @@ -475,6 +481,9 @@ func (a *App) Shutdown() { } a.PlaybackManager.DisableCallbacks() a.PlaybackManager.Shutdown() // will trigger scrobble check + if a.AudioCache != nil { + a.AudioCache.Shutdown() + } a.cancel() a.LocalPlayer.Destroy() } diff --git a/sharedutil/sharedutil.go b/sharedutil/sharedutil.go index cf5bccb..e094364 100644 --- a/sharedutil/sharedutil.go +++ b/sharedutil/sharedutil.go @@ -1,6 +1,12 @@ package sharedutil import ( + "context" + "fmt" + "io" + "net/http" + "os" + "github.com/dweymouth/supersonic/backend/mediaprovider" ) @@ -133,3 +139,50 @@ func ReorderItems[T any](items []T, idxToMove []int, insertIdx int) []T { return newItems } + +// DownloadFileWithContext downloads a file from the specified URL and saves it to destPath. +// It respects the provided context and will cancel the request and cleanup if context is done. +// Returns an error if an error other than cancellation occurs, and returns true IFF the file was completely downloaded. +func DownloadFileWithContext(ctx context.Context, url string, destPath string) (bool, error) { + // Create HTTP request with context + req, err := http.NewRequestWithContext(ctx, http.MethodGet, url, nil) + if err != nil { + return false, fmt.Errorf("creating request: %w", err) + } + + // Perform the request + resp, err := http.DefaultClient.Do(req) + if err != nil { + return false, fmt.Errorf("performing request: %w", err) + } + defer resp.Body.Close() + + // Check for non-200 status codes + if resp.StatusCode != http.StatusOK { + return false, fmt.Errorf("bad status: %s", resp.Status) + } + + // Create the destination file + out, err := os.Create(destPath) + if err != nil { + return false, fmt.Errorf("creating file: %w", err) + } + defer out.Close() + + _, err = io.Copy(out, resp.Body) + + select { + case <-ctx.Done(): + // Cancelled, delete partial file + out.Close() + os.Remove(destPath) + return false, nil + default: + if err != nil { + os.Remove(destPath) + return false, fmt.Errorf("error copying data: %w", err) + } + } + + return true, nil +}