Chunked video upload, persistent track order, per-track playlist entries

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) <[email protected]>
This commit is contained in:
ghassanandClaude Opus 4.7 committed 2026-07-31 02:42:25 +03:00
1 parent af276f2cd1
commit ba8ab11ce4
12 files changed
+547 -49

No files matched your search

+43 -4
View File
@@ -323,7 +323,8 @@ class PlaylistController extends Controller
}
$request->validate([
'video_id' => 'required|exists:videos,id',
'video_id' => 'required|exists:videos,id',
'audio_track_id' => 'nullable|integer|exists:video_audio_tracks,id',
]);
$video = Video::findOrFail($request->video_id);
@@ -333,7 +334,13 @@ class PlaylistController extends Controller
abort(403, 'You cannot add this video to your playlist.');
}
$added = $playlist->addVideo($video);
// Track must actually belong to this video (avoid arbitrary FK pinning).
$audioTrackId = $request->input('audio_track_id');
if ($audioTrackId && ! $video->audioTracks()->whereKey($audioTrackId)->exists()) {
$audioTrackId = null;
}
$added = $playlist->addVideo($video, $audioTrackId ? (int) $audioTrackId : null);
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
@@ -353,9 +360,30 @@ class PlaylistController extends Controller
*/
public function removeVideoByBody(Request $request, Playlist $playlist)
{
$request->validate(['video_id' => 'required|exists:videos,id']);
$request->validate([
'video_id' => 'required|exists:videos,id',
'audio_track_id' => 'nullable|integer',
]);
if (! $playlist->canEdit(Auth::user())) {
abort(403, 'You do not have permission to edit this playlist.');
}
$video = Video::findOrFail($request->video_id);
return $this->removeVideo($request, $playlist, $video);
$trackId = $request->filled('audio_track_id')
? (int) $request->input('audio_track_id')
: null;
$removed = $playlist->removeVideoWithTrack($video, $trackId);
if ($request->expectsJson() || $request->ajax()) {
return response()->json([
'success' => true,
'message' => $removed ? 'Video removed from playlist.' : 'Video was not in the playlist.',
'video_count' => $playlist->video_count,
]);
}
return back()->with('success', 'Video removed from playlist.');
}
// Remove video from playlist
@@ -431,6 +459,17 @@ class PlaylistController extends Controller
'visibility' => $p->visibility,
'thumbnail_url' => $p->thumbnail_url,
'video_ids' => $p->videos()->pluck('videos.id')->toArray(),
// Track-aware entries: each row = {video_id, audio_track_id or null}.
// Consumers that need per-track granularity use this; older callers
// can still rely on video_ids above.
'entries' => \DB::table('playlist_videos')
->where('playlist_id', $p->id)
->select('video_id', 'audio_track_id')
->get()
->map(fn ($r) => [
'video_id' => (int) $r->video_id,
'audio_track_id' => $r->audio_track_id ? (int) $r->audio_track_id : null,
])->all(),
];
});