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

@@ -237,9 +237,22 @@
.then(data => {
if (data.success && data.playlists && data.playlists.length > 0) {
let html = '';
// Language-track the modal is scoped to (0 = primary).
// Read once here so both the "already in?" check and the
// toggle call use the same value.
const currentTrackId = parseInt(window._ytpTrackId || 0);
data.playlists.forEach(function(playlist) {
const isInPlaylist = playlist.video_ids && playlist.video_ids.includes(parseInt(
videoId));
// Prefer entries[] (per-track granularity) when present.
// Fall back to video_ids for older API responses.
let isInPlaylist = false;
if (Array.isArray(playlist.entries)) {
isInPlaylist = playlist.entries.some(function (e) {
return e.video_id === parseInt(videoId)
&& (e.audio_track_id || 0) === currentTrackId;
});
} else if (playlist.video_ids) {
isInPlaylist = playlist.video_ids.includes(parseInt(videoId));
}
const visibilityText = playlist.visibility === 'public' ? 'Public' : 'Private';
const durationText = playlist.formatted_duration || '0m';
html += `
@@ -328,7 +341,11 @@
// of the encoded route key used by GET /videos/{video}.
var url = `/playlists/${playlistId}/videos`;
var method = currentlyIn ? 'DELETE' : 'POST';
var body = JSON.stringify({ video_id: videoId });
// Pin the row to the currently-active audio track (0 = primary/null).
var trackId = parseInt(window._ytpTrackId || 0);
var payload = { video_id: videoId };
if (trackId > 0) payload.audio_track_id = trackId;
var body = JSON.stringify(payload);
fetch(url, {
method,