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
@@ -168,7 +168,7 @@
|
||||
|
||||
@if ($video->isShareable())
|
||||
<button class="action-btn desktop-action"
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
onclick="shareCurrent('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
<i class="bi bi-share"></i><span>Share</span>
|
||||
</button>
|
||||
@endif
|
||||
@@ -271,7 +271,7 @@
|
||||
|
||||
@if ($video->isShareable())
|
||||
<button class="dropdown-item"
|
||||
onclick="openShareModal('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
onclick="shareCurrent('{{ $video->share_url }}', '{{ addslashes($video->title) }}', '{{ route('videos.recordShare', $video) }}')">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
@endif
|
||||
@@ -514,6 +514,14 @@ if (!window._slideshowDlInit) {
|
||||
document.getElementById('sl-dl-cancel').textContent = 'Close';
|
||||
}
|
||||
|
||||
// Share the version the viewer is on: append ?track={id} so the recipient's player
|
||||
// opens directly on that language (window._ytpTrackId; 0 = primary → plain link).
|
||||
window.shareCurrent = function (baseUrl, title, recordUrl) {
|
||||
var t = window._ytpTrackId || 0;
|
||||
var url = t ? baseUrl + (baseUrl.indexOf('?') === -1 ? '?' : '&') + 'track=' + t : baseUrl;
|
||||
if (typeof window.openShareModal === 'function') window.openShareModal(url, title, recordUrl);
|
||||
};
|
||||
|
||||
window.startSlideshowDownload = function (routeKey) {
|
||||
if (_currentKey && _currentKey !== routeKey) {
|
||||
// Different video — reset
|
||||
@@ -521,6 +529,17 @@ if (!window._slideshowDlInit) {
|
||||
_pollTimer = null;
|
||||
}
|
||||
_currentKey = routeKey;
|
||||
|
||||
// Act on the version the viewer is currently playing: the visualizer toggle AND
|
||||
// the selected language track (window._ytpTrackId; 0 = primary).
|
||||
var vizOn = localStorage.getItem('audioBarsOn') === '1';
|
||||
var trackId = window._ytpTrackId || 0;
|
||||
var _p = [];
|
||||
if (vizOn) _p.push('visualizer=1');
|
||||
if (trackId) _p.push('track=' + trackId);
|
||||
var qs = _p.length ? '?' + _p.join('&') : ''; // generate + download
|
||||
var qsAmp = _p.length ? '&' + _p.join('&') : ''; // appended to progress ?duration=
|
||||
|
||||
_maxPct = 0;
|
||||
document.getElementById('sl-dl-bar').style.background = ''; // restore gradient
|
||||
document.getElementById('sl-dl-cancel').textContent = 'Cancel';
|
||||
@@ -532,7 +551,7 @@ if (!window._slideshowDlInit) {
|
||||
var csrfMeta = document.querySelector('meta[name="csrf-token"]');
|
||||
var token = (typeof csrf !== 'undefined' ? csrf : '') || (csrfMeta ? csrfMeta.getAttribute('content') : '');
|
||||
|
||||
fetch('/videos/' + routeKey + '/slideshow/generate', {
|
||||
fetch('/videos/' + routeKey + '/slideshow/generate' + qs, {
|
||||
method: 'POST',
|
||||
headers: { 'X-CSRF-TOKEN': token, 'Accept': 'application/json' }
|
||||
})
|
||||
@@ -546,7 +565,7 @@ if (!window._slideshowDlInit) {
|
||||
document.getElementById('sl-dl-cancel').style.display = 'none';
|
||||
setTimeout(function () {
|
||||
window._slideshowDlCancel();
|
||||
window.location.href = '/videos/' + routeKey + '/download';
|
||||
window.location.href = '/videos/' + routeKey + '/download' + qs;
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
@@ -559,7 +578,7 @@ if (!window._slideshowDlInit) {
|
||||
_setProgress(2, 'Generating video...');
|
||||
|
||||
_pollTimer = setInterval(function () {
|
||||
fetch('/videos/' + routeKey + '/slideshow/progress?duration=' + duration, {
|
||||
fetch('/videos/' + routeKey + '/slideshow/progress?duration=' + duration + qsAmp, {
|
||||
headers: { 'Accept': 'application/json' }
|
||||
})
|
||||
.then(function (r) { return r.json(); })
|
||||
@@ -581,7 +600,7 @@ if (!window._slideshowDlInit) {
|
||||
document.getElementById('sl-dl-cancel').style.display = 'none';
|
||||
setTimeout(function () {
|
||||
window._slideshowDlCancel();
|
||||
window.location.href = '/videos/' + routeKey + '/download';
|
||||
window.location.href = '/videos/' + routeKey + '/download' + qs;
|
||||
}, 600);
|
||||
return;
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user