feat: make WebDAV version cleanup configurable

This commit is contained in:
OpenClaw Agent
2026-09-16 05:55:35 +08:00
parent 83224fe5b5
commit 94e84b53cf
2 changed files with 52 additions and 7 deletions
+3 -3
View File
@@ -13,7 +13,7 @@
<section id="home" class="view"><div class="hero"><h1>找到你想听的音乐</h1><p>搜索歌曲、歌手或专辑,加入下载队列即可。</p><form id="homeSearch" class="searchbar"><input id="homeKeyword" class="input" placeholder="输入歌曲名、歌手或专辑" autocomplete="off"><select id="homeSource" class="input"><option value="kw">小窝</option><option value="kg">小狗</option><option value="wy">小云</option><option value="tx">小Q</option></select><button class="btn">开始搜索</button></form></div><div class="card"><div class="section-head"><div class="title">最近下载</div><button class="btn alt" data-go="queue">查看全部</button></div><div id="recentTasks"></div></div></section>
<section id="search" class="view"><div class="card"><div class="section-head"><div><div class="title">搜索音乐</div><div class="muted">支持歌曲名、歌手、专辑</div></div></div><form id="searchForm" class="row"><input id="keyword" class="input" placeholder="输入关键词" autocomplete="off"><select id="source" class="input"><option value="kw">小窝</option><option value="kg">小狗</option><option value="wy">小云</option><option value="tx">小Q</option></select><button class="btn">搜索</button></form></div><div id="resultsCard" class="card" style="display:none"><div class="section-head"><div><div class="title">搜索结果</div><div id="resultHint" class="muted"></div></div></div><div id="results" class="results"></div><button id="loadMore" class="btn alt loadmore" style="display:none">加载更多</button></div></section>
<section id="queue" class="view"><div class="card"><div class="section-head"><div><div class="title">下载队列</div><div class="muted">任务会自动处理,完成后可在本地下载目录找到文件。</div></div></div><div id="taskTabs" class="task-tabs"><button class="active" data-filter="all">全部</button><button data-filter="active">进行中</button><button data-filter="done">已完成</button><button data-filter="error">失败</button></div><div id="tasks"></div></div></section>
<section id="settings" class="view"><div class="settings-grid"><div class="card setting-card"><h3>音乐 API</h3><p>用于搜索和解析音乐资源,与 Downloader Web 服务分离。</p><div class="field"><label>API 地址</label><input id="amApi" class="input" placeholder="http://127.0.0.1:9530"></div><div class="notice">当前服务架构:Downloader Web 使用 9540,音乐 API 使用 9530。</div></div><div class="card setting-card"><h3>下载</h3><p>设置文件保存位置与默认音质。</p><div class="field"><label>本地下载目录</label><input id="downloadDir" class="input"></div><div class="field"><label>默认下载品质</label><select id="defaultQuality" class="input"><option value="standard">标准</option><option value="exhigh">高品质</option><option value="lossless">无损</option><option value="hires">Hi-Res</option><option value="sky">Sky</option><option value="jyeffect">高清音效</option><option value="jymaster">母带</option><option value="atmos">Atmos</option><option value="atmos_plus">Atmos+</option><option value="master">Master</option></select></div></div><div class="card setting-card"><h3>WebDAV</h3><p>可选的 NAS/远程音乐库同步配置。</p><div class="field"><label>WebDAV 地址</label><input id="davUrl" class="input" placeholder="https://nas.example.com/dav/music"></div><div class="row"><div class="field"><label>用户名</label><input id="davUser" class="input"></div><div class="field"><label>密码</label><input id="davPass" type="password" class="input"></div></div><div class="field"><label>远程音乐目录</label><input id="davPath" class="input" placeholder="Music"></div><div class="row" style="margin-top:15px"><button id="testDav" type="button" class="btn alt">测试连接</button><button id="saveSettings" type="button" class="btn">保存设置</button></div></div></div></section>
<section id="settings" class="view"><div class="settings-grid"><div class="card setting-card"><h3>音乐 API</h3><p>用于搜索和解析音乐资源,与 Downloader Web 服务分离。</p><div class="field"><label>API 地址</label><input id="amApi" class="input" placeholder="http://127.0.0.1:9530"></div><div class="notice">当前服务架构:Downloader Web 使用 9540,音乐 API 使用 9530。</div></div><div class="card setting-card"><h3>下载</h3><p>设置文件保存位置与默认音质。</p><div class="field"><label>本地下载目录</label><input id="downloadDir" class="input"></div><div class="field"><label>默认下载品质</label><select id="defaultQuality" class="input"><option value="standard">标准</option><option value="exhigh">高品质</option><option value="lossless">无损</option><option value="hires">Hi-Res</option><option value="sky">Sky</option><option value="jyeffect">高清音效</option><option value="jymaster">母带</option><option value="atmos">Atmos</option><option value="atmos_plus">Atmos+</option><option value="master">Master</option></select></div></div><div class="card setting-card"><h3>WebDAV</h3><p>可选的 NAS/远程音乐库同步配置。</p><div class="field"><label>WebDAV 地址</label><input id="davUrl" class="input" placeholder="https://nas.example.com/dav/music"></div><div class="row"><div class="field"><label>用户名</label><input id="davUser" class="input"></div><div class="field"><label>密码</label><input id="davPass" type="password" class="input"></div></div><div class="field"><label>远程音乐目录</label><input id="davPath" class="input" placeholder="Music"></div><div class="field"><label>同曲不同音质</label><select id="davVersionPolicy" class="input"><option value="keep">保留所有版本</option><option value="replace_lower">高音质下载后清理旧低音质</option></select><div class="notice">“清理旧低音质”只删除数据库中已确认属于同一首歌、且音质低于当前版本的旧 WebDAV 文件,不会扫描或删除未知文件。</div></div><div class="row" style="margin-top:15px"><button id="testDav" type="button" class="btn alt">测试连接</button><button id="saveSettings" type="button" class="btn">保存设置</button></div></div></div></section>
</main><nav class="mobile-nav" id="mobileNav"><button data-view="home">⌂<br>首页</button><button data-view="search">⌕<br>搜索</button><button data-view="queue">♫ <span id="mobileBadge">0</span><br>下载</button><button data-view="settings">⚙<br>设置</button></nav></div>
<script>
const $=id=>document.getElementById(id);let searchPage=1,searchKeyword='',searchSource='kw',searchLoading=false,hasMore=false,taskFilter='all';
@@ -32,8 +32,8 @@ function taskHtml(t){let pct=Math.max(0,Math.min(100,Number(t.progress)||0));let
async function loadTasks(){try{const x=await api('/tasks');const all=x.data||[];const active=all.filter(t=>['pending','resolving','downloading'].includes(t.status));$('navBadge').textContent=active.length;$('mobileBadge').textContent=active.length;let arr=taskFilter==='active'?active:taskFilter==='done'?all.filter(t=>t.status==='done'):taskFilter==='error'?all.filter(t=>t.status==='error'):all;$('tasks').innerHTML=arr.length?arr.map(taskHtml).join(''):'<div class="empty">暂无任务</div>';$('tasks').querySelectorAll('.retry').forEach((b,i)=>b.addEventListener('click',async()=>{const errs=arr.filter(t=>t.status==='error');await api('/tasks/'+errs[i].id+'/retry',{method:'POST'});loadTasks()}))}catch(e){$('tasks').innerHTML='<div class="empty">加载失败:'+esc(e.message)+'</div>'}}
async function loadRecent(){try{const x=await api('/tasks');const a=(x.data||[]).slice(0,5);$('recentTasks').innerHTML=a.length?a.map(taskHtml).join(''):'<div class="empty">还没有下载记录</div>'}catch(e){$('recentTasks').innerHTML='<div class="empty">加载失败</div>'}}
document.querySelectorAll('#taskTabs button').forEach(b=>b.addEventListener('click',()=>{document.querySelectorAll('#taskTabs button').forEach(x=>x.classList.remove('active'));b.classList.add('active');taskFilter=b.dataset.filter;loadTasks()}));
async function loadSettings(){try{const s=await api('/settings');$('amApi').value=s.amApi||'';$('downloadDir').value=s.downloadDir||'';$('davUrl').value=s.webdavUrl||'';$('davUser').value=s.webdavUser||'';$('davPass').value=s.webdavPass||'';$('davPath').value=s.webdavPath||'';$('defaultQuality').value=s.quality||'standard'}catch(e){alert('设置加载失败:'+e.message)}}
$('saveSettings').addEventListener('click',async()=>{const b={amApi:$('amApi').value,downloadDir:$('downloadDir').value,webdavUrl:$('davUrl').value,webdavUser:$('davUser').value,webdavPass:$('davPass').value,webdavPath:$('davPath').value,quality:$('defaultQuality').value};try{await api('/settings',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});alert('设置已保存')}catch(e){alert('保存失败:'+e.message)}});
async function loadSettings(){try{const s=await api('/settings');$('amApi').value=s.amApi||'';$('downloadDir').value=s.downloadDir||'';$('davUrl').value=s.webdavUrl||'';$('davUser').value=s.webdavUser||'';$('davPass').value=s.webdavPass||'';$('davPath').value=s.webdavPath||'';$('defaultQuality').value=s.quality||'standard';$('davVersionPolicy').value=s.webdavVersionPolicy||'keep'}catch(e){alert('设置加载失败:'+e.message)}}
$('saveSettings').addEventListener('click',async()=>{const b={amApi:$('amApi').value,downloadDir:$('downloadDir').value,webdavUrl:$('davUrl').value,webdavUser:$('davUser').value,webdavPass:$('davPass').value,webdavPath:$('davPath').value,quality:$('defaultQuality').value,webdavVersionPolicy:$('davVersionPolicy').value};try{await api('/settings',{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});alert('设置已保存')}catch(e){alert('保存失败:'+e.message)}});
$('testDav').addEventListener('click',async()=>{try{const b={url:$('davUrl').value,user:$('davUser').value,pass:$('davPass').value,path:$('davPath').value};await api('/webdav/test',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(b)});alert('WebDAV 连接成功')}catch(e){alert('WebDAV 连接失败:'+e.message)}});
go(location.hash.slice(1)||'home');loadRecent();setInterval(()=>{loadTasks();if(document.getElementById('home').classList.contains('active'))loadRecent()},3000);
</script></body></html>
+49 -4
View File
@@ -59,8 +59,8 @@ type SongRequest struct {
Quality string `json:"quality"`
}
type Config struct {
AMAPI, DownloadDir, WebDAVURL, WebDAVUser, WebDAVPass, WebDAVPath, DefaultQuality string
WorkerCount int
AMAPI, DownloadDir, WebDAVURL, WebDAVUser, WebDAVPass, WebDAVPath, DefaultQuality, WebDAVVersionPolicy string
WorkerCount int
}
type App struct {
db *gorm.DB
@@ -86,6 +86,9 @@ func main() {
a := &App{db: db, cfg: Config{AMAPI: getenv("AM_API_URL", "http://127.0.0.1:9530/api"), DownloadDir: getenv("DOWNLOAD_DIR", "/downloads"), DefaultQuality: getenv("DEFAULT_QUALITY", "standard"), WorkerCount: atoi(getenv("WORKERS", "2"), 2)}, queue: make(chan uint, 100), httpClient: &http.Client{Timeout: 2 * time.Minute}}
_ = os.MkdirAll(a.cfg.DownloadDir, 0755)
a.loadSettings()
if a.cfg.WebDAVVersionPolicy == "" {
a.cfg.WebDAVVersionPolicy = "keep"
}
a.startWorkers()
r := gin.Default()
r.StaticFile("/", "/app/index.html")
@@ -120,6 +123,8 @@ func (a *App) loadSettings() {
a.cfg.WebDAVPath = s.Value
case "quality":
a.cfg.DefaultQuality = s.Value
case "webdav_version_policy":
a.cfg.WebDAVVersionPolicy = s.Value
}
}
}
@@ -442,10 +447,44 @@ func (a *App) upload(t *Task) {
a.fail(t, err)
return
}
if a.cfg.WebDAVVersionPolicy == "replace_lower" {
a.removeLowerQualityVersions(client, remote, t)
}
t.Status = "done"
t.Progress = 100
a.db.Save(t)
}
func (a *App) removeLowerQualityVersions(client *gowebdav.Client, remote string, t *Task) {
levels := map[string]int{"standard": 1, "exhigh": 2, "sq": 3, "lossless": 4, "hires": 5, "jyeffect": 6, "sky": 7, "jymaster": 8, "atmos": 9, "atmos_plus": 10, "master": 11}
current := levels[t.ActualQuality]
if current == 0 {
current = levels[t.ResolvedQuality]
}
if current == 0 {
current = levels[t.Quality]
}
if current == 0 {
return
}
var old []Task
a.db.Where("artist = ? AND title = ? AND album = ? AND status = ? AND id <> ?", t.Artist, t.Title, t.Album, "done", t.ID).Find(&old)
for _, candidate := range old {
q := candidate.ActualQuality
if q == "" {
q = candidate.ResolvedQuality
}
if q == "" {
q = candidate.Quality
}
if levels[q] <= 0 || levels[q] >= current || candidate.RemotePath == "" || candidate.RemotePath == remote {
continue
}
if _, err := client.Stat(candidate.RemotePath); err == nil {
_ = client.Remove(candidate.RemotePath)
}
}
}
func (a *App) fail(t *Task, err error) {
if t.LocalPath != "" {
_ = os.Remove(t.LocalPath + ".part")
@@ -456,7 +495,7 @@ func (a *App) fail(t *Task, err error) {
}
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})
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, "webdavVersionPolicy": a.cfg.WebDAVVersionPolicy})
}
func (a *App) putSettings(c *gin.Context) {
var v map[string]string
@@ -468,6 +507,9 @@ func (a *App) putSettings(c *gin.Context) {
a.cfg.AMAPI = x
a.save("am_api", x)
}
if p := v["webdavVersionPolicy"]; p != "keep" && p != "replace_lower" {
v["webdavVersionPolicy"] = "keep"
}
if x := v["downloadDir"]; x != "" {
a.cfg.DownloadDir = x
a.save("download_dir", x)
@@ -478,7 +520,10 @@ func (a *App) putSettings(c *gin.Context) {
a.cfg.WebDAVPass = v["webdavPass"]
a.cfg.WebDAVPath = strings.Trim(v["webdavPath"], "/")
a.cfg.DefaultQuality = v["quality"]
for k, x := range map[string]string{"webdav_url": a.cfg.WebDAVURL, "webdav_user": a.cfg.WebDAVUser, "webdav_pass": a.cfg.WebDAVPass, "webdav_path": a.cfg.WebDAVPath, "quality": a.cfg.DefaultQuality} {
if p := v["webdavVersionPolicy"]; p == "keep" || p == "replace_lower" {
a.cfg.WebDAVVersionPolicy = p
}
for k, x := range map[string]string{"webdav_url": a.cfg.WebDAVURL, "webdav_user": a.cfg.WebDAVUser, "webdav_pass": a.cfg.WebDAVPass, "webdav_path": a.cfg.WebDAVPath, "quality": a.cfg.DefaultQuality, "webdav_version_policy": a.cfg.WebDAVVersionPolicy} {
a.save(k, x)
}
c.JSON(200, gin.H{"ok": true})