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
@@ -198,8 +198,7 @@
|
||||
{{-- Description --}}
|
||||
<div class="form-group">
|
||||
<label class="form-label">Description</label>
|
||||
<textarea name="description" id="edit-description" rows="4" class="form-textarea"
|
||||
placeholder="Tell viewers about your video (Markdown supported)">{{ $video->description }}</textarea>
|
||||
<x-rich-text-editor name="description" id="edit-description" :value="$video->description" placeholder="Tell viewers about your video…" />
|
||||
</div>
|
||||
|
||||
@if(!$video->isAudioOnly())
|
||||
@@ -279,9 +278,11 @@
|
||||
value="{{ $track->title ?? '' }}"
|
||||
placeholder="Title in this language…"
|
||||
style="background:#121212;border:1px solid #333;border-radius:6px;padding:7px 10px;font-size:13px;color:#e5e5e5;outline:none;width:100%;box-sizing:border-box;margin-bottom:6px;">
|
||||
<textarea name="track_description_updates[{{ $track->id }}]"
|
||||
placeholder="Description in this language…" rows="2"
|
||||
style="background:#121212;border:1px solid #333;border-radius:6px;padding:7px 10px;font-size:13px;color:#e5e5e5;outline:none;width:100%;box-sizing:border-box;resize:none;margin-bottom:6px;">{{ $track->description ?? '' }}</textarea>
|
||||
<x-rich-text-editor :name="'track_description_updates[' . $track->id . ']'"
|
||||
:id="'ep-track-desc-' . $track->id"
|
||||
:value="$track->description ?? ''"
|
||||
min-height="72px"
|
||||
placeholder="Description in this language…" />
|
||||
<label style="display:flex;align-items:center;gap:8px;background:#0d0d0d;border:1px solid #222;border-radius:6px;padding:8px 10px;cursor:pointer;"
|
||||
onmouseenter="this.style.borderColor='#e61e1e'" onmouseleave="this.style.borderColor='#222'">
|
||||
<i class="bi bi-music-note-beamed" style="color:#e61e1e;font-size:14px;flex-shrink:0;"></i>
|
||||
@@ -542,16 +543,42 @@
|
||||
}
|
||||
|
||||
document.getElementById('ep-slides-add-input').addEventListener('change', function() {
|
||||
Array.from(this.files).forEach(file => {
|
||||
epSlidesCropStart(this.files);
|
||||
this.value = '';
|
||||
});
|
||||
|
||||
// ── Slides crop queue: every added image is cropped before it enters the strip ──
|
||||
let _epSlidesCropQueue = [];
|
||||
|
||||
function epSlidesCropStart(fileList) {
|
||||
const imgs = Array.from(fileList || []).filter(f => f.type && f.type.startsWith('image/'));
|
||||
if (!imgs.length) return;
|
||||
_epSlidesCropQueue = imgs;
|
||||
_epSlidesCropLoadNext();
|
||||
}
|
||||
|
||||
function _epSlidesCropLoadNext() {
|
||||
if (!_epSlidesCropQueue.length) { window.closeCropperModal('slides_edit_mobile'); return; }
|
||||
const f = _epSlidesCropQueue.shift();
|
||||
if (typeof window.tcPreload_slides_edit_mobile === 'function') {
|
||||
window.tcPreload_slides_edit_mobile(f);
|
||||
window.openCropperModal_slides_edit_mobile();
|
||||
}
|
||||
}
|
||||
|
||||
window.epSlidesCropDone = function(file) {
|
||||
if (file) {
|
||||
const reader = new FileReader();
|
||||
reader.onload = e => {
|
||||
epSlidesData.push({ file, url: e.target.result });
|
||||
epSlidesRefresh();
|
||||
_epSlidesCropLoadNext();
|
||||
};
|
||||
reader.readAsDataURL(file);
|
||||
});
|
||||
this.value = '';
|
||||
});
|
||||
return;
|
||||
}
|
||||
_epSlidesCropLoadNext();
|
||||
};
|
||||
|
||||
// Initialise strip with existing slides
|
||||
epSlidesRefresh();
|
||||
@@ -785,4 +812,14 @@
|
||||
output-width="1280"
|
||||
title="Crop Thumbnail"
|
||||
/>
|
||||
|
||||
<x-image-cropper
|
||||
id="slides_edit_mobile"
|
||||
:width="448"
|
||||
:height="252"
|
||||
shape="square"
|
||||
output-width="1280"
|
||||
title="Crop Cover Slide"
|
||||
result-callback="epSlidesCropDone"
|
||||
/>
|
||||
@endsection
|
||||
Reference in new issue
Block a user