Fix download progress and prevent re-downloading existing audio.
This commit is contained in:
@@ -0,0 +1,31 @@
|
|||||||
|
- [X] Networks listed
|
||||||
|
- [X] Cursor moves between networks
|
||||||
|
- [X] Enter opens a password prompt for a protected network
|
||||||
|
- [X] You can type and see what you are typing
|
||||||
|
- [X] Email address page appears
|
||||||
|
- [X] Can type in email
|
||||||
|
- [X] Can submit email
|
||||||
|
- [X] License Key page appears
|
||||||
|
- [X] Can type in license key
|
||||||
|
- [X] Can submit license key
|
||||||
|
- [X] Version string is correct
|
||||||
|
- [X] Status: Downloading Assets
|
||||||
|
- [X] F3 navigates to settings
|
||||||
|
- [X] Cursor moves
|
||||||
|
- [X] Can view application logs
|
||||||
|
- [X] No logs are leaking proprietary information
|
||||||
|
- [X] WiFi setup shows connected to a network
|
||||||
|
- [X] License Key shows current license key
|
||||||
|
- [X] I can backspace the license key
|
||||||
|
- [X] I cannot submit an incomplete key
|
||||||
|
- [X] I can type in the box
|
||||||
|
- [X] I can submit the new key
|
||||||
|
- [X] License Key shows updated key
|
||||||
|
- [X] Email address shows current email
|
||||||
|
- [X] I can backspace characters
|
||||||
|
- [X] I cannot submit an incomplete email
|
||||||
|
- [X] I can type in the box
|
||||||
|
- [X] I can submit the new email
|
||||||
|
- [X] Email address shows new email
|
||||||
|
- [X] Audio page loads
|
||||||
|
- [ ] I can toggle the audio on and off
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
- [X] Version string is correct
|
||||||
|
- [X] Status shows READY or Downloading Assets
|
||||||
|
- [X] Email address is present and correct
|
||||||
|
- [X] License key is present and correct
|
||||||
|
- [X] Settings menu opens
|
||||||
|
- [X] Logs open
|
||||||
|
- [X] No proprietary data is exposed in logs
|
||||||
|
- [X] Logs are colored correctly
|
||||||
|
- [X] WiFi Setup opens
|
||||||
|
- [X] WiFi page shows "Something went wrong" because there are no WiFi nics available
|
||||||
|
- [X] License key opens
|
||||||
|
- [X] License key displays current license key
|
||||||
|
- [X] Backspace deletes characters
|
||||||
|
- [X] I can type in a new license key
|
||||||
|
- [X] I can submit a license key
|
||||||
|
- [X] License Key shows new license key
|
||||||
|
- [X] I cannot submit an incomplete license key
|
||||||
|
- [X] Email address opens
|
||||||
|
- [X] Email addrss displays current email address
|
||||||
|
- [X] Backspace deletes characters
|
||||||
|
- [X] I can type a new email address
|
||||||
|
- [X] I can submit an email address
|
||||||
|
- [X] Email address shows new email address
|
||||||
|
- [X] I cannot submit an invalid email address
|
||||||
|
- [X] Audio opens
|
||||||
|
- [X] I can toggle audio
|
||||||
|
- [X] At least one audio device is listed
|
||||||
|
- [X] Volume is at 100%
|
||||||
|
- [X] I can change the volume
|
||||||
|
- [X] Returning to the Audio page shows toggle and volume updated values
|
||||||
|
- [X] Proclamation Browser opens
|
||||||
|
- [X] Proclamation Browser is populated
|
||||||
|
- [X] Space bar opens and closes categories
|
||||||
|
- [X] Enter opens and closes categories
|
||||||
|
- [X] Space toggles a proclamation
|
||||||
|
- [X] Enter toggles a proclamation
|
||||||
|
- [X] Typing characters starts a search
|
||||||
|
- [X] I can search for Confidence and ADHD
|
||||||
|
- [X] Enter during search toggles selection
|
||||||
|
- [X] Space during search toggles selection
|
||||||
|
- [X] Returning to the Proclamation browser shows my saved proclamations
|
||||||
|
- [X] Scroller opens and starts playing
|
||||||
|
- [X] Audio is playing
|
||||||
|
- [X] Colors appear correct
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
- [ ] Version string is correct
|
||||||
|
- [ ] Status shows READY or Downloading Assets
|
||||||
|
- [ ]
|
||||||
|
|
||||||
|
|
||||||
@@ -164,29 +164,32 @@ func Run() {
|
|||||||
sync.SyncProclamations(DB)
|
sync.SyncProclamations(DB)
|
||||||
sync.SyncCategories(DB)
|
sync.SyncCategories(DB)
|
||||||
sync.SyncFrequencies(DB)
|
sync.SyncFrequencies(DB)
|
||||||
audioFileNames := []string{
|
|
||||||
"OriginalAudio.wav",
|
// audioFileNames := []string{
|
||||||
"174Hz.wav",
|
// "OriginalAudio.wav",
|
||||||
"396Hz.wav",
|
// "174Hz.wav",
|
||||||
"417Hz.wav",
|
// "396Hz.wav",
|
||||||
"432Hz.wav",
|
// "417Hz.wav",
|
||||||
"639Hz.wav",
|
// "432Hz.wav",
|
||||||
"741Hz.wav",
|
// "639Hz.wav",
|
||||||
"852Hz.wav",
|
// "741Hz.wav",
|
||||||
"963Hz.wav",
|
// "852Hz.wav",
|
||||||
"1111Hz.wav",
|
// "963Hz.wav",
|
||||||
"14Hz_Binaural.wav",
|
// "1111Hz.wav",
|
||||||
"40Hz_Binaural.wav",
|
// "14Hz_Binaural.wav",
|
||||||
"AlphaWaves.wav",
|
// "40Hz_Binaural.wav",
|
||||||
"PinealGland.wav",
|
// "AlphaWaves.wav",
|
||||||
}
|
// "PinealGland.wav",
|
||||||
|
// }
|
||||||
|
|
||||||
config, err := db.GetConfig(app.DB)
|
config, err := db.GetConfig(app.DB)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go download.DownloadAudioFiles(app.PageRouter.Context.MessageChannel, audioFileNames, filepath.Join(config.AssetsDir, "audio"))
|
audioDir := filepath.Join(config.AssetsDir, "audio")
|
||||||
|
|
||||||
|
go download.DownloadAudioFiles(app.PageRouter.Context.MessageChannel, audioDir, "https://files.radiantwavetech.com/audio/")
|
||||||
|
|
||||||
app.runEventLoop()
|
app.runEventLoop()
|
||||||
}
|
}
|
||||||
|
|||||||
+242
-185
@@ -1,214 +1,271 @@
|
|||||||
package download
|
package download
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bufio"
|
||||||
|
"crypto/md5"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
|
||||||
"rhemawavetech.com/rhemawave/internal/logger"
|
"rhemawavetech.com/rhemawave/internal/logger"
|
||||||
)
|
)
|
||||||
|
|
||||||
// DownloadStatus holds information related to downloads and is
|
// DownloadStatus holds information related to downloads and is
|
||||||
// the container for information returned via channels within this
|
// the container for information returned via channels within this package.
|
||||||
// package.
|
|
||||||
type DownloadStatus struct {
|
type DownloadStatus struct {
|
||||||
Max int64 // Total bytes to download
|
Max int64
|
||||||
Cur int64 // Total bytes downloaded
|
Cur int64
|
||||||
Msg string // Status message
|
Msg string
|
||||||
Err error // Error, if any
|
Err error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ensureDir checks if a dire
|
// manifest maps filename → expected MD5 hex string
|
||||||
|
type manifest map[string]string
|
||||||
|
|
||||||
|
// ─── Server script to generate checksums.md5 ─────────────────────────────────
|
||||||
|
//
|
||||||
|
// Run this on your file server whenever you add or replace audio files:
|
||||||
|
//
|
||||||
|
// cd /path/to/audio && md5sum *.mp3 > checksums.md5
|
||||||
|
//
|
||||||
|
// That's it. The app will pick up changes automatically on next run.
|
||||||
|
// ─────────────────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
const manifestFilename = "checksums.md5"
|
||||||
|
|
||||||
|
// DownloadAudioFiles downloads audio files, skipping any that already exist
|
||||||
|
// locally with a matching MD5. Files with a wrong checksum are deleted and
|
||||||
|
// re-downloaded. Progress is sent to progressChan, which is closed on return.
|
||||||
|
func DownloadAudioFiles(progressChan chan<- DownloadStatus, destDir string, urlBase string) {
|
||||||
|
defer close(progressChan)
|
||||||
|
|
||||||
|
send := func(cur, max int64, msg string, err error) {
|
||||||
|
progressChan <- DownloadStatus{Cur: cur, Max: max, Msg: msg, Err: err}
|
||||||
|
}
|
||||||
|
|
||||||
|
send(0, 0, "Fetching file manifest...", nil)
|
||||||
|
m, err := fetchManifest(urlBase + manifestFilename)
|
||||||
|
if err != nil {
|
||||||
|
send(0, 0, "Failed to fetch manifest", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Info("download: manifest loaded, %d files listed", len(m))
|
||||||
|
|
||||||
|
send(0, 0, "Comparing checksums...", nil)
|
||||||
|
|
||||||
|
type work struct {
|
||||||
|
filename string
|
||||||
|
expectedMD5 string
|
||||||
|
}
|
||||||
|
var queue []work
|
||||||
|
|
||||||
|
for filename, expectedMD5 := range m {
|
||||||
|
localPath := filepath.Join(destDir, filename)
|
||||||
|
localMD5, err := fileMD5(localPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Debug("download: %s not found locally, queuing", filename)
|
||||||
|
queue = append(queue, work{filename, expectedMD5})
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if localMD5 != expectedMD5 {
|
||||||
|
logger.Debug("download: %s checksum mismatch (got %s, want %s), re-downloading",
|
||||||
|
filename, localMD5, expectedMD5)
|
||||||
|
if err := os.Remove(localPath); err != nil {
|
||||||
|
logger.Warning("download: failed to remove corrupt file %s: %v", localPath, err)
|
||||||
|
}
|
||||||
|
queue = append(queue, work{filename, expectedMD5})
|
||||||
|
} else {
|
||||||
|
logger.Debug("download: %s OK", filename)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(queue) == 0 {
|
||||||
|
send(0, 0, "All files are up to date.", nil)
|
||||||
|
logger.Info("download: nothing to do")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
total := len(queue)
|
||||||
|
send(0, 0, fmt.Sprintf("0 / %d Files Downloaded", total), nil)
|
||||||
|
logger.Info("download: %d file(s) to download", total)
|
||||||
|
|
||||||
|
totalDownloaded := int64(0)
|
||||||
|
filesDownloaded := 0
|
||||||
|
|
||||||
|
for _, w := range queue {
|
||||||
|
url := urlBase + w.filename
|
||||||
|
destPath := filepath.Join(destDir, w.filename)
|
||||||
|
|
||||||
|
logger.Debug("download: starting %s", w.filename)
|
||||||
|
|
||||||
|
downloaded, err := downloadFile(url, destPath, func(n int64) {
|
||||||
|
totalDownloaded += n
|
||||||
|
send(totalDownloaded, 0, fmt.Sprintf("%d / %d Files Downloaded", filesDownloaded, total), nil)
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("download: failed to download %s: %v", w.filename, err)
|
||||||
|
send(totalDownloaded, 0, fmt.Sprintf("%d / %d Files Downloaded", filesDownloaded, total), err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
gotMD5, err := fileMD5(destPath)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("download: could not hash %s after download: %v", w.filename, err)
|
||||||
|
os.Remove(destPath)
|
||||||
|
send(totalDownloaded, 0, "Checksum verification failed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if gotMD5 != w.expectedMD5 {
|
||||||
|
err := fmt.Errorf("checksum mismatch after download: got %s, want %s", gotMD5, w.expectedMD5)
|
||||||
|
logger.Error("download: %s %v", w.filename, err)
|
||||||
|
os.Remove(destPath)
|
||||||
|
send(totalDownloaded, 0, "Checksum verification failed — file removed", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
filesDownloaded++
|
||||||
|
statusMsg := fmt.Sprintf("%d / %d Files Downloaded", filesDownloaded, total)
|
||||||
|
logger.Info("download: %s (%d bytes)", statusMsg, downloaded)
|
||||||
|
send(totalDownloaded, 0, statusMsg, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
send(totalDownloaded, totalDownloaded, "All downloads complete!", nil)
|
||||||
|
logger.Info("download: all files downloaded and verified")
|
||||||
|
}
|
||||||
|
|
||||||
|
// ─── Helpers ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
// fetchManifest downloads and parses a checksums.md5 file.
|
||||||
|
// Format per line: "<md5hex> <filename>" (two spaces, same as md5sum output)
|
||||||
|
func fetchManifest(url string) (manifest, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("fetch manifest: %w", err)
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return nil, fmt.Errorf("fetch manifest: server returned %s", resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
m := make(manifest)
|
||||||
|
scanner := bufio.NewScanner(resp.Body)
|
||||||
|
for scanner.Scan() {
|
||||||
|
line := strings.TrimSpace(scanner.Text())
|
||||||
|
if line == "" || strings.HasPrefix(line, "#") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
// md5sum format: "hash filename" (two spaces)
|
||||||
|
parts := strings.SplitN(line, " ", 2)
|
||||||
|
if len(parts) != 2 {
|
||||||
|
logger.Warning("download: skipping malformed manifest line: %q", line)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
hash := strings.TrimSpace(parts[0])
|
||||||
|
name := strings.TrimSpace(parts[1])
|
||||||
|
m[name] = hash
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("parse manifest: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// fileMD5 computes the MD5 checksum of a local file.
|
||||||
|
// Returns an error if the file doesn't exist or can't be read.
|
||||||
|
func fileMD5(path string) (string, error) {
|
||||||
|
f, err := os.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
|
||||||
|
h := md5.New()
|
||||||
|
if _, err := io.Copy(h, f); err != nil {
|
||||||
|
return "", fmt.Errorf("hash %s: %w", path, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%x", h.Sum(nil)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// remoteFileSize does a HEAD request and returns Content-Length.
|
||||||
|
func remoteFileSize(url string) (int64, error) {
|
||||||
|
resp, err := http.Head(url)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
resp.Body.Close()
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return 0, fmt.Errorf("HEAD %s: %s", url, resp.Status)
|
||||||
|
}
|
||||||
|
return resp.ContentLength, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// downloadFile streams url to destPath, calling onChunk(n) after each write
|
||||||
|
// with the number of bytes just written. Returns total bytes downloaded.
|
||||||
|
func downloadFile(url, destPath string, onChunk func(int64)) (int64, error) {
|
||||||
|
resp, err := http.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
|
||||||
|
if resp.StatusCode != http.StatusOK {
|
||||||
|
return 0, fmt.Errorf("GET %s: %s", url, resp.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
out, err := os.Create(destPath)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("create %s: %w", destPath, err)
|
||||||
|
}
|
||||||
|
defer out.Close()
|
||||||
|
|
||||||
|
var total int64
|
||||||
|
buf := make([]byte, 32*1024) // 32KB chunks
|
||||||
|
|
||||||
|
for {
|
||||||
|
n, err := resp.Body.Read(buf)
|
||||||
|
if n > 0 {
|
||||||
|
if _, werr := out.Write(buf[:n]); werr != nil {
|
||||||
|
os.Remove(destPath)
|
||||||
|
return total, fmt.Errorf("write %s: %w", destPath, werr)
|
||||||
|
}
|
||||||
|
total += int64(n)
|
||||||
|
onChunk(int64(n))
|
||||||
|
}
|
||||||
|
if err == io.EOF {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
os.Remove(destPath)
|
||||||
|
return total, fmt.Errorf("read %s: %w", url, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ensureDir creates path and all parents if they don't exist.
|
||||||
func ensureDir(path string) error {
|
func ensureDir(path string) error {
|
||||||
info, err := os.Stat(path)
|
info, err := os.Stat(path)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
if !info.IsDir() {
|
if !info.IsDir() {
|
||||||
return fmt.Errorf("path exists but is not a directory: %s", path)
|
return fmt.Errorf("path exists but is not a directory: %s", path)
|
||||||
}
|
}
|
||||||
fmt.Printf("Directory already exists: %s\n", path)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if !os.IsNotExist(err) {
|
if !os.IsNotExist(err) {
|
||||||
return fmt.Errorf("error checking path: %w", err)
|
return fmt.Errorf("stat %s: %w", path, err)
|
||||||
}
|
}
|
||||||
|
if err := os.MkdirAll(path, 0o750); err != nil {
|
||||||
// 0760 = rwxrw---- (owner: rwx, group: rw-, others: ---)
|
return fmt.Errorf("mkdir %s: %w", path, err)
|
||||||
if err := os.MkdirAll(path, 0760); err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory: %w", err)
|
|
||||||
}
|
}
|
||||||
|
logger.Info("download: created directory %s", path)
|
||||||
logger.Info("Directory created: %s\n", path)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// DownloadAudioFiles downloads multiple audio files and sends progress updates
|
|
||||||
// to the provided channel. The channel is closed when all downloads are complete.
|
|
||||||
func DownloadAudioFiles(progressChan chan<- DownloadStatus, filenames []string, destDir string) {
|
|
||||||
defer close(progressChan)
|
|
||||||
|
|
||||||
logger.Info("Starting downloads")
|
|
||||||
|
|
||||||
if err := ensureDir(destDir); err != nil {
|
|
||||||
logger.Error("failed to ensureDir %s %v", destDir, err)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to ensure directory exists: %v", err),
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
urlBase := "https://files.radiantwavetech.com/audio/"
|
|
||||||
|
|
||||||
// First, calculate the total size of files that need to be downloaded
|
|
||||||
totalSize := int64(0)
|
|
||||||
filesToDownload := []string{}
|
|
||||||
totalDownloaded := int64(0)
|
|
||||||
|
|
||||||
for _, filename := range filenames {
|
|
||||||
logger.Debug("Downloading %s", filename)
|
|
||||||
destPath := filepath.Join(destDir, filename)
|
|
||||||
// Check if the file already exists
|
|
||||||
url := urlBase + filename
|
|
||||||
resp, err := http.Head(url)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("Failed to get size for %s: %v", filename, err)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to get size for %s: %v", filename, err),
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
logger.Error("Failed to get size for %s: status %s", filename, resp.Status)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to get size for %s: status %s", filename, resp.Status),
|
|
||||||
Err: fmt.Errorf("bad status: %s", resp.Status),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
fileStat, err := os.Stat(destPath)
|
|
||||||
// If the file is incomplete, or does not exist
|
|
||||||
if os.IsNotExist(err) || fileStat.Size() != resp.ContentLength {
|
|
||||||
// log a warning if the file is not complete
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
logger.Warning("%s corrupt. Adding to download queue.", filename)
|
|
||||||
} else {
|
|
||||||
logger.Debug("%s not found. Adding to download queue.", filename)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Append it to the list to download
|
|
||||||
filesToDownload = append(filesToDownload, filename)
|
|
||||||
} else if err != nil {
|
|
||||||
logger.Error("unknown error: %v", err)
|
|
||||||
} else {
|
|
||||||
logger.Warning("File %s exists, skipping...", filename)
|
|
||||||
totalDownloaded += fileStat.Size()
|
|
||||||
}
|
|
||||||
// Calculate the total of ALL files to give the illusion of
|
|
||||||
// "picking up where you left off" to the user
|
|
||||||
totalSize += resp.ContentLength
|
|
||||||
resp.Body.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send initial status
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Max: totalSize,
|
|
||||||
Cur: 0,
|
|
||||||
Msg: "Starting download...",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Download each file that doesn't exist
|
|
||||||
for _, filename := range filesToDownload {
|
|
||||||
url := urlBase + filename
|
|
||||||
destPath := filepath.Join(destDir, filename)
|
|
||||||
|
|
||||||
// Ensure the destination directory exists
|
|
||||||
|
|
||||||
// Create the destination file
|
|
||||||
out, err := os.Create(destPath)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("Failed to create file %s: %v", destPath, err)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to create file %s: %v", destPath, err),
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the file
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
out.Close()
|
|
||||||
os.Remove(destPath)
|
|
||||||
logger.Error("Failed to download %s: %v", filename, err)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to download %s: %v", filename, err),
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
out.Close()
|
|
||||||
os.Remove(destPath)
|
|
||||||
logger.Error("Failed to download %s: status %s", filename, resp.Status)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to download %s: status %s", filename, resp.Status),
|
|
||||||
Err: fmt.Errorf("bad status: %s", resp.Status),
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use a buffered reader for efficiency
|
|
||||||
buf := make([]byte, 8192)
|
|
||||||
for {
|
|
||||||
n, err := resp.Body.Read(buf)
|
|
||||||
if err != nil && err != io.EOF {
|
|
||||||
out.Close()
|
|
||||||
os.Remove(destPath)
|
|
||||||
logger.Error("Failed to read %s: %v", filename, err)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to read %s: %v", filename, err),
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if n == 0 {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
// Write to file
|
|
||||||
if _, err := out.Write(buf[:n]); err != nil {
|
|
||||||
out.Close()
|
|
||||||
os.Remove(destPath)
|
|
||||||
logger.Error("Failed to write %s: %v", filename, err)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Msg: fmt.Sprintf("Failed to write %s: %v", filename, err),
|
|
||||||
Err: err,
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// Update progress
|
|
||||||
totalDownloaded += int64(n)
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Max: totalSize,
|
|
||||||
Cur: totalDownloaded,
|
|
||||||
Msg: fmt.Sprintf("%s", filename),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Send final status
|
|
||||||
progressChan <- DownloadStatus{
|
|
||||||
Max: totalSize,
|
|
||||||
Cur: totalDownloaded,
|
|
||||||
Msg: "All audio downloads completed!",
|
|
||||||
}
|
|
||||||
logger.Info("All audio downloads complete!")
|
|
||||||
}
|
|
||||||
|
|||||||
+35
-46
@@ -171,72 +171,61 @@ func (p *Home) Update(dt float32) error {
|
|||||||
select {
|
select {
|
||||||
case progressUpdate, ok = <-p.Context.MessageChannel:
|
case progressUpdate, ok = <-p.Context.MessageChannel:
|
||||||
gotUpdate = true
|
gotUpdate = true
|
||||||
|
|
||||||
if !ok {
|
if !ok {
|
||||||
// Channel was closed while draining.
|
p.setMessage("Status: READY", colors.Green)
|
||||||
newMsg, err := NewStringItem("Status: READY", p.font, colors.Green)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
messageScale := float32(28) / float32(p.baseFontSize)
|
|
||||||
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
|
||||||
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
|
||||||
p.message = newMsg
|
|
||||||
p.isDownloadComplete = true
|
p.isDownloadComplete = true
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
default:
|
default:
|
||||||
goto drained
|
goto drained
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
drained:
|
drained:
|
||||||
|
|
||||||
if !gotUpdate {
|
if !gotUpdate {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
if progressUpdate.Err != nil {
|
if progressUpdate.Err != nil {
|
||||||
// Handle error
|
p.setMessage(fmt.Sprintf("Error: %s", progressUpdate.Err.Error()), colors.Red)
|
||||||
newMsg, err := NewStringItem(
|
return nil
|
||||||
fmt.Sprintf("Status: %s", progressUpdate.Err.Error()),
|
|
||||||
p.font,
|
|
||||||
colors.Red,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
messageScale := float32(28) / float32(p.baseFontSize)
|
|
||||||
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
|
||||||
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
|
||||||
p.message = newMsg
|
|
||||||
|
|
||||||
} else {
|
|
||||||
// Update progress message using ONLY the latest update.
|
|
||||||
percentComplete := float64(progressUpdate.Cur) / float64(progressUpdate.Max) * 100
|
|
||||||
|
|
||||||
msg := fmt.Sprintf(
|
|
||||||
"Status: Downloading assets %.1f%% Complete",
|
|
||||||
percentComplete,
|
|
||||||
)
|
|
||||||
|
|
||||||
newMsg, err := NewStringItem(msg, p.font, colors.White)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
messageScale := float32(28) / float32(p.baseFontSize)
|
|
||||||
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
|
||||||
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
|
||||||
p.message = newMsg
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Build the display string based on what the download package tells us.
|
||||||
|
var msg string
|
||||||
|
if progressUpdate.Max <= 0 {
|
||||||
|
// No total known yet — show the descriptive message from the package.
|
||||||
|
// This covers: "Fetching file manifest", "Checking local files",
|
||||||
|
// "Comparing checksums", "Building download list", etc.
|
||||||
|
msg = fmt.Sprintf("Status: %s", progressUpdate.Msg)
|
||||||
|
} else {
|
||||||
|
// We have a known total — show percentage.
|
||||||
|
pct := float64(progressUpdate.Cur) / float64(progressUpdate.Max) * 100
|
||||||
|
msg = fmt.Sprintf("Status: Downloading Assets — %.1f%%", pct)
|
||||||
|
}
|
||||||
|
|
||||||
|
p.setMessage(msg, colors.White)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// setMessage deletes the old message texture and builds a new one.
|
||||||
|
func (p *Home) setMessage(text string, color sdl.Color) {
|
||||||
|
if p.message.ID != 0 {
|
||||||
|
gl.DeleteTextures(1, &p.message.ID)
|
||||||
|
p.message.ID = 0
|
||||||
|
}
|
||||||
|
|
||||||
|
newMsg, err := NewStringItem(text, p.font, color)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
messageScale := float32(28) / float32(p.baseFontSize)
|
||||||
|
newMsg.W = int32(float32(newMsg.W) * messageScale)
|
||||||
|
newMsg.H = int32(float32(newMsg.H) * messageScale)
|
||||||
|
p.message = newMsg
|
||||||
|
}
|
||||||
|
|
||||||
func (p *Home) Render() error {
|
func (p *Home) Render() error {
|
||||||
baseFontSize := p.baseFontSize
|
baseFontSize := p.baseFontSize
|
||||||
sm := shaderManager.Get()
|
sm := shaderManager.Get()
|
||||||
|
|||||||
Reference in New Issue
Block a user