Compact HE-AAC audio saves: server lane with fdkaac, shrink saved songs from Saved videos

This commit is contained in:
Jonathan Sykes
2026-10-18 17:28:47 +08:00
parent eef9fbf05f
commit b48d36b3dd
12 changed files with 342 additions and 22 deletions

View File

@@ -658,7 +658,27 @@ async function renderSaved() {
c.innerHTML = '';
const summary = document.createElement('div');
summary.className = 'saved-summary saved-storage-summary';
summary.innerHTML = `<span class="saved-storage-text">${fmtBytes(total)} · ${items.length} video${items.length === 1 ? '' : 's'} stored offline</span>`;
const audioCount = items.filter((it) => /\.m4a$/i.test(it.name || '')).length;
summary.innerHTML = `<span class="saved-storage-text">${fmtBytes(total)} · ${items.length} saved${audioCount ? ` · ${audioCount} as audio` : ''}</span>`;
// Every saved YouTube song not yet in compact audio can be shrunk in place.
const shrinkable = items.filter((it) => canShrinkSaved(it));
if (shrinkable.length) {
const shrinkAll = document.createElement('button');
shrinkAll.type = 'button';
shrinkAll.className = 'saved-clear saved-shrink-all';
shrinkAll.textContent = `Shrink ${shrinkable.length}`;
shrinkAll.title = 'Replace these saves with compact audio (HE-AAC): about a third of the space, no video';
shrinkAll.setAttribute('aria-label', `Shrink ${shrinkable.length} saved songs to compact audio`);
shrinkAll.onclick = () => {
const before = shrinkable.reduce((n, it) => n + (it.size || 0), 0);
showModal(`Shrink ${shrinkable.length} saved song${shrinkable.length === 1 ? '' : 's'}?`, document.createTextNode(
`They are swapped for compact audio (HE-AAC, ~48 kbps): sound only, no video, about a third of an audio save and far less than a video (now ${fmtBytes(before)}). The server converts each song once; a song keeps playing from its current copy until the new one is in place.`), [
{ label: 'Cancel', onClick: closeModal },
{ label: 'Shrink', onClick: () => { closeModal(); shrinkSavedAll(shrinkable.map((it) => it.id)); } },
]);
};
summary.appendChild(shrinkAll);
}
summary.appendChild(clearAll);
$('listFilterBar').classList.remove('hidden');
c.appendChild(summary);
@@ -679,13 +699,20 @@ async function renderSaved() {
onerror="this.classList.add('no-art')" />${isCustom ? '<span class="edit-badge" title="Edited copy">✂</span>' : ''}${isUploadId(v.id) ? `<span class="upload-badge" title="On this server">${v.kind === 'audio' ? '♪' : '📁'}</span>` : ''}</div>
<div class="card-info">
<div class="card-title"></div>
<div class="card-channel saved-size">${isCustom ? '✂ edited · ' : ''}${fmtBytes(it.size)}</div>
<div class="card-channel saved-size">${isCustom ? '✂ edited · ' : ''}${savedFormatLabel(it)} · ${fmtBytes(it.size)}</div>
</div>`;
row.querySelector('.card-title').textContent = v.title || it.id;
row.addEventListener('click', (e) => {
if (e.target.closest('.saved-file-actions')) return;
playFromList([v], 0, 'saved');
});
const shrink = canShrinkSaved(it) ? async () => {
toast(`Shrinking “${v.title || v.id}”…`);
const r = await shrinkSaved(v);
toast(r.ok ? (r.compact ? `Shrunk ${fmtBytes(r.before)} → ${fmtBytes(r.after)}` : 'This song has no smaller version')
: '⚠ ' + (r.error || 'Could not shrink it'));
if (view.type === 'saved') renderSaved();
} : null;
SavedPage.actions(row, v, () => exportToDevice(v), () => {
showModal(`Delete saved file “${v.title || v.id}”?`, document.createTextNode(isCustom
? 'This edited copy exists only on this device and will be removed permanently.'
@@ -708,7 +735,7 @@ async function renderSaved() {
toast('Removed saved file'); renderSaved();
} },
]);
});
}, shrink);
c.appendChild(row);
});
if (!c.querySelector('.saved-card')) {
@@ -959,6 +986,13 @@ async function renderSettings() {
</span>
<input id="setAutoPreload" type="checkbox" ${data.settings.autoPreload ? 'checked' : ''} />
</label>
<label class="set-row">
<span>
Save songs as compact audio
<small>New saves keep only the sound, as HE-AAC (about a third of the space, no video). The server converts each song once. Shrink songs you already saved from Saved videos.</small>
</span>
<input id="setCompactAudio" type="checkbox" ${data.settings.compactAudio ? 'checked' : ''} />
</label>
<label class="set-row">
<span>
Videos saved at once
@@ -1355,6 +1389,10 @@ async function renderSettings() {
data.settings.autoSavePlayed = e.target.checked;
persist();
});
$('setCompactAudio').addEventListener('change', (e) => {
data.settings.compactAudio = e.target.checked;
persist();
});
$('setAutoPreload').addEventListener('change', (e) => {
data.settings.autoPreload = e.target.checked;
persist();