Skip to main content

#
jellytau_lib/player/
queue.rs

    1use rand::seq::SliceRandom;
    2use serde::{Deserialize, Serialize};
    3
    4use super::media::{MediaItem, MediaSource, QueueContext};
    5
    6/// Repeat mode for the queue
    7///
    8/// TRACES: UR-005 | DR-005
    9#[derive(specta::Type, Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)]
    10#[serde(rename_all = "lowercase")]
    11pub enum RepeatMode {
    12    #[default]
    13    Off,
    14    All,
    15    One,
    16}
    17
    18/// Queue manager for playlist functionality
    19///
    20/// TRACES: UR-005, UR-015 | DR-005, DR-020
    21#[derive(specta::Type, Debug, Clone, Serialize, Deserialize)]
    22pub struct QueueManager {
    23    /// All items in the queue
    24    items: Vec<MediaItem>,
    25    /// Current item index
    26    current_index: Option<usize>,
    27    /// Whether shuffle is enabled
    28    shuffle: bool,
    29    /// Current repeat mode
    30    repeat: RepeatMode,
    31    /// Shuffled order of indices (used when shuffle is on)
    32    shuffle_order: Vec<usize>,
    33    /// History of played indices (for going back with shuffle)
    34    history: Vec<usize>,
    35    /// Context for the queue (album, playlist, or custom)
    36    /// Used for remote playback transfer to maintain album/playlist context
    37    #[serde(default)]
    38    context: QueueContext,
    39}
    40
    41impl Default for QueueManager {
    42    fn default() -> Self {
    43        Self::new()
    44    }
    45}
    46
    47impl QueueManager {
    48    pub fn new() -> Self {
    49        Self {
    50            items: Vec::new(),
    51            current_index: None,
    52            shuffle: false,
    53            repeat: RepeatMode::Off,
    54            shuffle_order: Vec::new(),
    55            history: Vec::new(),
    56            context: QueueContext::Custom,
    57        }
    58    }
    59
    60    /// Get all items in the queue
    61    pub fn items(&self) -> &[MediaItem] {
    62        &self.items
    63    }
    64
    65    /// Mutable access to queue items.
    66    ///
    67    /// Used to re-point streaming entries at a completed local download
    68    /// without disturbing queue order, shuffle state, or the current index.
    69    pub fn items_mut(&mut self) -> &mut [MediaItem] {
    70        &mut self.items
    71    }
    72
    73    /// Get the current item index
    74    pub fn current_index(&self) -> Option<usize> {
    75        self.current_index
    76    }
    77
    78    /// Get the current item
    79    pub fn current(&self) -> Option<&MediaItem> {
    80        self.current_index.and_then(|i| self.items.get(i))
    81    }
    82
    83    /// Check if shuffle is enabled
    84    pub fn is_shuffle(&self) -> bool {
    85        self.shuffle
    86    }
    87
    88    /// Get the current repeat mode
    89    pub fn repeat_mode(&self) -> RepeatMode {
    90        self.repeat
    91    }
    92
    93    /// Get the current queue context (album, playlist, or custom)
    94    pub fn context(&self) -> &QueueContext {
    95        &self.context
    96    }
    97
    98    /// Set the queue context
    99    pub fn set_context(&mut self, context: QueueContext) {
    100        self.context = context;
    101    }
    102
    103    /// Set the queue with new items (resets context to Custom)
    104    pub fn set_queue(&mut self, items: Vec<MediaItem>, start_index: usize) {
    105        self.set_queue_with_context(items, start_index, QueueContext::Custom);
    106    }
    107
    108    /// Set the queue with new items and explicit context
    109    pub fn set_queue_with_context(
    110        &mut self,
    111        items: Vec<MediaItem>,
    112        start_index: usize,
    113        context: QueueContext,
    114    ) {
    115        let start_index = start_index.min(items.len().saturating_sub(1));
    116
    117        if self.shuffle && !items.is_empty() {
    118            self.shuffle_order = self.generate_shuffle_order(items.len(), Some(start_index));
    119        } else {
    120            self.shuffle_order.clear();
    121        }
    122
    123        self.items = items;
    124        self.current_index = if self.items.is_empty() {
    125            None
    126        } else {
    127            Some(start_index)
    128        };
    129        self.history.clear();
    130        self.context = context;
    131    }
    132
    133    /// Clear the queue entirely, returning it to the empty state.
    134    ///
    135    /// Used when playback genuinely stops (sleep timer fires, or the queue ends
    136    /// with repeat off) so the frontend's `currentQueueItem` becomes null and
    137    /// the mini player hides. History and shuffle order are reset too.
    138    pub fn clear(&mut self) {
    139        self.items.clear();
    140        self.current_index = None;
    141        self.history.clear();
    142        self.shuffle_order.clear();
    143    }
    144
    145    /// Add items to the queue
    146    pub fn add(&mut self, items: Vec<MediaItem>, position: AddPosition) {
    147        if items.is_empty() {
    148            return;
    149        }
    150
    151        let insert_index = match position {
    152            AddPosition::Next => self
    153                .current_index
    154                .map(|i| i + 1)
    155                .unwrap_or(self.items.len()),
    156            AddPosition::End => self.items.len(),
    157        };
    158
    159        // Insert items
    160        for (i, item) in items.into_iter().enumerate() {
    161            self.items.insert(insert_index + i, item);
    162        }
    163
    164        // Update current index if needed
    165        if let Some(current) = self.current_index {
    166            if insert_index <= current {
    167                self.current_index = Some(current + 1);
    168            }
    169        }
    170
    171        // Regenerate shuffle order if shuffle is on
    172        if self.shuffle {
    173            self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
    174        }
    175    }
    176
    177    /// Remove an item from the queue
    178    pub fn remove(&mut self, index: usize) -> Option<MediaItem> {
    179        if index >= self.items.len() {
    180            return None;
    181        }
    182
    183        let removed = self.items.remove(index);
    184
    185        // Update current index
    186        if let Some(current) = self.current_index {
    187            if index < current {
    188                self.current_index = Some(current - 1);
    189            } else if index == current {
    190                self.current_index = if self.items.is_empty() {
    191                    None
    192                } else if index >= self.items.len() {
    193                    Some(self.items.len() - 1)
    194                } else {
    195                    Some(index)
    196                };
    197            }
    198        }
    199
    200        // Update shuffle order
    201        if self.shuffle {
    202            self.shuffle_order = self
    203                .shuffle_order
    204                .iter()
    205                .filter(|&&i| i != index)
    206                .map(|&i| if i > index { i - 1 } else { i })
    207                .collect();
    208        }
    209
    210        Some(removed)
    211    }
    212
    213    /// Move to the next item
    214    pub fn next(&mut self) -> Option<&MediaItem> {
    215        if self.items.is_empty() {
    216            return None;
    217        }
    218
    219        let current = self.current_index?;
    220
    221        let next_index = if self.repeat == RepeatMode::One {
    222            // Repeat current track
    223            current
    224        } else if self.shuffle {
    225            // Find current position in shuffle order and get next
    226            let pos = self.shuffle_order.iter().position(|&i| i == current)?;
    227            if pos + 1 < self.shuffle_order.len() {
    228                self.shuffle_order[pos + 1]
    229            } else if self.repeat == RepeatMode::All {
    230                // Wrap around to beginning of shuffle
    231                self.shuffle_order[0]
    232            } else {
    233                log::debug!("[Queue] next() at end of shuffle order, no next track");
    234                return None;
    235            }
    236        } else {
    237            // Normal sequential order
    238            if current + 1 < self.items.len() {
    239                current + 1
    240            } else if self.repeat == RepeatMode::All {
    241                0
    242            } else {
    243                log::debug!(
    244                    "[Queue] next() at end of queue (index {}), no next track",
    245                    current
    246                );
    247                return None;
    248            }
    249        };
    250
    251        // Only add to history if we're actually changing position
    252        if next_index != current {
    253            self.history.push(current);
    254            log::debug!("[Queue] next() moving: {} -> {}", current, next_index);
    255        } else {
    256            log::debug!("[Queue] next() repeat one, staying at index {}", current);
    257        }
    258
    259        self.current_index = Some(next_index);
    260        self.items.get(next_index)
    261    }
    262
    263    /// Move to the previous item
    264    pub fn previous(&mut self) -> Option<&MediaItem> {
    265        if self.items.is_empty() {
    266            return None;
    267        }
    268
    269        let current = self.current_index?;
    270
    271        // If we have history, go back in history
    272        // But validate it to prevent wraparound bugs
    273        if let Some(prev) = self.history.pop() {
    274            // Safety check: ensure the history entry is valid
    275            if prev >= self.items.len() {
    276                log::warn!(
    277                    "[Queue] Invalid history entry {} (queue has {} items), clearing history",
    278                    prev,
    279                    self.items.len()
    280                );
    281                self.history.clear();
    282                return None;
    283            }
    284
    285            // In non-shuffle mode, previous track should be before current (or this is from a skip_to)
    286            // This prevents going from first track to last track
    287            if !self.shuffle && prev >= current {
    288                log::warn!("[Queue] Suspicious history: going from index {} to {} (non-shuffle mode), clearing history",
    289                    current, prev);
    290                self.history.clear();
    291                return None;
    292            }
    293
    294            log::debug!("[Queue] previous() using history: {} -> {}", current, prev);
    295            self.current_index = Some(prev);
    296            return self.items.get(prev);
    297        }
    298
    299        log::debug!("[Queue] previous() no history, current={}", current);
    300
    301        let prev_index = if self.shuffle {
    302            // In shuffle mode without history, go to previous in shuffle order
    303            let pos = self.shuffle_order.iter().position(|&i| i == current)?;
    304            if pos > 0 {
    305                self.shuffle_order[pos - 1]
    306            } else {
    307                log::debug!("[Queue] previous() at start of shuffle order, staying at current");
    308                return None;
    309            }
    310        } else {
    311            // Normal sequential order
    312            if current > 0 {
    313                current - 1
    314            } else {
    315                log::debug!("[Queue] previous() at index 0, staying at current");
    316                return None;
    317            }
    318        };
    319
    320        log::debug!("[Queue] previous() moving: {} -> {}", current, prev_index);
    321        self.current_index = Some(prev_index);
    322        self.items.get(prev_index)
    323    }
    324
    325    /// Skip to a specific index
    326    pub fn skip_to(&mut self, index: usize) -> Option<&MediaItem> {
    327        if index >= self.items.len() {
    328            return None;
    329        }
    330
    331        if let Some(current) = self.current_index {
    332            self.history.push(current);
    333        }
    334
    335        self.current_index = Some(index);
    336        self.items.get(index)
    337    }
    338
    339    /// Toggle shuffle mode
    340    pub fn toggle_shuffle(&mut self) {
    341        self.shuffle = !self.shuffle;
    342
    343        if self.shuffle && !self.items.is_empty() {
    344            self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
    345        } else {
    346            self.shuffle_order.clear();
    347        }
    348    }
    349
    350    /// Cycle through repeat modes
    351    pub fn cycle_repeat(&mut self) {
    352        self.repeat = match self.repeat {
    353            RepeatMode::Off => RepeatMode::All,
    354            RepeatMode::All => RepeatMode::One,
    355            RepeatMode::One => RepeatMode::Off,
    356        };
    357    }
    358
    359    /// Set repeat mode directly (for testing)
    360    #[cfg(test)]
    361    pub fn set_repeat(&mut self, mode: RepeatMode) {
    362        self.repeat = mode;
    363    }
    364
    365    /// Check if there's a next item available
    366    pub fn has_next(&self) -> bool {
    367        if self.items.is_empty() {
    368            return false;
    369        }
    370
    371        match self.current_index {
    372            None => false,
    373            Some(current) => {
    374                if self.repeat == RepeatMode::All || self.repeat == RepeatMode::One {
    375                    true
    376                } else if self.shuffle {
    377                    let pos = self.shuffle_order.iter().position(|&i| i == current);
    378                    pos.map(|p| p + 1 < self.shuffle_order.len())
    379                        .unwrap_or(false)
    380                } else {
    381                    current + 1 < self.items.len()
    382                }
    383            }
    384        }
    385    }
    386
    387    /// Check if there's a previous item available
    388    pub fn has_previous(&self) -> bool {
    389        !self.history.is_empty() || {
    390            match self.current_index {
    391                None => false,
    392                Some(current) => {
    393                    if self.shuffle {
    394                        let pos = self.shuffle_order.iter().position(|&i| i == current);
    395                        pos.map(|p| p > 0).unwrap_or(false)
    396                    } else {
    397                        current > 0
    398                    }
    399                }
    400            }
    401        }
    402    }
    403
    404    /// Get the next N upcoming items (for preloading)
    405    /// Returns items that will play after the current item, respecting shuffle order
    406    pub fn get_upcoming(&self, count: usize) -> Vec<&MediaItem> {
    407        if self.items.is_empty() || count == 0 {
    408            return Vec::new();
    409        }
    410
    411        let current = match self.current_index {
    412            Some(idx) => idx,
    413            None => return Vec::new(),
    414        };
    415
    416        let mut upcoming = Vec::with_capacity(count);
    417
    418        if self.shuffle {
    419            // Find current position in shuffle order
    420            if let Some(pos) = self.shuffle_order.iter().position(|&i| i == current) {
    421                for i in 1..=count {
    422                    let next_pos = pos + i;
    423                    if next_pos < self.shuffle_order.len() {
    424                        if let Some(item) = self.items.get(self.shuffle_order[next_pos]) {
    425                            upcoming.push(item);
    426                        }
    427                    } else if self.repeat == RepeatMode::All {
    428                        // Wrap around if repeat all is enabled
    429                        let wrapped_pos = (next_pos) % self.shuffle_order.len();
    430                        if let Some(item) = self.items.get(self.shuffle_order[wrapped_pos]) {
    431                            upcoming.push(item);
    432                        }
    433                    }
    434                }
    435            }
    436        } else {
    437            // Normal sequential order
    438            for i in 1..=count {
    439                let next_idx = current + i;
    440                if next_idx < self.items.len() {
    441                    upcoming.push(&self.items[next_idx]);
    442                } else if self.repeat == RepeatMode::All {
    443                    // Wrap around if repeat all is enabled
    444                    let wrapped_idx = next_idx % self.items.len();
    445                    upcoming.push(&self.items[wrapped_idx]);
    446                }
    447            }
    448        }
    449
    450        upcoming
    451    }
    452
    453    /// Move an item from one index to another
    454    pub fn move_item(&mut self, from_index: usize, to_index: usize) -> bool {
    455        if from_index >= self.items.len() || to_index >= self.items.len() || from_index == to_index
    456        {
    457            return false;
    458        }
    459
    460        // Remove the item and insert at new position
    461        let item = self.items.remove(from_index);
    462        self.items.insert(to_index, item);
    463
    464        // Update current_index if affected
    465        if let Some(current) = self.current_index {
    466            if current == from_index {
    467                // The moved item was the current one
    468                self.current_index = Some(to_index);
    469            } else if from_index < current && to_index >= current {
    470                // Item moved from before current to after/at current
    471                self.current_index = Some(current - 1);
    472            } else if from_index > current && to_index <= current {
    473                // Item moved from after current to before/at current
    474                self.current_index = Some(current + 1);
    475            }
    476        }
    477
    478        // Update shuffle order if shuffle is on
    479        if self.shuffle && !self.shuffle_order.is_empty() {
    480            // Regenerate shuffle order to maintain consistency
    481            self.shuffle_order = self.generate_shuffle_order(self.items.len(), self.current_index);
    482        }
    483
    484        true
    485    }
    486
    487    /// Update the stream URL of the current item (for transcoded seeking)
    488    /// Returns true if the update was successful
    489    pub fn update_current_stream_url(&mut self, new_url: String) -> bool {
    490        if let Some(current_index) = self.current_index {
    491            if let Some(item) = self.items.get_mut(current_index) {
    492                // Only update if it's a Remote source
    493                if let MediaSource::Remote {
    494                    jellyfin_item_id, ..
    495                } = &item.source
    496                {
    497                    item.source = MediaSource::Remote {
    498                        stream_url: new_url,
    499                        jellyfin_item_id: jellyfin_item_id.clone(),
    500                    };
    501                    return true;
    502                }
    503            }
    504        }
    505        false
    506    }
    507
    508    /// Generate a shuffle order, optionally starting from a specific index
    509    fn generate_shuffle_order(&self, length: usize, start_index: Option<usize>) -> Vec<usize> {
    510        let mut indices: Vec<usize> = (0..length).collect();
    511        let mut rng = rand::thread_rng();
    512        indices.shuffle(&mut rng);
    513
    514        // Move start index to the front if specified
    515        if let Some(start) = start_index {
    516            if let Some(pos) = indices.iter().position(|&i| i == start) {
    517                indices.remove(pos);
    518                indices.insert(0, start);
    519            }
    520        }
    521
    522        indices
    523    }
    524}
    525
    526/// Position to add items to the queue
    527#[derive(Debug, Clone, Copy, PartialEq, Eq)]
    528pub enum AddPosition {
    529    /// Add immediately after current item
    530    Next,
    531    /// Add at the end of the queue
    532    End,
    533}
    534
    535// TRACES: UR-005, UR-015 | DR-005 | UT-003, UT-004, UT-005
    536#[cfg(test)]
    537mod tests {
    538    use super::*;
    539    use crate::player::media::{MediaSource, MediaType};
    540
    541    fn create_test_items(count: usize) -> Vec<MediaItem> {
    542        (0..count)
    543            .map(|i| MediaItem {
    544                // Audio and direct-URL items never negotiate a transport.
    545                transport: None,
    546                id: format!("item_{}", i),
    547                title: format!("Track {}", i + 1),
    548                name: Some(format!("Track {}", i + 1)),
    549                artist: Some("Artist".to_string()),
    550                album: None,
    551                album_name: None,
    552                album_id: None,
    553                artist_items: None,
    554                artists: Some(vec!["Artist".to_string()]),
    555                primary_image_tag: None,
    556                image_id: None,
    557                item_type: Some("Audio".to_string()),
    558                playlist_id: None,
    559                duration: Some(180.0),
    560                artwork_url: None,
    561                media_type: MediaType::Audio,
    562                source: MediaSource::DirectUrl {
    563                    url: format!("http://example.com/track_{}.mp3", i),
    564                },
    565                video_codec: None,
    566                needs_transcoding: false,
    567                video_width: None,
    568                video_height: None,
    569                subtitles: vec![],
    570                series_id: None,
    571                server_id: None,
    572            })
    573            .collect()
    574    }
    575
    576    /// Test setting up the queue with items
    577    ///
    578    /// @req-test: UR-015 - View and manage audio queue (add tracks)
    579    /// @req-test: DR-005 - Queue manager with shuffle, repeat, history
    580    #[test]
    581    fn test_set_queue() {
    582        let mut queue = QueueManager::new();
    583        let items = create_test_items(5);
    584
    585        queue.set_queue(items.clone(), 0);
    586
    587        assert_eq!(queue.items().len(), 5);
    588        assert_eq!(queue.current_index(), Some(0));
    589        assert_eq!(queue.current().unwrap().id, "item_0");
    590    }
    591
    592    /// Test clearing the queue returns it to the empty state so the frontend
    593    /// hides the mini player on a genuine stop.
    594    #[test]
    595    fn test_clear() {
    596        let mut queue = QueueManager::new();
    597        queue.set_queue(create_test_items(3), 1);
    598        assert_eq!(queue.current_index(), Some(1));
    599
    600        queue.clear();
    601
    602        assert_eq!(queue.items().len(), 0);
    603        assert_eq!(queue.current_index(), None);
    604        assert!(queue.current().is_none());
    605    }
    606
    607    /// Test next track navigation
    608    ///
    609    /// @req-test: UR-005 - Control media playback (skip to next track)
    610    /// @req-test: DR-005 - Queue manager with shuffle, repeat, history
    611    #[test]
    612    fn test_next() {
    613        let mut queue = QueueManager::new();
    614        queue.set_queue(create_test_items(3), 0);
    615
    616        assert_eq!(queue.current().unwrap().id, "item_0");
    617
    618        queue.next();
    619        assert_eq!(queue.current().unwrap().id, "item_1");
    620
    621        queue.next();
    622        assert_eq!(queue.current().unwrap().id, "item_2");
    623
    624        // No next without repeat
    625        assert!(queue.next().is_none());
    626    }
    627
    628    /// Test repeat all mode wraps to beginning
    629    ///
    630    /// @req-test: UR-005 - Control media playback (repeat all mode)
    631    /// @req-test: DR-005 - Queue manager with repeat
    632    #[test]
    633    fn test_repeat_all() {
    634        let mut queue = QueueManager::new();
    635        queue.set_queue(create_test_items(2), 0);
    636        queue.set_repeat(RepeatMode::All);
    637
    638        queue.next(); // Move to item_1
    639        let next = queue.next(); // Should wrap to item_0
    640
    641        assert!(next.is_some());
    642        assert_eq!(queue.current().unwrap().id, "item_0");
    643    }
    644
    645    /// Test previous track navigation
    646    ///
    647    /// @req-test: UR-005 - Control media playback (previous track)
    648    /// @req-test: DR-005 - Queue manager
    649    #[test]
    650    fn test_previous() {
    651        let mut queue = QueueManager::new();
    652        queue.set_queue(create_test_items(3), 2);
    653
    654        queue.previous();
    655        assert_eq!(queue.current_index(), Some(1));
    656    }
    657
    658    /// Test viewing upcoming tracks in queue
    659    ///
    660    /// @req-test: UR-015 - View and manage audio queue
    661    /// @req-test: DR-020 - Queue management UI (upcoming tracks)
    662    #[test]
    663    fn test_get_upcoming() {
    664        let mut queue = QueueManager::new();
    665        queue.set_queue(create_test_items(5), 0);
    666
    667        // Get next 3 items
    668        let upcoming = queue.get_upcoming(3);
    669        assert_eq!(upcoming.len(), 3);
    670        assert_eq!(upcoming[0].id, "item_1");
    671        assert_eq!(upcoming[1].id, "item_2");
    672        assert_eq!(upcoming[2].id, "item_3");
    673
    674        // Move to item 2 and get upcoming
    675        queue.next();
    676        queue.next();
    677        let upcoming = queue.get_upcoming(3);
    678        assert_eq!(upcoming.len(), 2); // Only 2 items remaining
    679        assert_eq!(upcoming[0].id, "item_3");
    680        assert_eq!(upcoming[1].id, "item_4");
    681    }
    682
    683    /// Test upcoming tracks with repeat all mode
    684    ///
    685    /// @req-test: UR-015 - View and manage audio queue
    686    /// @req-test: DR-005 - Queue manager with repeat
    687    #[test]
    688    fn test_get_upcoming_with_repeat() {
    689        let mut queue = QueueManager::new();
    690        queue.set_queue(create_test_items(3), 1);
    691        queue.set_repeat(RepeatMode::All);
    692
    693        // At item_1, get upcoming with repeat
    694        let upcoming = queue.get_upcoming(4);
    695        assert_eq!(upcoming.len(), 4);
    696        assert_eq!(upcoming[0].id, "item_2"); // Next
    697        assert_eq!(upcoming[1].id, "item_0"); // Wrapped
    698        assert_eq!(upcoming[2].id, "item_1"); // Wrapped (current again)
    699        assert_eq!(upcoming[3].id, "item_2"); // Wrapped
    700    }
    701
    702    /// Test upcoming tracks on empty queue
    703    ///
    704    /// @req-test: DR-005 - Queue manager (edge case: empty queue)
    705    #[test]
    706    fn test_get_upcoming_empty() {
    707        let queue = QueueManager::new();
    708        let upcoming = queue.get_upcoming(3);
    709        assert!(upcoming.is_empty());
    710    }
    711
    712    /// Test next stops at end without repeat mode
    713    ///
    714    /// @req-test: UR-005 - Control media playback (queue end behavior)
    715    /// @req-test: DR-005 - Queue manager
    716    #[test]
    717    fn test_next_no_repeat_reaches_end() {
    718        let mut queue = QueueManager::new();
    719        queue.set_queue(create_test_items(3), 0);
    720        queue.set_repeat(RepeatMode::Off);
    721
    722        // At item 0, should have next
    723        assert!(queue.has_next());
    724        assert_eq!(queue.current_index(), Some(0));
    725
    726        // Move to item 1
    727        queue.next();
    728        assert!(queue.has_next());
    729        assert_eq!(queue.current_index(), Some(1));
    730
    731        // Move to item 2 (last)
    732        queue.next();
    733        assert!(!queue.has_next()); // No more items
    734        assert_eq!(queue.current_index(), Some(2));
    735
    736        // Try to move past end
    737        let result = queue.next();
    738        assert!(result.is_none());
    739        assert_eq!(queue.current_index(), Some(2)); // Should stay at last item
    740    }
    741
    742    /// Test next wraps to beginning with repeat all
    743    ///
    744    /// @req-test: UR-005 - Control media playback (repeat all wrapping)
    745    /// @req-test: DR-005 - Queue manager with repeat
    746    #[test]
    747    fn test_next_repeat_all_wraps() {
    748        let mut queue = QueueManager::new();
    749        queue.set_queue(create_test_items(3), 0);
    750        queue.set_repeat(RepeatMode::All);
    751
    752        // With repeat all, has_next should always be true
    753        assert!(queue.has_next());
    754
    755        // Move through all items
    756        queue.next(); // item_1
    757        assert!(queue.has_next());
    758        queue.next(); // item_2
    759        assert!(queue.has_next());
    760
    761        // Wrap to beginning
    762        let result = queue.next();
    763        assert!(result.is_some());
    764        assert_eq!(queue.current().unwrap().id, "item_0");
    765        assert!(queue.has_next()); // Still has next (loops forever)
    766    }
    767
    768    /// Test next repeats same track with repeat one mode
    769    ///
    770    /// @req-test: UR-005 - Control media playback (repeat one mode)
    771    /// @req-test: DR-005 - Queue manager with repeat
    772    #[test]
    773    fn test_next_repeat_one_stays() {
    774        let mut queue = QueueManager::new();
    775        queue.set_queue(create_test_items(3), 1);
    776        queue.set_repeat(RepeatMode::One);
    777
    778        // Should always have next (repeats current)
    779        assert!(queue.has_next());
    780        assert_eq!(queue.current_index(), Some(1));
    781
    782        // Call next multiple times - should stay on same track
    783        for _ in 0..5 {
    784            let result = queue.next();
    785            assert!(result.is_some());
    786            assert_eq!(queue.current().unwrap().id, "item_1");
    787            assert_eq!(queue.current_index(), Some(1));
    788            assert!(queue.has_next());
    789        }
    790    }
    791
    792    /// Test shuffle mode follows randomized order
    793    ///
    794    /// @req-test: UR-005 - Control media playback (shuffle mode)
    795    /// @req-test: DR-005 - Queue manager with shuffle
    796    #[test]
    797    fn test_next_shuffle_follows_order() {
    798        let mut queue = QueueManager::new();
    799        queue.set_queue(create_test_items(4), 0);
    800        queue.toggle_shuffle(); // Enable shuffle
    801
    802        // Get the shuffle order for verification
    803        let shuffle_order = queue.shuffle_order.clone();
    804        assert_eq!(shuffle_order.len(), 4);
    805
    806        // Current should be first item in shuffle order
    807        let first_shuffled_index = shuffle_order[0];
    808        assert_eq!(queue.current_index(), Some(first_shuffled_index));
    809
    810        // Move through shuffle order
    811        for &expected_index in &shuffle_order[1..] {
    812            assert!(queue.has_next());
    813            let result = queue.next();
    814            assert!(result.is_some());
    815            assert_eq!(queue.current_index(), Some(expected_index));
    816        }
    817
    818        // At end of shuffle without repeat
    819        assert!(!queue.has_next());
    820        let result = queue.next();
    821        assert!(result.is_none());
    822    }
    823
    824    /// Test shuffle with repeat all wraps shuffle order
    825    ///
    826    /// @req-test: UR-005 - Control media playback (shuffle + repeat)
    827    /// @req-test: DR-005 - Queue manager with shuffle and repeat
    828    #[test]
    829    fn test_next_shuffle_with_repeat_all() {
    830        let mut queue = QueueManager::new();
    831        queue.set_queue(create_test_items(3), 0);
    832        queue.toggle_shuffle(); // Enable shuffle
    833        queue.set_repeat(RepeatMode::All);
    834
    835        let shuffle_order = queue.shuffle_order.clone();
    836
    837        // Move through entire shuffle order
    838        for _ in 1..shuffle_order.len() {
    839            queue.next();
    840        }
    841
    842        // At end, should wrap to beginning of shuffle order
    843        assert!(queue.has_next());
    844        let result = queue.next();
    845        assert!(result.is_some());
    846        assert_eq!(queue.current_index(), Some(shuffle_order[0]));
    847    }
    848
    849    /// Test has_next logic accuracy across different scenarios
    850    ///
    851    /// @req-test: DR-005 - Queue manager (has_next accuracy)
    852    /// @req-test: UR-015 - View and manage audio queue
    853    #[test]
    854    fn test_has_next_accuracy() {
    855        // Test 1: Empty queue
    856        let queue = QueueManager::new();
    857        assert!(!queue.has_next());
    858
    859        // Test 2: Last track with repeat off
    860        let mut queue = QueueManager::new();
    861        queue.set_queue(create_test_items(2), 1); // Start at last item
    862        queue.set_repeat(RepeatMode::Off);
    863        assert!(!queue.has_next());
    864
    865        // Test 3: Last track with repeat all
    866        let mut queue = QueueManager::new();
    867        queue.set_queue(create_test_items(2), 1); // Start at last item
    868        queue.set_repeat(RepeatMode::All);
    869        assert!(queue.has_next()); // Should wrap
    870
    871        // Test 4: Repeat one mode (always has next)
    872        let mut queue = QueueManager::new();
    873        queue.set_queue(create_test_items(1), 0);
    874        queue.set_repeat(RepeatMode::One);
    875        assert!(queue.has_next()); // Repeats forever
    876
    877        // Test 5: Middle of queue with repeat off
    878        let mut queue = QueueManager::new();
    879        queue.set_queue(create_test_items(3), 1); // Middle item
    880        queue.set_repeat(RepeatMode::Off);
    881        assert!(queue.has_next()); // Has item_2 next
    882
    883        // Test 6: Shuffle at end without repeat
    884        let mut queue = QueueManager::new();
    885        queue.set_queue(create_test_items(3), 0);
    886        queue.toggle_shuffle(); // Enable shuffle
    887        queue.set_repeat(RepeatMode::Off);
    888        // Move to last item in shuffle order
    889        let shuffle_len = queue.shuffle_order.len();
    890        for _ in 1..shuffle_len {
    891            queue.next();
    892        }
    893        assert!(!queue.has_next());
    894    }
    895
    896    /// Test has_next on empty queue edge case
    897    ///
    898    /// @req-test: DR-005 - Queue manager (edge case: empty queue)
    899    #[test]
    900    fn test_has_next_empty_queue() {
    901        let queue = QueueManager::new();
    902        assert!(!queue.has_next());
    903        assert_eq!(queue.current_index(), None);
    904    }
    905
    906    /// Test that selecting a specific track in an album starts at the correct index
    907    ///
    908    /// Reproduces the bug where clicking songs 1-5 always played song 13
    909    #[test]
    910    fn test_play_specific_track_from_album() {
    911        let mut queue = QueueManager::new();
    912        let items = create_test_items(14); // 14-track album
    913
    914        // Simulate playing track 0 (first track)
    915        queue.set_queue(items.clone(), 0);
    916        assert_eq!(queue.current_index(), Some(0));
    917        assert_eq!(queue.current().unwrap().id, "item_0");
    918
    919        // Simulate playing track 3 (fourth track)
    920        queue.set_queue(items.clone(), 3);
    921        assert_eq!(queue.current_index(), Some(3));
    922        assert_eq!(queue.current().unwrap().id, "item_3");
    923
    924        // Simulate playing track 13 (last track)
    925        queue.set_queue(items, 13);
    926        assert_eq!(queue.current_index(), Some(13));
    927        assert_eq!(queue.current().unwrap().id, "item_13");
    928    }
    929
    930    /// Test that next() then previous() returns to the original track
    931    #[test]
    932    fn test_next_previous_roundtrip() {
    933        let mut queue = QueueManager::new();
    934        queue.set_queue(create_test_items(5), 2); // Start at middle track
    935
    936        assert_eq!(queue.current_index(), Some(2));
    937
    938        // Go to next track
    939        queue.next();
    940        assert_eq!(queue.current_index(), Some(3));
    941
    942        // Go back - should return to track 2
    943        queue.previous();
    944        assert_eq!(queue.current_index(), Some(2));
    945        assert_eq!(queue.current().unwrap().id, "item_2");
    946    }
    947
    948    /// Test that previous() at the first track stays at first track
    949    #[test]
    950    fn test_previous_at_first_track_stays() {
    951        let mut queue = QueueManager::new();
    952        queue.set_queue(create_test_items(5), 0); // Start at first track
    953
    954        assert_eq!(queue.current_index(), Some(0));
    955
    956        // Try to go to previous - should stay at 0
    957        let result = queue.previous();
    958        assert!(result.is_none());
    959        assert_eq!(queue.current_index(), Some(0));
    960        assert_eq!(queue.current().unwrap().id, "item_0");
    961    }
    962
    963    /// Test that next() at last track (no repeat) stays at last track
    964    #[test]
    965    fn test_next_at_last_track_stays() {
    966        let mut queue = QueueManager::new();
    967        queue.set_queue(create_test_items(5), 4); // Start at last track
    968
    969        assert_eq!(queue.current_index(), Some(4));
    970
    971        // Try to go to next - should return None and stay at 4
    972        let result = queue.next();
    973        assert!(result.is_none());
    974        assert_eq!(queue.current_index(), Some(4));
    975        assert_eq!(queue.current().unwrap().id, "item_4");
    976    }
    977
    978    /// Test history validation prevents invalid wraparound
    979    #[test]
    980    fn test_history_validation_prevents_wraparound() {
    981        let mut queue = QueueManager::new();
    982        queue.set_queue(create_test_items(5), 0);
    983
    984        // Manually corrupt history to simulate the bug
    985        // (In the real bug, history would have last track's index)
    986        queue.history.push(4);
    987
    988        // Try to go previous - should detect invalid history and clear it
    989        let result = queue.previous();
    990        assert!(result.is_none()); // Should not wrap to track 4
    991        assert_eq!(queue.current_index(), Some(0)); // Should stay at track 0
    992        assert!(queue.history.is_empty()); // History should be cleared
    993    }
    994
    995    /// Test multiple next() calls build correct history
    996    #[test]
    997    fn test_multiple_next_builds_history() {
    998        let mut queue = QueueManager::new();
    999        queue.set_queue(create_test_items(5), 0);
    1000
    1001        // Navigate: 0 -> 1 -> 2 -> 3
    1002        queue.next(); // Now at 1, history=[0]
    1003        queue.next(); // Now at 2, history=[0, 1]
    1004        queue.next(); // Now at 3, history=[0, 1, 2]
    1005
    1006        assert_eq!(queue.current_index(), Some(3));
    1007
    1008        // Go back through history: 3 -> 2 -> 1 -> 0
    1009        queue.previous();
    1010        assert_eq!(queue.current_index(), Some(2));
    1011
    1012        queue.previous();
    1013        assert_eq!(queue.current_index(), Some(1));
    1014
    1015        queue.previous();
    1016        assert_eq!(queue.current_index(), Some(0));
    1017    }
    1018}