Add lyrics pipeline, playlist views, admin toggles, and player polish
Lyrics pipeline (Whisper + Demucs + description alignment):
- New GenerateLyricsJob runs WhisperX with VAD filtering and forced word
alignment, writes per-track JSON to NAS.
- New DecorateLyricsJob calls the active LLM provider to bake one to
several emojis into each line (heavy decoration prompt).
- LyricsDescriptionParser strips heading content, section markers, and
emoji-decoration from a song's description while preserving every
language verbatim.
- correct_whisper_with_description aligner: strong-match anchors only,
vocal-region-aware gap-fill so missing verses land on actual singing.
- Owner UI for generate/regenerate/edit/delete in the player gear.
Admin pages:
- /admin/lyrics toggles for VAD, vocal gap-fill, Demucs, master
- /admin/gpu extracted GPU section, encoder picker, FFmpeg path
- /admin/backup extracted users-and-settings export/import
- /admin/settings now AI/LLM only with provider list and Test button
- /admin/nas-storage hosts NAS settings, repair, disable flow, browser
- Shared partials/settings-styles for a uniform look across pages.
Playlist view tracking:
- Migration adds playlists.view_count and playlist_views dedup table.
- Playlist::bumpViewIfNew increments per device with a one-hour window.
- Tracked from /playlists/{id}, /playlists/share/{token}, /ps/{token},
and /videos/{id}?playlist={token}. Dispatched after-response so it
never blocks the page render.
- Loading a playlist on the video page now runs one query instead of
the four the old getNextVideo/getPreviousVideo path triggered.
- View counts shown on every playlist card and the playlist hero.
Player polish:
- Floating mini-player is draggable, persists its position in
localStorage, clamps to viewport on resize.
- Mini disabled entirely on mobile (less than 768px).
- New gear-menu Mini Player toggle (persists in localStorage) lets the
user disable both scroll-activation and SPA-nav-activation.
- Close button keeps media playing when used on the player's own page.
- SPA navigator now swaps a #page-scripts container so per-page JS
(channel tabs, etc.) gets re-executed after content swaps.
Storage layout:
- Runtime data moved from /storage/* to /data/* and gitignored.
- /ml/venv, /ml/cache, /ml/__pycache__ excluded.
Co-Authored-By: Claude Opus 4.7 <[email protected]>
This commit is contained in:
1 parent
73527f3781
commit
f98e5415a3
60 files changed
+7920
-2675
No files matched your search
@@ -6,6 +6,8 @@
|
||||
$plUrl = $firstVid
|
||||
? route('videos.show', $firstVid) . '?playlist=' . $pl->share_token
|
||||
: route('playlists.show', $pl->id);
|
||||
$plIsOwner = auth()->check() && auth()->id() === $pl->user_id;
|
||||
$plShuffleUrl = $firstVid ? route('playlists.shuffle', $pl->id) : null;
|
||||
@endphp
|
||||
|
||||
@once
|
||||
@@ -115,5 +117,84 @@
|
||||
{{ number_format($pl->view_count) }} {{ Str::plural('view', $pl->view_count) }} · {{ $pl->created_at->diffForHumans() }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="position-relative">
|
||||
<button class="yt-more-btn" type="button" data-bs-toggle="dropdown" data-bs-auto-close="true" aria-expanded="false" aria-label="More options">
|
||||
<i class="bi bi-three-dots-vertical"></i>
|
||||
</button>
|
||||
<ul class="dropdown-menu dropdown-menu-end dropdown-menu-dark">
|
||||
@if($firstVid)
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ $plUrl }}">
|
||||
<i class="bi bi-play-fill"></i> Play all
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ $plShuffleUrl }}">
|
||||
<i class="bi bi-shuffle"></i> Shuffle
|
||||
</a>
|
||||
</li>
|
||||
@endif
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('playlists.show', $pl->id) }}">
|
||||
<i class="bi bi-collection-play"></i> View playlist
|
||||
</a>
|
||||
</li>
|
||||
@if($pl->visibility !== 'private' || $plIsOwner)
|
||||
<li>
|
||||
<button type="button" class="dropdown-item"
|
||||
onclick="openShareModal({{ json_encode($pl->share_url) }}, {{ json_encode($pl->name) }}, {{ json_encode(route('playlists.recordShare', $pl->id)) }})">
|
||||
<i class="bi bi-share"></i> Share
|
||||
</button>
|
||||
</li>
|
||||
@endif
|
||||
@if($plIsOwner)
|
||||
<li><hr class="dropdown-divider"></li>
|
||||
<li>
|
||||
<a class="dropdown-item" href="{{ route('playlists.show', $pl->id) }}#edit">
|
||||
<i class="bi bi-pencil"></i> Edit
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<button type="button" class="dropdown-item text-danger"
|
||||
onclick="plCardDelete({{ $pl->id }}, {{ json_encode($pl->name) }}, this)">
|
||||
<i class="bi bi-trash"></i> Delete
|
||||
</button>
|
||||
</li>
|
||||
@endif
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@once
|
||||
<script>
|
||||
function plCardDelete(plId, plName, btnEl) {
|
||||
var done = function () {
|
||||
fetch('/playlists/' + plId, {
|
||||
method: 'DELETE',
|
||||
headers: {
|
||||
'X-CSRF-TOKEN': '{{ csrf_token() }}',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'XMLHttpRequest'
|
||||
}
|
||||
}).then(function (r) { return r.json(); }).then(function (d) {
|
||||
if (d && d.success) {
|
||||
if (typeof showToast === 'function') showToast('Playlist deleted', 'success');
|
||||
var card = btnEl && btnEl.closest('.yt-video-card');
|
||||
if (card) card.remove();
|
||||
else window.location.reload();
|
||||
} else {
|
||||
if (typeof showToast === 'function') showToast((d && d.message) || 'Failed to delete', 'error');
|
||||
}
|
||||
}).catch(function () {
|
||||
if (typeof showToast === 'function') showToast('Failed to delete playlist', 'error');
|
||||
});
|
||||
};
|
||||
if (typeof showConfirm === 'function') {
|
||||
showConfirm('Delete "' + plName + '"?', done, 'Delete');
|
||||
} else {
|
||||
done();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
@endonce
|
||||
Reference in new issue
Block a user