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
@@ -28,9 +28,8 @@ class PlaylistController extends Controller
|
||||
}
|
||||
|
||||
// View a single playlist
|
||||
public function show(Playlist $playlist)
|
||||
public function show(Request $request, Playlist $playlist)
|
||||
{
|
||||
// Check if user can view this playlist
|
||||
if (! $playlist->canView(Auth::user())) {
|
||||
abort(404, 'Playlist not found');
|
||||
}
|
||||
@@ -38,11 +37,17 @@ class PlaylistController extends Controller
|
||||
$playlist->loadMissing('user');
|
||||
$videos = $playlist->videos()->with('user')->orderBy('position')->get();
|
||||
|
||||
// Count this visit (deduped per device) after the response is sent so
|
||||
// the page render isn't blocked by the EXISTS / INSERT / UPDATE round-trip.
|
||||
dispatch(function () use ($playlist, $request) {
|
||||
$playlist->bumpViewIfNew($request);
|
||||
})->afterResponse();
|
||||
|
||||
return view('playlists.show', compact('playlist', 'videos'));
|
||||
}
|
||||
|
||||
// View playlist via unguessable share token (unlisted playlists)
|
||||
public function showByToken(string $token)
|
||||
public function showByToken(Request $request, string $token)
|
||||
{
|
||||
$playlist = Playlist::where('share_token', $token)->firstOrFail();
|
||||
|
||||
@@ -53,6 +58,10 @@ class PlaylistController extends Controller
|
||||
$playlist->loadMissing('user');
|
||||
$videos = $playlist->videos()->with('user')->orderBy('position')->get();
|
||||
|
||||
dispatch(function () use ($playlist, $request) {
|
||||
$playlist->bumpViewIfNew($request);
|
||||
})->afterResponse();
|
||||
|
||||
return view('playlists.show', compact('playlist', 'videos'));
|
||||
}
|
||||
|
||||
@@ -137,6 +146,11 @@ class PlaylistController extends Controller
|
||||
->withCookie(cookie('_did', $did, 60 * 24 * 365 * 5));
|
||||
}
|
||||
|
||||
// Human share-link click counts as a playlist view (deduped per device).
|
||||
dispatch(function () use ($playlist, $request) {
|
||||
$playlist->bumpViewIfNew($request);
|
||||
})->afterResponse();
|
||||
|
||||
$firstVideo = $playlist->videos()->orderBy('position')->first();
|
||||
$destination = $firstVideo
|
||||
? route('videos.show', $firstVideo) . '?playlist=' . $playlist->share_token
|
||||
@@ -336,6 +350,18 @@ class PlaylistController extends Controller
|
||||
return back()->with('success', $added ? 'Video added to playlist!' : 'Video is already in playlist.');
|
||||
}
|
||||
|
||||
/**
|
||||
* Body-based mirror of removeVideo: looks up the Video by raw numeric id
|
||||
* from the request body. Lets callers (e.g. the add-to-playlist modal) drive
|
||||
* the toggle without having to know the encoded route key for Video.
|
||||
*/
|
||||
public function removeVideoByBody(Request $request, Playlist $playlist)
|
||||
{
|
||||
$request->validate(['video_id' => 'required|exists:videos,id']);
|
||||
$video = Video::findOrFail($request->video_id);
|
||||
return $this->removeVideo($request, $playlist, $video);
|
||||
}
|
||||
|
||||
// Remove video from playlist
|
||||
public function removeVideo(Request $request, Playlist $playlist, Video $video)
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user