feat: detect actual audio format and resolve quality mismatch
This commit is contained in:
@@ -23,13 +23,15 @@ import (
|
||||
|
||||
type Task struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Source string `json:"source"`
|
||||
RID string `json:"rid"`
|
||||
Source string `gorm:"uniqueIndex:idx_tasks_source_rid_quality" json:"source"`
|
||||
RID string `gorm:"uniqueIndex:idx_tasks_source_rid_quality" json:"rid"`
|
||||
Title string `json:"title"`
|
||||
Artist string `json:"artist"`
|
||||
Album string `json:"album"`
|
||||
Pic string `json:"pic"`
|
||||
Quality string `json:"quality"`
|
||||
Quality string `gorm:"uniqueIndex:idx_tasks_source_rid_quality" json:"quality"`
|
||||
ResolvedQuality string `json:"resolvedQuality,omitempty"`
|
||||
ActualQuality string `json:"actualQuality,omitempty"`
|
||||
Format string `json:"format"`
|
||||
Status string `json:"status"`
|
||||
Error string `json:"error,omitempty"`
|
||||
@@ -70,6 +72,7 @@ type App struct {
|
||||
|
||||
type playInfo struct {
|
||||
URL string `json:"url"`
|
||||
Level string `json:"level"`
|
||||
Format string `json:"format"`
|
||||
Headers map[string]string `json:"headers"`
|
||||
}
|
||||
@@ -146,19 +149,31 @@ func (a *App) addTask(c *gin.Context) {
|
||||
c.JSON(400, gin.H{"error": "source/rid/title required"})
|
||||
return
|
||||
}
|
||||
if in.Source == "" {
|
||||
in.Source = "kw"
|
||||
}
|
||||
if in.Quality == "" {
|
||||
in.Quality = a.cfg.DefaultQuality
|
||||
}
|
||||
var existing Task
|
||||
err := a.db.Where("source = ? AND rid = ? AND quality = ?", in.Source, in.RID, in.Quality).Order("id desc").First(&existing).Error
|
||||
err := a.db.Where("source = ? AND r_id = ? AND quality = ?", in.Source, in.RID, in.Quality).Order("id desc").First(&existing).Error
|
||||
if err == nil {
|
||||
if existing.Status != "error" {
|
||||
c.JSON(200, existing)
|
||||
return
|
||||
}
|
||||
a.db.Model(&existing).Updates(map[string]any{"title": in.Title, "artist": in.Artist, "album": in.Album, "pic": in.Pic, "status": "pending", "error": ""})
|
||||
a.queue <- existing.ID
|
||||
c.JSON(202, existing)
|
||||
return
|
||||
}
|
||||
t := Task{Source: in.Source, RID: in.RID, Title: in.Title, Artist: in.Artist, Album: in.Album, Pic: in.Pic, Quality: in.Quality, Status: "pending"}
|
||||
if err := a.db.Create(&t).Error; err != nil {
|
||||
// A concurrent request may have inserted the same task; return that task.
|
||||
if a.db.Where("source = ? AND r_id = ? AND quality = ?", in.Source, in.RID, in.Quality).Order("id desc").First(&existing).Error == nil {
|
||||
c.JSON(200, existing)
|
||||
return
|
||||
}
|
||||
c.JSON(500, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
@@ -201,7 +216,9 @@ func (a *App) retryTask(c *gin.Context) {
|
||||
c.Status(404)
|
||||
return
|
||||
}
|
||||
a.db.Model(&t).Updates(map[string]any{"status": "pending", "error": ""})
|
||||
a.db.Model(&t).Updates(map[string]any{"status": "pending", "error": "", "progress": 0, "downloaded_bytes": 0})
|
||||
t.Status = "pending"
|
||||
t.Error = ""
|
||||
a.queue <- t.ID
|
||||
c.JSON(202, t)
|
||||
}
|
||||
@@ -216,24 +233,60 @@ func (a *App) download(id uint) {
|
||||
if a.db.First(&t, id).Error != nil {
|
||||
return
|
||||
}
|
||||
// A task may be queued twice; only pending tasks are eligible to start.
|
||||
if t.Status != "pending" {
|
||||
return
|
||||
}
|
||||
a.db.Model(&t).Updates(map[string]any{"status": "resolving", "error": "", "progress": 0})
|
||||
info, err := a.resolve(t)
|
||||
if err != nil {
|
||||
a.fail(&t, err)
|
||||
return
|
||||
}
|
||||
ext := info.Format
|
||||
priorLocal := t.LocalPath
|
||||
ext := normalizedFormat(info.Format)
|
||||
if ext == "" {
|
||||
ext = normalizedFormat(filepath.Ext(info.URL))
|
||||
}
|
||||
if ext == "" {
|
||||
ext = "mp3"
|
||||
}
|
||||
filename := safe(t.Artist) + " - " + safe(t.Title) + "." + safe(ext)
|
||||
filename := safe(t.Artist) + " - " + safe(t.Title) + "." + ext
|
||||
local := filepath.Join(a.cfg.DownloadDir, filename)
|
||||
t.Format = ext
|
||||
t.ResolvedQuality = info.Level
|
||||
t.ActualQuality = ""
|
||||
t.LocalPath = local
|
||||
if _, err = os.Stat(local); err == nil {
|
||||
if priorLocal != "" {
|
||||
if fi, statErr := os.Stat(priorLocal); statErr == nil && fi.Size() > 0 {
|
||||
local = priorLocal
|
||||
t.LocalPath = local
|
||||
}
|
||||
}
|
||||
if fi, statErr := os.Stat(local); statErr == nil && fi.Size() > 0 {
|
||||
actual, detectErr := detectAudioFormat(local)
|
||||
if detectErr == nil {
|
||||
actual = normalizedFormat(actual)
|
||||
if actual != t.Format {
|
||||
newLocal, renameErr := renameToActualFormat(local, actual)
|
||||
if renameErr != nil {
|
||||
a.fail(&t, renameErr)
|
||||
return
|
||||
}
|
||||
local = newLocal
|
||||
t.LocalPath = local
|
||||
}
|
||||
t.Format = actual
|
||||
t.ActualQuality = actualQuality(t.Quality, t.ResolvedQuality, actual)
|
||||
}
|
||||
t.TotalBytes = fi.Size()
|
||||
t.DownloadedBytes = fi.Size()
|
||||
t.Progress = 100
|
||||
a.db.Save(&t)
|
||||
a.upload(&t)
|
||||
return
|
||||
}
|
||||
a.db.Model(&t).Updates(map[string]any{"status": "downloading", "format": ext, "local_path": local})
|
||||
req, err := http.NewRequest("GET", info.URL, nil)
|
||||
if err != nil {
|
||||
a.fail(&t, err)
|
||||
@@ -301,10 +354,26 @@ func (a *App) download(id uint) {
|
||||
a.fail(&t, cerr)
|
||||
return
|
||||
}
|
||||
if err = os.Rename(tmp, local); err != nil {
|
||||
a.fail(&t, err)
|
||||
if t.TotalBytes > 0 && n != t.TotalBytes {
|
||||
a.fail(&t, fmt.Errorf("incomplete download: got %d of %d bytes", n, t.TotalBytes))
|
||||
return
|
||||
}
|
||||
actual, detectErr := detectAudioFormat(tmp)
|
||||
if detectErr != nil {
|
||||
a.fail(&t, fmt.Errorf("detect audio format: %w", detectErr))
|
||||
_ = os.Remove(tmp)
|
||||
return
|
||||
}
|
||||
actual = normalizedFormat(actual)
|
||||
local = strings.TrimSuffix(local, filepath.Ext(local)) + "." + actual
|
||||
if err = os.Rename(tmp, local); err != nil {
|
||||
a.fail(&t, err)
|
||||
_ = os.Remove(tmp)
|
||||
return
|
||||
}
|
||||
t.Format = actual
|
||||
t.LocalPath = local
|
||||
t.ActualQuality = actualQuality(t.Quality, t.ResolvedQuality, actual)
|
||||
t.DownloadedBytes = n
|
||||
if t.TotalBytes > 0 {
|
||||
t.Progress = 100
|
||||
@@ -377,7 +446,14 @@ func (a *App) upload(t *Task) {
|
||||
t.Progress = 100
|
||||
a.db.Save(t)
|
||||
}
|
||||
func (a *App) fail(t *Task, err error) { t.Status = "error"; t.Error = err.Error(); a.db.Save(t) }
|
||||
func (a *App) fail(t *Task, err error) {
|
||||
if t.LocalPath != "" {
|
||||
_ = os.Remove(t.LocalPath + ".part")
|
||||
}
|
||||
t.Status = "error"
|
||||
t.Error = err.Error()
|
||||
a.db.Save(t)
|
||||
}
|
||||
|
||||
func (a *App) getSettings(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"amApi": a.cfg.AMAPI, "downloadDir": a.cfg.DownloadDir, "webdavUrl": a.cfg.WebDAVURL, "webdavUser": a.cfg.WebDAVUser, "webdavPass": a.cfg.WebDAVPass, "webdavPath": a.cfg.WebDAVPath, "quality": a.cfg.DefaultQuality})
|
||||
|
||||
Reference in New Issue
Block a user