diff --git a/internal/page/scrollerPage.go b/internal/page/scrollerPage.go index 5f19b3d..6992935 100644 --- a/internal/page/scrollerPage.go +++ b/internal/page/scrollerPage.go @@ -2,7 +2,11 @@ package page import ( "fmt" + "io" "math" + "net/http" + "os" + "path/filepath" "strings" "github.com/go-gl/gl/v3.3-core/gl" @@ -23,6 +27,87 @@ const ( defaultProclamationPlayCount int = 1 ) +// ProgressUpdate is sent over a channel as bytes are read from the network +// during a download. Read/Total let a consumer compute percentage; Done +// signals a clean finish; Err signals failure. The channel carrying these +// is closed after the final update (whether that's a Done or an Err), so +// ranging over it is sufficient to know when the download has finished. +type ProgressUpdate struct { + Read int64 + Total int64 + Done bool + Err error +} + +// ProgressReader wraps an io.Reader and emits a ProgressUpdate on every +// Read() call. It never blocks on the send: if nobody is currently +// listening on updates (or the consumer is behind), the update is dropped +// rather than stalling the download itself. +type ProgressReader struct { + Reader io.Reader + Total int64 + read int64 + updates chan<- ProgressUpdate +} + +func (pr *ProgressReader) Read(p []byte) (int, error) { + n, err := pr.Reader.Read(p) + if n > 0 { + pr.read += int64(n) + select { + case pr.updates <- ProgressUpdate{Read: pr.read, Total: pr.Total}: + default: + } + } + return n, err +} + +// downloadFile starts the download in its own goroutine and returns +// immediately with a channel of progress updates. The caller ranges over +// the returned channel to observe progress; the channel is closed when the +// download finishes, successfully or not, so the loop ends naturally. +func downloadFile(url, destPath string) <-chan ProgressUpdate { + updates := make(chan ProgressUpdate, 16) + + go func() { + defer close(updates) + + resp, err := http.Get(url) + if err != nil { + updates <- ProgressUpdate{Err: err} + return + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + updates <- ProgressUpdate{Err: fmt.Errorf("bad status: %s", resp.Status)} + return + } + + out, err := os.Create(destPath) + if err != nil { + updates <- ProgressUpdate{Err: err} + return + } + defer out.Close() + + pr := &ProgressReader{ + Reader: resp.Body, + Total: resp.ContentLength, // -1 if server didn't send it + updates: updates, + } + + if _, err := io.Copy(out, pr); err != nil { + updates <- ProgressUpdate{Err: err} + return + } + + updates <- ProgressUpdate{Read: pr.read, Total: pr.Total, Done: true} + }() + + return updates +} + type ScrollerProclamation struct { db.Proclamation colorPattern.ColorPattern @@ -260,31 +345,42 @@ func (p *ScrollerPage) Init(ctx *PageCtx) error { } } - audioFilePath := p.Context.DbPtr.Find + var selectedAudio db.AudioFile - // Download if it doesn't exist - fmt.Printf("%s", audioFilePath) + if err := p.Context.DbPtr.Where("Selected = ?", true).First(&selectedAudio).Error; err != nil { + logger.Error("error querying database for selected audio: %v", err) + } else { + _, err = os.Stat(selectedAudio.Path) + if err != nil { // If err == nil, then the file was most likely not found + url := "https://files.radiantwavetech.com/audio/" + filepath.Base(selectedAudio.Path) - // // Check if the track has previously been downloaded - // // TODO: Download audio via a goroutine - // // this is a path - // // figure out how to get the filename, not the path, from the path. - // _, err = os.Stat(audioFilePath) - // if err != nil { - // filename := filepath.Base(audioFilePath) - // url := "https://files.radiantwavetech.com/audio/" + filename - // logger.Info("%s not present. Downloading...", entry.DisplayName) - // // Download the track if not, display loading bar - // if err = netreq.Download(filename, filepath.Join(p.Base.Context.CfgPtr.AssetsDir, "audio"), url); err != nil { - // logger.Error("Failed to download file: %v", err) - // return - // } - // logger.Info("Downloaded %s", entry.DisplayName) - // } + updates := downloadFile(url, selectedAudio.Path) + var downloadErr error + for u := range updates { + if u.Err != nil { + downloadErr = u.Err + break + } + if u.Total > 0 { + pct := float64(u.Read) / float64(u.Total) * 100 + fmt.Printf("\rDownloaded %d / %d bytes (%.1f%%)", u.Read, u.Total, pct) + } else { + fmt.Printf("\rDownloaded %d bytes", u.Read) + } + if u.Done { + fmt.Println() + } + } + if downloadErr != nil { + return fmt.Errorf("failed to downoad audio file: %v", downloadErr) + } + } + } - // if err := mixer.Play(audioFile, true, p.Context.CfgPtr); err != nil { - // logger.Error("Mixer returned an error: %v", err) - // } + logger.Info("Playing %s", selectedAudio.Path) + if err := mixer.Play(selectedAudio.Path, true, p.Context.CfgPtr); err != nil { + logger.Error("Mixer returned an error: %v", err) + } currentDevice := mixer.CurrentDevice() if currentDevice == "" {