Replaces the point Add/Edit popup with an in-player capture strip and fixes
the underlying running-score bug.
Backend (MatchEventController)
- storePoint used pluck('points','competitor'), which collapses same-side
points into a single row (last value wins) and excluded same-timestamp
points (< instead of <=). Same-moment Blue+Red produced 1-0 / 0-1 instead
of 1-1. Replaced with recomputeRoundScores($roundId), a single pass over
the round ordered by (timestamp_seconds, id) that walks a running total
and updates each row. Called after create/update/delete so edits ripple.
Player-embedded capture strip (match.blade.php)
- Adding / editing points no longer opens a modal; the video's chrome
bottom-bar is swapped for a strip inside #ytpWrap that survives fullscreen.
- Editable mm:ss field + draggable red marker + −1s/+1s nudges. Zoom control
(1x / 5x / 20x) narrows the slider window around the current time for
frame-level positioning; nudges scale with zoom. Auto-recentre on edge.
- Inline form: Blue / Red / Both toggle (equal-width segments), action text,
points number. Both mode splits into two per-side rows. Values carry over
when switching modes so nothing is retyped.
- Enter saves, Esc cancels. Save button auto-disables during in-flight
requests. On mobile the buttons collapse to icon-only (× / ✓).
Grouped highlights entry
- Points sharing a timestamp within a round collapse into ONE card with a
chip per side (equal-width Blue/Red pills), a single ✏️/🗑 pair, and a
meta line coloured by role: ROUND N in amber, blue score in blue, red
score in red.
- ✏️ on a grouped entry opens the strip in Both mode with both sides
pre-filled. Saving PUTs both rows; switching to a single competitor
keeps that side and deletes the other.
- 🗑 on a grouped entry deletes both rows via the shared custom-confirm.
- loadMatchData now writes back to window.matchRounds so subsequent
edit/lookup handlers find newly-added points without a page reload.
Removed
- #pointModal HTML block and the openAddPointModal / savePoint /
confirmDeletePoint helpers (superseded by the capture strip).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Match highlights sidebar (videos/types/match.blade.php)
- Replaced hardcoded 3-round/5-point/3-review mock with server-rendered loops
over $video->matchRounds (with points) and $video->coachReviews. Owner-only
buttons carry real DB ids to the existing JS handlers.
- Fixed savePoint create branch: was doing parseInt(rawMmSs) which truncated
"1.15" -> 1 second. Now sends the parsed value that edit already used.
- Added a point-capture bar: clicking + on a round pauses the video and shows a
bottom bar with a big draggable red marker + nudge buttons. Dragging seeks
the video live; Confirm reads video.currentTime and opens the Add Point modal
with mm.ss pre-filled and Action focused. Fixed both server- and JS-rendered
round buttons so dynamic reloads keep the new flow.
Edit → sports sheet routing
- Added sports_match_id to /videos/{video}/edit JSON.
- openEditVideoModal delegates type=match videos to openSportsMatchModal —
edit mode when a SportsMatch is linked, create-with-attached-video mode
when the video is match-typed but has no SportsMatch row yet.
- Sports sheet gained attachExistingVideo() so create-mode can be seeded with
a video that's already in the library (skips the upload dropzone).
Upload chooser mobile bottom-nav
- Bottom nav "Upload" now opens the type chooser (bottom sheet on mobile,
centred modal on desktop) instead of hard-navigating to /videos/create.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- CompressVideoJob: rewrite to exec ffmpeg directly. php-ffmpeg's X264
format class validates the codec name and only accepts libx264, so
every job dispatched with h264_nvenc had been throwing within ~1s and
leaving the source untouched. The job now honours the GPU encoder
settings, and always replaces the source when the codec is HEVC/H.265
so downloads and the MP4 fallback stay playable.
- VideoController + video-player component: point HLS URLs at
playlist.m3u8 (the actual master file GenerateHlsJob writes) instead
of the non-existent master.m3u8, which was 404-ing and forcing the
player back to the raw MP4.
- VideoController@store: on the NAS-success branch, flip status to
'ready' and populate duration/is_shorts before dispatching
GenerateHlsJob, so the job no longer silently no-ops.
- Upload chooser: render as a bottom sheet on mobile, and forward the
picked type to /videos/create?type= so the create page pre-selects it.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
VideoController@update() now handles a replacement media file: if the
new file is audio the row is forced to type=music and, when the on-disk
type folder differs from the declared type (e.g. a video sitting under
users/{u}/videos/ that was later marked music), the media is relocated
into the canonical users/{u}/music/{slug}/tracks/{lang}-{id}/audio.ext
layout. Old primary file and stale meta.json are deleted from NAS; HLS
and slideshow caches are reset. The same relocation is applied by
replaceFile() so the mobile "Replace Media File" flow behaves the same.
Slide- and audio-track-management gates in update() also switched from
isAudioOnlyFile() (extension-based) to the declared type, so secondary
tracks and cover slides added mid-conversion are no longer silently
dropped. The edit modal now renders existing secondary tracks whenever
type=music regardless of the primary file extension.
Also carries an unrelated app.blade.php tweak: the mobile filter bar's
sticky reset moved from .yt-filter-bar to its wrapper, with padding
kept on the inner bar.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Removes the 12-chip cap on the language filter so every language that
appears in public content is listed (video 175 alone contributes 12).
The bar wrapper is now sticky and horizontally scrollable with chevron
buttons that appear only when the bar overflows in that direction, a
gradient fade behind each button, vertical-wheel → horizontal scroll
that hands off to the page at the edges, and sessionStorage-persisted
scroll position so navigation doesn't snap it back to the start.
Chip clicks are intercepted (only for /videos URLs; Trending/Shorts
still render different templates via a normal navigation) and swap
only the #filter-content region via fetch + DOMParser, updating
history.pushState, document.title, and active-chip state without a
full page reload. Modifier-clicks and middle-clicks fall through
to native "open in new tab" behaviour.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds one chip per language currently present in public content —
counting both primary language and any secondary audio track, sorted
by video count, capped at 12. Clicking a chip lists every video whose
primary is that language or that has a matching secondary track; cards
surfaced only via a secondary track link straight to /videos/{key}?track=<id>
so the pinned playback wiring loads the right audio, get a small flag
badge on the thumbnail, and their hover-preview streams the track file
instead of the primary.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Bundled pre-existing changes on the branch:
- Video merge: controller, service, admin merge modal, and a repair
console command; new video_redirects table (with source_folder)
so merged/moved videos keep resolving under their old keys.
- Default avatar SVG for users without a profile picture.
- Assorted view tweaks across profile, video actions/comments/insights,
admin layout, video-details, and the app layout.
- Small touch-ups in SuperAdminController, User model, NasSyncService.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Chunked uploads: bypass Cloudflare's 100 MB request cap by slicing the
video/audio into 8 MB chunks and reassembling server-side before the
normal store flow runs.
Track reorder: audio tracks gain a `position` column so a user's edit-
modal ordering survives reload. `Video::audioTracks()` now orders by
position, `promote_track_id` remembers the swapped row, and freshly
uploaded tracks append to the end.
Playlist per-track pinning: `playlist_videos.audio_track_id` remembers
which language the user pinned when adding a song. Same song with a
different track becomes a separate row (Spotify-style duplicates).
Player-data exposes `active_stream_url` + `active_track_id`, sidebar
cards render `?track=` from the pivot, and both `plTransitionTo` and
initial-load code activate the corresponding language option.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
After my previous fix the upload succeeds, but the avatar path is always
.../profile/avatar.<ext>, so its media URL never changes. Pages other than the
channel (which cache-busts with ?t=) kept serving the browser-cached OLD image
— the user "still saw the old avatar" on /videos.
- avatar_url / banner_url accessors append ?v=<updated_at timestamp>.
- updateAvatar / updateBanner now bump updated_at explicitly (the path is
unchanged, so Eloquent wouldn't otherwise touch it), changing ?v so every
page fetches the new image.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The cropper's onAvatarSaved/onBannerSaved callback used the url from
image.upload, which points at the temp buffer file. updateAvatar/updateBanner
then move that temp file to the NAS profile dir and delete the local copy, so
the url is dead the moment it's used — the avatar/banner rendered broken until
the next page load (the file itself was saved fine).
- updateAvatar/updateBanner now return the canonical media route URL
(route('media.avatar'|'media.banner', $relPath)), matching the model's
avatar_url/banner_url accessors and the project's no-asset('storage/...') rule.
- The image-cropper prefers the update endpoint's returned url over the temp
image.upload url; falls back to the old behavior when none is returned.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Player/UI fixes:
- Sports (match) Highlights pane: move the toggle click-handler script out of
the always-false @if(false ...) legacy block so the "Highlights" button opens,
and emit videoId/isOwner with @json instead of {{ json_encode() }} (escaped
quotes crashed the data/tab/CRUD block once route keys became hashid strings).
- Progress scrubber knob (video + audio players): center on the track line with
translate(-50%, -50%); drop the margin-top hack that floated it above the line.
- Music player volume: give the slider wrap height so overflow:hidden no longer
crops the knob; add a subtle thumb shadow for visibility.
- Music language menu: cap height to available space above the header, sticky
header, internal scroll; keep controls from auto-hiding while the menu is open.
- Description box: re-check "Show more" overflow at every settle point (fonts,
images, rAF, tab reopen) so SPA autonext keeps the expand button.
Also bundles pending work: save-to-playlist button component, playlist/channel
views, PlaylistController + NasSyncService, and component-usage tracker updates.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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 <noreply@anthropic.com>
New POST /videos/{video}/share/email route (auth + throttled) handled by
VideoController@shareByEmail, sending the VideoShared mailable rendered from
emails/video-shared.blade.php. Wired into the share modal and video-actions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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) <noreply@anthropic.com>
Introduce per-video language support and multiple audio tracks
(VideoAudioTrack model + migrations for language, description, title),
a reusable language-select component, and a track-editor form. Bundle
the self-hosted flag-icons v7.2.3 library and a NAS auto-sync command.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Users can now control which in-app and email notifications they receive
from their Settings tab on their channel page.
Bell (in-app) preferences:
- New comment on my video (default: on)
- Reply to my comment (default: on)
- Comment liked (default: on)
- Video liked (default: on)
- New subscriber (default: on)
- New video from channels I follow (default: on)
- New post from channels I follow (default: on)
- New user registration — super admins only (default: on)
Email preferences (same set plus):
- Comment liked (default: off — too noisy)
- Video liked (default: off — too noisy)
- New post from channels I follow (default: off)
- My video finished processing (default: on)
- Weekly activity digest every Monday (default: on)
- New user registration — super admins only (default: on)
Implementation:
- Migration: notification_preferences JSON column on users table
- User::notificationPref($key) helper with typed defaults
- All existing notification classes updated to check prefs in via()
- 4 new notification classes: NewSubscriberNotification,
VideoLikedNotification, NewPostNotification, WeeklyDigestNotification
- 8 new email views matching existing dark theme
- SendWeeklyDigest artisan command, scheduled every Monday 09:00
- NewSubscriberNotification wired into UserController::toggleSubscribe
- VideoLikedNotification wired into UserController::toggleLike
- NewPostNotification wired into PostController::store (to all subscribers)
- Bell renderer updated for new_subscriber, video_like, new_post types
- Preferences saved via AJAX (POST /settings/notifications) — instant
toggle with automatic revert on failure
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a new user registers, all super_admin users receive:
- A database notification shown in the bell (type: new_user), with the
new user's avatar and a link to their channel
- An email congratulating them with the new member's name, email,
join date, gender, and nationality, plus a View Profile CTA
Notification rendering in app.blade.php refactored into notifHref() and
notifThumb() helpers so new_user notifications link to /channel/{slug}
and show a circular avatar instead of a video thumbnail. Also fixed the
legacy /storage/thumbnails/ path to /media/thumbnails/ for video notifications.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
VideoController constructor applied auth middleware to all methods
not in the except list. playerData was missing, causing guest
requests to get 401 → SPA fallback to window.location.href → page refresh.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add _ytpLoadSource(hlsUrl, mp4Url) to video-player component:
destroys old HLS instance, creates a new one with the new source,
then plays — browser retains autoplay permission since the <video>
element never leaves the page
- Add _ytpNavOverride hook: playlist overlay can replace navigateNext/
navigatePrev without modifying the component internals
- Add _plOnVideoEnd hook to 'ended' handler so the playlist overlay
can control autoplay/loop behavior independently
- Expose window._ytpHls for HLS instance lifecycle management
- Add hls_url + has_hls to /videos/{video}/player-data JSON endpoint
- Replace generic.blade.php playlist controls with full SPA system
identical in structure to music type: plTransitionTo, plSwapContent,
plAdj, plRender, plHighlight — no page refresh on track change
- Sidebar shows all playlist tracks; current track highlighted in red
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add GET /videos/{video}/player-data JSON endpoint returning stream URL,
cover, slides, title, duration (used by client-side SPA transitions)
- Replace music playlist JS with full SPA system: plTransitionTo() swaps
audio.src in-place (preserving browser autoplay permission), updates
cover art, resets progress bar, then background-fetches the new page
to swap #vdbWrap (description) and #ytcSection (comments) via DOMParser
- plSwapContent() re-runs the YTC comments IIFE after swapping innerHTML
so comments load correctly for the new video
- Prev/next/shuffle/loop/autoplay controls now computed dynamically from
PL_VIDEOS array — buttons stay correct after each SPA transition
- Sidebar shows ALL playlist tracks (removed @if filter); current track
highlighted in red; clicking any card triggers SPA transition
- Browser back/forward handled via popstate + history.pushState
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When NAS storage is enabled, uploaded files go directly to the NAS share
(users/{username}/videos/{title-slug}/) with no permanent local copy kept.
Thumbnails and video are fetched from NAS on demand for streaming/playback.
When NAS is disabled, files are organised into the same directory schema
in local storage.
- VideoController: branch upload flow on NAS enabled/disabled
- NasSyncService: add uploadDirectToNas() for direct NAS writes,
organizeLocalFiles() for local NAS-schema, localVideoDir() resolver,
deleteLocalAssets() for post-sync cleanup
- GenerateHlsJob: download from NAS via ensureLocalCopy() when local
file is absent (NAS-primary mode); clean up temp after HLS generation
- CompressVideoJob: place compressed file alongside original (any dir)
- Video/VideoSlide models: localVideoPath(), localThumbnailPath(),
thumbnailStorageKey(), localPath(), storageKey() helpers for
format-agnostic path resolution (old flat paths + new NAS-schema paths)
- MediaController: serve thumbnails from NAS-mirrored paths with NAS fallback
- SuperAdminController: use model path helpers for file deletion
- NasFreeLocalStorage: scan new users/ tree in addition to legacy flat dirs
- Settings: rename "NAS Storage Sync" tab to "NAS Storage", update description
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When NAS sync is enabled:
- Audio uploads: pushed to NAS via NasSyncVideoJob, local file deleted immediately after
- Video uploads: processed locally (ffprobe, compress, HLS), then at the end of
GenerateHlsJob the final compressed file is re-synced to NAS and the local copy removed
- stream() and download(): if local file is missing, pull from NAS into a local
stream cache (storage/app/nas_cache/videos/) and serve from there with full
byte-range support — so seeking still works over NAS-sourced files
When NAS is disabled:
- Upload, stream, and download all use local storage exclusively (no change)
HLS segments are intentionally kept local: they are small, generated on-demand,
and serving them via per-segment SMB round-trips would hurt playback performance.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Installed p7h/nas-file-manager package via private VCS repo
- Published config/nas-file-manager.php with super_admin middleware restriction
- Added NAS env vars to .env.example
- Created admin/nas-storage page with connection info panel and file browser widget
- Added NAS Storage link to admin sidebar (super_admin only)
- Added SuperAdminController@nasStorage method and admin.nas-storage route
- Includes all accumulated branch changes: profile wall, 2FA, audit logs,
settings panel, country/phone/timezone components, posts, slideshow,
playlist shares, video downloads/shares, comment likes, notifications,
social links, and more
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Added authentication controllers (Login, Register)
- Added UserController for user profile management
- Added VideoController with full CRUD operations
- Added Video model with relationships (user, likes, views)
- Added User model enhancements (avatar, video relationships)
- Added database migrations for video_likes, video_views, user_avatar, video_visibility
- Added CompressVideoJob for video processing
- Added VideoUploaded mail notification
- Added authentication routes
- Updated web routes with video and user routes
- Added layout templates (app, plain, partials)
- Added user views (profile, settings, channel, history, liked)
- Added video views (create, edit, index, show)
- Added email templates