Audio songs: one-folder storage, version-aware download/share, GPU-checked renders
Storage structure
- All audio tracks (primary + per-language) now live in one folder per song with
unique lowercase names ({slug}-{lang}-{id}); no more tracks/ subfolder.
- Generated renders (download video + HLS) moved into the song's local-only cache/
subfolder, separated from source files (never synced to NAS, safe to wipe).
- tracks:reorganize artisan command (dry-run default) consolidates legacy files,
updates DB paths, and deletes orphans + empty folders.
- CLAUDE.md documents the canonical layout as a global rule (identical local + NAS).
Version-aware download & share
- Download MP3/Video and Share now act on the version being played; ?track={id}
is carried through share links and auto-selects audio + title + flag + about +
OG/meta on open.
GPU + visualizer
- Setting::gpuUsable() runs a cached health probe (nvidia-smi + nvenc smoke test,
256x144) before sending any encode to the GPU; falls back to CPU otherwise.
- Visualizer "Download Video" bakes in equal-width, cover-coloured, translucent
frequency bars; loop-filter rebuild makes generation ~25x faster.
Image cropper
- result-callback mode + per-song cover-slide cropper in upload/edit (modal + mobile).
Co-Authored-By: Claude Opus 4.7 (1M context) <[email protected]>
This commit is contained in:
1 parent
66fd78c10f
commit
a4384113c2
24 files changed
+1738
-258
No files matched your search
@@ -196,7 +196,7 @@
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" id="video-description" rows="4" class="form-textarea" placeholder="Tell viewers about your video (Markdown supported)"></textarea>
|
||||
<x-rich-text-editor name="description" id="video-description" placeholder="Tell viewers about your video…" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -266,9 +266,7 @@
|
||||
<!-- Description (audio mode) -->
|
||||
<div style="margin-bottom:14px;">
|
||||
<label class="form-label" style="font-size:13px;margin-bottom:6px;">Description</label>
|
||||
<textarea id="lt-track1-desc-create" class="form-textarea"
|
||||
rows="2" style="font-size:13px;min-height:64px;"
|
||||
placeholder="Tell viewers about this track…"></textarea>
|
||||
<x-rich-text-editor name="" id="lt-track1-desc-create" min-height="80px" placeholder="Tell viewers about this track…" />
|
||||
</div>
|
||||
|
||||
<!-- Slides -->
|
||||
@@ -562,7 +560,10 @@
|
||||
if (cFname) cFname.textContent = 'Choose audio file…';
|
||||
document.getElementById('video-title').value = '';
|
||||
document.getElementById('lt-track1-title-create').value = '';
|
||||
document.getElementById('lt-track1-desc-create').value = '';
|
||||
['video-description','lt-track1-desc-create'].forEach(function(id){
|
||||
const el = document.getElementById(id);
|
||||
if (el) { el.value = ''; el.dispatchEvent(new Event('rte:refresh')); }
|
||||
});
|
||||
setAudioMode(false);
|
||||
}
|
||||
|
||||
@@ -646,15 +647,43 @@
|
||||
}
|
||||
|
||||
function handleCSlidesForTrack(trackId, fileList) {
|
||||
if (!fileList || !fileList.length) return;
|
||||
if (!_cSlidesData[trackId]) _cSlidesData[trackId] = [];
|
||||
for (const f of Array.from(fileList)) {
|
||||
if (_cSlidesData[trackId].length >= 10) break;
|
||||
_cSlidesData[trackId].push(f);
|
||||
}
|
||||
renderCSlidesStrip(trackId);
|
||||
_cSlidesCropStart(fileList, trackId);
|
||||
}
|
||||
|
||||
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
||||
let _cSlidesCropQueue = [];
|
||||
let _cSlidesCropTid = null;
|
||||
|
||||
function _cSlidesCropStart(fileList, trackId) {
|
||||
const imgs = Array.from(fileList || []).filter(f => f.type && f.type.startsWith('image/'));
|
||||
if (!imgs.length) return;
|
||||
_cSlidesCropTid = trackId;
|
||||
_cSlidesCropQueue = imgs;
|
||||
_cSlidesCropLoadNext();
|
||||
}
|
||||
|
||||
function _cSlidesCropLoadNext() {
|
||||
if (!_cSlidesCropQueue.length) { window.closeCropperModal('slides_create_mobile'); return; }
|
||||
const f = _cSlidesCropQueue.shift();
|
||||
if (typeof window.tcPreload_slides_create_mobile === 'function') {
|
||||
window.tcPreload_slides_create_mobile(f);
|
||||
window.openCropperModal_slides_create_mobile();
|
||||
}
|
||||
}
|
||||
|
||||
function cSlidesCropDone(file) {
|
||||
const trackId = _cSlidesCropTid;
|
||||
if (trackId && file) {
|
||||
if (!_cSlidesData[trackId]) _cSlidesData[trackId] = [];
|
||||
if (_cSlidesData[trackId].length < 10) {
|
||||
_cSlidesData[trackId].push(file);
|
||||
renderCSlidesStrip(trackId);
|
||||
}
|
||||
}
|
||||
_cSlidesCropLoadNext();
|
||||
}
|
||||
window.cSlidesCropDone = cSlidesCropDone;
|
||||
|
||||
function renderCSlidesStrip(trackId) {
|
||||
const files = _cSlidesData[trackId] || [];
|
||||
const strip = document.getElementById('slides-strip-' + trackId);
|
||||
@@ -945,9 +974,10 @@
|
||||
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
|
||||
Localized Description <span style="font-weight:400;font-size:12px;color:var(--text-secondary);">optional</span>
|
||||
</label>
|
||||
<textarea name="extra_track_descriptions[]" class="form-textarea"
|
||||
rows="2" style="font-size:13px;min-height:64px;"
|
||||
placeholder="Description in this language…"></textarea>
|
||||
<div class="rte-wrap" data-min-height="80px">
|
||||
<textarea class="rte-source" name="extra_track_descriptions[]" id="lt-desc-ce${n}"
|
||||
placeholder="Description in this language…"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-bottom:12px;">
|
||||
<label class="form-label" style="font-size:13px;margin-bottom:6px;">
|
||||
@@ -1038,4 +1068,14 @@
|
||||
output-width="1280"
|
||||
title="Crop Thumbnail"
|
||||
/>
|
||||
|
||||
<x-image-cropper
|
||||
id="slides_create_mobile"
|
||||
:width="448"
|
||||
:height="252"
|
||||
shape="square"
|
||||
output-width="1280"
|
||||
title="Crop Cover Slide"
|
||||
result-callback="cSlidesCropDone"
|
||||
/>
|
||||
@endsection
|
||||
Reference in new issue
Block a user