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
File diff suppressed because it is too large.
Load diff
File diff suppressed because it is too large.
Load diff
@@ -0,0 +1,150 @@
|
||||
{{-- ===========================================================
|
||||
Channel page — MOBILE overrides
|
||||
This file owns every mobile/touch-screen rule. All selectors
|
||||
are wrapped in @media (max-width: 768px) or smaller so they
|
||||
cannot affect desktop. Editing here is safe.
|
||||
-----------------------------------------------------------
|
||||
• Edit here for: phone layout, tap-target sizing, mobile
|
||||
hero, mobile tabs, anything that should only apply on
|
||||
small viewports.
|
||||
• Add new mobile rules INSIDE one of the @media blocks below.
|
||||
=========================================================== --}}
|
||||
<style>
|
||||
/* ══════════════════════════════════════════════════
|
||||
MOBILE
|
||||
══════════════════════════════════════════════════ */
|
||||
/* ══════════════════════════════════════════════════
|
||||
MOBILE — full redesign of the channel header
|
||||
Strategy: avatar floats over the banner, then name +
|
||||
meta + actions stack vertically below in a single
|
||||
column so nothing has to compete for width.
|
||||
══════════════════════════════════════════════════ */
|
||||
@media (max-width: 768px) {
|
||||
.ch-banner { height: 130px; }
|
||||
.ch-header { padding: 0 14px 18px; }
|
||||
|
||||
/* Stack avatar + info instead of inline */
|
||||
.ch-header-inner {
|
||||
display: block; gap: 0;
|
||||
margin-top: -42px;
|
||||
}
|
||||
|
||||
/* Centred floating avatar at top-left, overlapping the banner */
|
||||
.ch-avatar-wrap { display: inline-block; margin-bottom: 12px; }
|
||||
.ch-avatar {
|
||||
width: 84px; height: 84px; border-radius: 14px;
|
||||
box-shadow: 0 0 0 3px hsl({{ $hue }}, 55%, 42%),
|
||||
0 8px 24px rgba(0,0,0,.6),
|
||||
0 0 0 7px rgba(0,0,0,.45);
|
||||
}
|
||||
.ch-avatar-edit-btn { border-radius: 14px; font-size: 18px; opacity: .85; }
|
||||
|
||||
/* Hide hover-only banner "Change banner" text — keep camera icon */
|
||||
.ch-banner-edit-btn {
|
||||
opacity: 1; padding: 0; width: 34px; height: 34px;
|
||||
justify-content: center; bottom: 10px; right: 10px;
|
||||
background: rgba(0,0,0,.55);
|
||||
}
|
||||
.ch-banner-edit-btn span { display: none; }
|
||||
.ch-banner-edit-btn i { font-size: 14px; }
|
||||
|
||||
/* Info column takes full width */
|
||||
.ch-info { padding-top: 0; width: 100%; }
|
||||
|
||||
/* Name big and tight */
|
||||
.ch-name-row { gap: 8px; margin-bottom: 6px; }
|
||||
.ch-name { font-size: 22px; line-height: 1.1; letter-spacing: -.3px; }
|
||||
.ch-verified-badge { width: 18px; height: 18px; font-size: 10px; }
|
||||
|
||||
/* Meta pill — denser, horizontally scrolls if it would wrap awkwardly */
|
||||
.ch-meta-row {
|
||||
display: flex; flex-wrap: nowrap; overflow-x: auto;
|
||||
scrollbar-width: none;
|
||||
font-size: 12px; padding: 6px 12px;
|
||||
margin-bottom: 10px;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
}
|
||||
.ch-meta-row::-webkit-scrollbar { display: none; }
|
||||
.ch-meta-sep { margin: 0 8px; font-size: 11px; }
|
||||
.ch-meta-item { font-size: 12px; }
|
||||
.ch-meta-item i { font-size: 10px; }
|
||||
|
||||
/* Bio readable on small screens */
|
||||
.ch-bio { font-size: 13px; line-height: 1.55; margin-bottom: 12px; }
|
||||
.ch-bio-short { -webkit-line-clamp: 3; }
|
||||
|
||||
/* Horoscope strip = full-width row, action row beneath */
|
||||
.ch-horo-strip { padding: 7px 11px; gap: 4px 10px; width: 100%; box-sizing: border-box; }
|
||||
.ch-horo-name { font-size: 13px; }
|
||||
.ch-horo-emoji { font-size: 20px; }
|
||||
|
||||
/* Owner action row (horoscope-wrap parent flex) wraps on mobile */
|
||||
.ch-info > div[style*="display:flex"] { row-gap: 8px; }
|
||||
|
||||
/* Manage/Upload/Preview row: full-width, evenly distributed */
|
||||
.ch-manage-wrap, .ch-btn-ghost, .ch-btn-icon { flex: 0 0 auto; }
|
||||
.ch-manage-btn, .ch-btn-ghost {
|
||||
height: 38px; padding: 0 13px; font-size: 13px; gap: 6px;
|
||||
}
|
||||
.ch-manage-btn span, .ch-btn-ghost span { display: inline; } /* keep labels readable */
|
||||
.ch-btn-icon {
|
||||
width: 38px; height: 38px;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
border-radius: 8px;
|
||||
background: rgba(255,255,255,.07);
|
||||
border: 1px solid rgba(255,255,255,.14);
|
||||
color: var(--text-primary); text-decoration: none;
|
||||
}
|
||||
|
||||
/* Social row */
|
||||
.ch-social-row { gap: 6px; margin-bottom: 12px; }
|
||||
.ch-social-btn { width: 38px; height: 38px; font-size: 14px; }
|
||||
|
||||
/* sticky doesn't work inside .yt-main on mobile — use fixed instead */
|
||||
.ch-tabs-wrap {
|
||||
position: fixed !important;
|
||||
top: 56px;
|
||||
left: 0; right: 0;
|
||||
z-index: 80;
|
||||
padding: 0 12px;
|
||||
background: var(--bg-dark);
|
||||
border-bottom: 1px solid rgba(255,255,255,.07);
|
||||
}
|
||||
/* push tab content below the fixed tabs bar (46px height + 1px border) */
|
||||
.ch-tabs-spacer { display: block; height: 47px; }
|
||||
.ch-tab { padding: 0 14px; font-size: 12px; min-width: 0; }
|
||||
.ch-tab i { font-size: 13px; margin-right: 4px; }
|
||||
.ch-tab-badge { font-size: 10px; padding: 1px 5px; }
|
||||
.ch-tab-content { padding: 14px; }
|
||||
.ch-sort-bar { gap: 8px; flex-wrap: wrap; }
|
||||
.ch-about-card { padding: 16px; }
|
||||
.ch-stat-cards { grid-template-columns: 1fr 1fr; gap: 10px; }
|
||||
.ch-stat-card { padding: 14px 12px; }
|
||||
.ch-stat-card-val { font-size: 22px; }
|
||||
.ch-wall-layout { grid-template-columns: 1fr; gap: 16px; }
|
||||
.ch-vid-search-wrap { max-width: 100%; }
|
||||
|
||||
/* Manage dropdown — make taps comfortable on phones */
|
||||
.ch-manage-menu { min-width: 240px; padding: 8px; }
|
||||
.ch-manage-item { padding: 12px 14px; font-size: 14px; }
|
||||
}
|
||||
|
||||
@media (max-width: 480px) {
|
||||
.ch-banner { height: 110px; }
|
||||
.ch-header { padding: 0 12px 16px; }
|
||||
.ch-header-inner { margin-top: -36px; }
|
||||
.ch-avatar { width: 72px; height: 72px; border-radius: 12px; }
|
||||
.ch-name { font-size: 20px; }
|
||||
.ch-horo-divider { display: none; }
|
||||
|
||||
/* Stack the owner action row 2-up so labels stay visible */
|
||||
.ch-info > div[style*="display:flex"][style*="flex-wrap:wrap"] { gap: 8px; }
|
||||
|
||||
.ch-meta-row { font-size: 11.5px; padding: 5px 11px; }
|
||||
.ch-meta-sep { margin: 0 7px; }
|
||||
|
||||
.ch-tab { padding: 0 12px; font-size: 11.5px; }
|
||||
|
||||
.ch-stat-cards { grid-template-columns: 1fr 1fr; }
|
||||
}
|
||||
</style>
|
||||
Reference in new issue
Block a user