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:
ghassanandClaude Opus 4.7 committed 2026-05-31 22:01:47 +03:00
1 parent 73527f3781
commit f98e5415a3
60 files changed
+7920 -2675

No files matched your search

@@ -134,6 +134,12 @@
<svg viewBox="0 0 24 24"><path d="M19.14 12.94c.04-.3.06-.61.06-.94s-.02-.64-.07-.94l2.03-1.58c.18-.14.23-.41.12-.61l-1.92-3.32c-.12-.22-.37-.29-.59-.22l-2.39.96c-.5-.38-1.03-.7-1.62-.94l-.36-2.54c-.04-.24-.24-.41-.48-.41h-3.84c-.24 0-.43.17-.47.41l-.36 2.54c-.59.24-1.13.57-1.62.94l-2.39-.96c-.22-.08-.47 0-.59.22L2.74 8.87c-.12.21-.08.47.12.61l2.03 1.58c-.05.3-.09.63-.09.94s.02.64.07.94l-2.03 1.58c-.18.14-.23.41-.12.61l1.92 3.32c.12.22.37.29.59.22l2.39-.96c.5.38 1.03.7 1.62.94l.36 2.54c.05.24.24.41.48.41h3.84c.24 0 .44-.17.47-.41l.36-2.54c.59-.24 1.13-.56 1.62-.94l2.39.96c.22.08.47 0 .59-.22l1.92-3.32c.12-.22.07-.47-.12-.61l-2.01-1.58zM12 15.6c-1.98 0-3.6-1.62-3.6-3.6s1.62-3.6 3.6-3.6 3.6 1.62 3.6 3.6-1.62 3.6-3.6 3.6z"/></svg>
</button>
<div class="ytp-settings-panel" id="ytpSettingsPanel">
{{-- Mini player toggle — desktop-only, persisted in localStorage --}}
<div class="ytp-settings-item" id="ytpMiniToggleRow" onclick="(function(el){var on=!(window._ytpMiniEnabled&&window._ytpMiniEnabled());window._ytpMiniSetEnabled(on);el.querySelector('.ytp-settings-val').textContent=on?'On':'Off';})(this)">
<svg viewBox="0 0 24 24"><path d="M19 11h-8v6h8v-6zm4 8V4.98C23 3.88 22.1 3 21 3H3c-1.1 0-2 .88-2 1.98V19c0 1.1.9 2 2 2h18c1.1 0 2-.9 2-2zm-2 .02H3V4.97h18v14.05z"/></svg>
<span>Mini player</span>
<span class="ytp-settings-val">On</span>
</div>
<div class="ytp-settings-item" id="ytpSpeedRow">
<svg viewBox="0 0 24 24"><path d="M10 8v8l6-4-6-4zm6.5 4A6.5 6.5 0 1 1 9 6.04V4.02A8.5 8.5 0 1 0 18.5 12H16.5z"/></svg>
<span>Playback speed</span>
@@ -818,6 +824,19 @@ video.addEventListener('pause', function () { window._ytpWasPlaying = false; });
function initSource() {
video.muted = true;
video.autoplay = true;
/* Resume handoff from the mini player: ?t=<sec> seeks the video to that
position once metadata is ready. One-shot — only the initial load. */
try {
var _qs = new URLSearchParams(location.search);
var _t = parseInt(_qs.get('t') || '0', 10);
if (_t > 0) {
video.addEventListener('loadedmetadata', function () {
if (_t < (video.duration || Infinity)) {
try { video.currentTime = _t; } catch (e) {}
}
}, { once: true });
}
} catch (e) {}
if (HLS_URL && window.Hls && Hls.isSupported()) {
window._ytpHls = new Hls({ startLevel: -1 });
// Register MANIFEST_PARSED before loadSource to avoid cache race condition
@@ -1077,6 +1096,17 @@ settingsBtn.addEventListener('click', e => {
e.stopPropagation();
const open = settingsPanel.classList.toggle('open');
if (!open) { speedPanel.classList.remove('open'); speedRow.style.display = ''; }
/* Sync the mini-player toggle row's label to the current preference each
time the gear opens, so reloading the page or toggling from the music
player keeps the indicator honest. */
if (open) {
const miniRow = document.getElementById('ytpMiniToggleRow');
if (miniRow) {
const v = miniRow.querySelector('.ytp-settings-val');
const on = !window._ytpMiniEnabled || window._ytpMiniEnabled();
if (v) v.textContent = on ? 'On' : 'Off';
}
}
showControls();
clearTimeout(hideTimer); // keep controls visible while settings open
});
@@ -1370,10 +1400,11 @@ function init() {
largePlay.classList.add('visible');
showControls();
// Scroll-based mini player: watch when #ytpWrap leaves the viewport
if (window.IntersectionObserver && window._miniPlayer) {
/* On mobile, #main is the scroll container; on desktop the window scrolls */
var _scrollRoot = window.innerWidth <= 768 ? document.getElementById('main') : null;
// Scroll-based mini player: watch when #ytpWrap leaves the viewport.
// Desktop-only — on mobile the fixed bottom-nav + locked scroll model
// make a floating overlay disruptive.
if (window.IntersectionObserver && window._miniPlayer && window.innerWidth > 768) {
var _scrollRoot = null; /* desktop: window scrolls */
var _scrollMiniOn = false; /* guard against rapid toggling / initial fire */
new IntersectionObserver(function (entries) {
var e0 = entries[0];
@@ -1381,7 +1412,8 @@ function init() {
Using !video.paused was unreliable: autoplay fires asynchronously and the
initial IntersectionObserver callback could run before HLS.js even attaches,
teleporting the element before it ever played in the main player. */
if (!e0.isIntersecting && !_scrollMiniOn && window._ytpWasPlaying && !window._miniPlayer.isNavMode()) {
var miniAllowed = !window._ytpMiniEnabled || window._ytpMiniEnabled();
if (!e0.isIntersecting && !_scrollMiniOn && window._ytpWasPlaying && miniAllowed && !window._miniPlayer.isNavMode()) {
_scrollMiniOn = true;
window._miniPlayer.activateScroll(
document.title.replace(/\s*\|.*$/, '').trim(),
@@ -1392,6 +1424,12 @@ function init() {
window._miniPlayer.deactivateScroll();
}
}, { root: _scrollRoot, threshold: 0.15 }).observe(wrap);
/* User clicked the X on the mini while still on the video page —
reset the flag so a subsequent scroll-away re-activates it. */
window.addEventListener('miniplayer:scroll-closed', function () {
_scrollMiniOn = false;
});
}
}