fix(local-audio): reference-aware cleanup, upload quota, stricter validation

Uploaded local files were deleted whenever a track left the current slot,
with no check on whether the file was still needed — causing data loss in
several flows. Replace with reference-aware cleanup: a file is deleted only
once it has been played AND is no longer referenced by ANY bot's queue
(BotManager.getReferencedLocalSongIds wired into the provider via
setInUseResolver), with the sweep run AFTER each queue mutation.

Fixes:
- play-song replay no longer deletes the file it is about to play
- loop / repeat-all / prev no longer destroy uploads mid-cycle
- a shared upload queued on multiple bots is not deleted while still in use
- !play / play-playlist / play-album clean the whole replaced queue, and an
  empty/failed playlist/album load keeps the previous queue + files intact
- bound disk use with an upload quota (evict oldest UNREFERENCED files)
- validate uploads by extension against the audio whitelist (never trust the
  client Content-Type); the stored extension is always a known audio type

Deletion now unlinks the file FIRST and drops the record only on success,
with a bounded non-blocking retry for briefly-locked files (Windows/ffmpeg),
so a failed unlink never orphans a file or diverges index.json. The quota
never evicts the just-uploaded file, and long filenames keep their extension.

Adds src/music/local.test.ts covering the cleanup lifecycle, quota eviction,
and upload validation.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
saopig1andClaude Opus 4.8 committed 2026-06-30 15:58:22 +08:00
1 parent e12cbf8863
commit e849db2286
5 files changed
+453 -77

No files matched your search

+9 -5
View File
@@ -297,7 +297,6 @@ export function createPlayerRouter(
// Stop current playback
bot.getPlayer().stop();
bot.cleanupQueuedLocalSongs?.("queue_replaced");
bot.getPlayer().resetFailures();
const songs = await provider.getPlaylistSongs(playlistId);
@@ -330,11 +329,14 @@ export function createPlayerRouter(
}
const queue = bot.getQueueManager();
bot.cleanupQueuedLocalSongs?.("queue_replaced");
queue.clear();
for (const song of queueable) {
queue.add({ ...song, platform: provider.platform });
}
// Sweep AFTER the queue is rebuilt: the previous queue's local uploads are
// released and deleted, but an empty/failed playlist (early return above)
// leaves the previous queue — and its files — intact.
bot.cleanupQueuedLocalSongs?.("queue_replaced");
// Use queue.play() for sequential, or pick random index for random modes
const mode = queue.getMode();
@@ -388,7 +390,6 @@ export function createPlayerRouter(
// Stop current playback
bot.getPlayer().stop();
bot.cleanupQueuedLocalSongs?.("queue_replaced");
bot.getPlayer().resetFailures();
const songs = await provider.getAlbumSongs(albumId);
@@ -414,11 +415,12 @@ export function createPlayerRouter(
}
const queue = bot.getQueueManager();
bot.cleanupQueuedLocalSongs?.("queue_replaced");
queue.clear();
for (const song of queueable) {
queue.add({ ...song, platform: provider.platform });
}
// Sweep AFTER the queue is rebuilt (see play-playlist).
bot.cleanupQueuedLocalSongs?.("queue_replaced");
const mode = queue.getMode();
let first;
@@ -464,13 +466,15 @@ export function createPlayerRouter(
}
const queue = bot.getQueueManager();
bot.getPlayer().stop();
bot.cleanupQueuedLocalSongs?.("queue_replaced");
queue.clear();
queue.add(song);
queue.play();
bot.getPlayer().resetFailures();
const ok = await bot.resolveAndPlay(queue.current()!);
// Sweep AFTER the new song is queued+resolved, so replaying a local song
// that was still in the queue doesn't delete the file we're about to play.
bot.cleanupQueuedLocalSongs?.("queue_replaced");
if (!ok) {
res.json({ ok: false, message: `无法播放「${song.name || song.id}」(区域/版权限制)` });
return;