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:
1 parent
af276f2cd1
commit
ba8ab11ce4
12 files changed
+547
-49
No files matched your search
+46
-7
@@ -34,7 +34,7 @@ class Playlist extends Model
|
||||
public function videos()
|
||||
{
|
||||
return $this->belongsToMany(Video::class, 'playlist_videos')
|
||||
->withPivot('position', 'watched_seconds', 'watched', 'added_at', 'last_watched_at')
|
||||
->withPivot('position', 'audio_track_id', 'watched_seconds', 'watched', 'added_at', 'last_watched_at')
|
||||
->orderBy('position');
|
||||
}
|
||||
|
||||
@@ -296,24 +296,48 @@ class Playlist extends Model
|
||||
}
|
||||
}
|
||||
|
||||
// Add video to playlist
|
||||
public function addVideo(Video $video)
|
||||
// Add video to playlist. When $audioTrackId is null the primary track
|
||||
// plays; otherwise the specified language track is pinned to this row.
|
||||
// Same video with a *different* track becomes a separate row.
|
||||
public function addVideo(Video $video, ?int $audioTrackId = null)
|
||||
{
|
||||
if ($this->hasVideo($video)) {
|
||||
if ($this->hasVideoWithTrack($video, $audioTrackId)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$maxPosition = $this->videos()->max('position') ?? -1;
|
||||
|
||||
$this->videos()->attach($video->id, [
|
||||
'position' => $maxPosition + 1,
|
||||
'added_at' => now(),
|
||||
'position' => $maxPosition + 1,
|
||||
'audio_track_id' => $audioTrackId,
|
||||
'added_at' => now(),
|
||||
]);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove video from playlist
|
||||
// Remove a specific (video, track) row from the playlist. When
|
||||
// $audioTrackId is null, only the "primary track" row is removed —
|
||||
// language-pinned rows for the same video are left in place.
|
||||
public function removeVideoWithTrack(Video $video, ?int $audioTrackId = null)
|
||||
{
|
||||
$query = \DB::table('playlist_videos')
|
||||
->where('playlist_id', $this->id)
|
||||
->where('video_id', $video->id);
|
||||
if ($audioTrackId === null) {
|
||||
$query->whereNull('audio_track_id');
|
||||
} else {
|
||||
$query->where('audio_track_id', $audioTrackId);
|
||||
}
|
||||
$deleted = $query->delete();
|
||||
|
||||
if ($deleted) $this->reorderPositions();
|
||||
|
||||
return (bool) $deleted;
|
||||
}
|
||||
|
||||
// Remove *all* rows for a given video (any track). Preserved for
|
||||
// callers that don't care about the track distinction.
|
||||
public function removeVideo(Video $video)
|
||||
{
|
||||
if (! $this->hasVideo($video)) {
|
||||
@@ -328,6 +352,21 @@ class Playlist extends Model
|
||||
return true;
|
||||
}
|
||||
|
||||
// Does this playlist contain (video, track)? track=null matches the
|
||||
// "primary track" row specifically, not any row for the video.
|
||||
public function hasVideoWithTrack(Video $video, ?int $audioTrackId = null): bool
|
||||
{
|
||||
$query = \DB::table('playlist_videos')
|
||||
->where('playlist_id', $this->id)
|
||||
->where('video_id', $video->id);
|
||||
if ($audioTrackId === null) {
|
||||
$query->whereNull('audio_track_id');
|
||||
} else {
|
||||
$query->where('audio_track_id', $audioTrackId);
|
||||
}
|
||||
return $query->exists();
|
||||
}
|
||||
|
||||
// Reorder positions after removal
|
||||
protected function reorderPositions()
|
||||
{
|
||||
|
||||
Reference in new issue
Block a user