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:
ghassanandClaude Opus 4.7 committed 2026-05-23 14:03:43 +03:00
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;
}