using DeepDrftModels.DTOs; namespace DeepDrftPublic.Client.Services; /// /// Orchestrates ordered playback ("what plays next") above the single-slot /// . The player stays a single-track device; the queue owns the /// track list, the current position, skip-forward/back, and auto-advance on natural track end. It /// drives playback solely through the player's existing /// — it adds no new playback semantics. /// /// /// Two-level deque model (the load-bearing invariant). The queue is a deque whose /// track (the item at ) is the live "front of play". /// Two families of mutation enter the deque from opposite ends: /// /// PLAY (manual) / prepend to the /// front. The previously-current track is removed, the prepended track(s) become the head /// in order, the new head becomes current and starts streaming, and whatever sat after the old /// current stays intact behind the prepend. A whole release prepends in order in one operation. /// Add-to-queue / append to the /// end. They never interrupt the current track and never start playback. /// /// /// /// /// Advance and end-of-track. and auto-advance (the player's /// ) walk forward, leaving the just- /// played track in the list behind the pointer so can step back to it. The one /// exception is the last track: when the current track ends naturally and there is nothing /// after it, the queue empties and goes dormant ( == -1) rather than /// stranding the finished track as current. /// /// /// /// With an empty queue ( == -1) the queue is dormant: it drives nothing and /// auto-advances nothing, so direct single-track play through the player behaves exactly as it did /// before the queue existed. The first / into a /// dormant queue while a track is already playing externally seeds the head from the player's current /// track (learned through the attached player, no extra dependency) and then appends the added item, so /// the resulting deque is [now-playing, added…] rather than a phantom single entry. /// /// public interface IQueueService { /// The ordered tracks currently queued. Empty when nothing is enqueued. IReadOnlyList Items { get; } /// /// Index into of the track the queue considers current, or -1 when the /// queue is empty. Always a valid index into when non-negative. /// int CurrentIndex { get; } /// The current track, or null when the queue is empty. TrackDto? Current { get; } /// /// True when the queue has been loaded via but no track has streamed yet — /// the embed's pre-gesture state. Set by ; cleared the moment playback actually /// starts (//// /// ) or on . The player bar reads this to route the first /// play gesture through (which begins the armed release) rather than streaming /// the staged track alone. /// bool IsArmed { get; } /// True when there is a track after to advance to. bool HasNext { get; } /// True when there is a track before to step back to. bool HasPrevious { get; } /// /// Raised whenever the queue's contents or current position change. The player bar subscribes /// to re-render its skip-forward/back affordances. Fires on enqueue, prepend, advance, step-back, /// and clear. /// event Action? QueueChanged; /// /// Manual PLAY of a single track: prepends to the front of the /// deque, removes the previously-current track, makes the new head/current, /// and starts streaming it. The rest of the queue (everything that sat after the old current) stays /// intact behind the new head. Into a dormant queue this simply becomes the sole head and plays. /// This is the deque-front counterpart to the append-only . /// Task PlayTrack(TrackDto track); /// /// Manual PLAY of a release: prepends (in the order given) to the /// front of the deque, removes the previously-current track, and starts streaming the /// prepended track at — which becomes current. Tracks prepended /// before sit behind the pointer (reachable via ); /// tracks after it are up-next; whatever sat after the old current stays intact behind the whole /// prepend. This is the "play album" entry point the detail pages consume: a header Play uses /// startIndex: 0; a mid-album row play passes that row's index. No-op when /// is empty. /// Task PlayRelease(IEnumerable tracks, int startIndex = 0); /// /// Loads as the queue and sets the current position to index 0 WITHOUT /// streaming anything — the queue is "armed". This is the embed's prerender-safe entry point: it /// performs no JS interop, so it runs identically during prerender and after WASM boot. It replaces /// the queue (an armed embed is a fresh staged release, not a prepend). The first play gesture (see /// ) then starts playback via , which keeps the loaded /// release queued so it advances through its tracks. No-op when is empty /// (the queue stays empty and disarmed). /// void Arm(IEnumerable tracks); /// /// Begins playback of an armed queue (see ): streams the current track and clears /// , leaving the loaded list and position intact so auto-advance carries on /// through the release. This is the first-gesture entry point the embed bar calls. No-op (and stays /// disarmed) when the queue is not armed or is empty — so it never double-streams or disturbs a /// queue already playing. /// Task Start(); /// /// Appends a track to the end of the queue without changing what is currently playing. /// Into a dormant queue ( == -1) while a track is already playing /// externally (through the attached player but not via the queue), the append first seeds the head /// with that now-playing track, then appends — yielding /// [now-playing, track] so the queue reflects what the listener actually hears. Into a fully /// dormant queue with nothing playing, the single appended track becomes the head at /// == 0. Either way it does NOT begin playback (add is not play). /// Interop-free; safe during prerender. /// void Enqueue(TrackDto track); /// /// Appends tracks to the end of the queue without changing what is currently playing. /// Into a dormant queue while a track is already playing externally, the append first seeds the head /// with that now-playing track (see ), then appends the range. Into a fully /// dormant queue with nothing playing, the first appended track becomes the head at /// == 0. It does NOT begin playback (add is not play). Interop-free; safe /// during prerender. /// void EnqueueRange(IEnumerable tracks); /// /// Reorders the queue, moving the track at to /// , and re-emits . Adjusts /// so the same track stays current across the move — it does /// not restart, re-stream, or interrupt the currently-playing track. Interop-free; safe during /// prerender. No-op (no throw, no ) when either index is out of range /// or the indices are equal. /// void Move(int fromIndex, int toIndex); /// /// Removes the track at and re-emits . Does /// not touch playback (the player stays a single-track device): removing the current track does /// not stop it — the playing track runs to its natural end while /// resolves to the new occupant of that slot (the next track) so the next auto-advance/skip is /// coherent. Removing a track before the current decrements (the same /// track stays current); removing after the current leaves it unchanged. Removing the last /// remaining track empties the queue ( == -1, dormant). Interop-free; /// safe during prerender. No-op (no throw, no ) when /// is out of range. /// void RemoveAt(int index); /// /// Advances to the next track and streams it. No-op when is false. /// Task Next(); /// /// Steps back to the previous track and streams it. No-op when is false. /// Task Previous(); /// /// Moves the current pointer to and streams that track once. This is the /// row-jump primitive the open playlist panel uses: unlike it does not /// prepend (the track is already in the deque), and unlike repeated it does not /// stream the intervening rows. No-op when is out of range or already /// current. /// Task JumpTo(int index); /// Empties the queue and resets the position. Does not stop the player. void Clear(); /// /// Empties the up-next while keeping the currently-playing track: removes every item except /// , leaving it as the sole remaining item at == 0, /// and re-emits . Unlike (which empties everything and /// goes dormant), this preserves what is playing — the player is never stopped and the current track /// stays queued, so playback continues uninterrupted while the rest of the queue is discarded. /// Interop-free; safe during prerender. No-op (no throw, no ) when the queue /// is empty/dormant or already holds only the current track. /// void ClearUpcoming(); }