Krolyxon
Commit 222ab2f

rmptui222ab2f

Play the selected song when loading a playlist with missing entries

MPD's load silently skips entries it cannot resolve, so the playlist
index no longer matched the queue offset and a later song played. Match
the playlist's entries against the songs load actually appended instead.
Author
krolyxon
Date
2026-10-17 17:38 +0530
Parent
61cb83e
Commit
222ab2ffa1cc04c6fe2e282510e7a3cb9090f24c

+75 −3

1 file changed

  1. M src/mpd/worker.rs +75 −3
M src/mpd/worker.rs View
@@ -15,7 +15,7 @@ use super::{
1515 commands,
1616 connection::{Address, Connection},
1717 error::{MpdError, MpdResult},
18− protocol::Command,
18+ protocol::{Command, Pair},
1919 types::{
2020 Album, DirEntry, PlaylistInfo, Song, Status, Subsystem, parse_albums, parse_entries,
2121 parse_playlists, parse_songs, parse_subsystems, parse_tag_values,
@@ -296,11 +296,59 @@ async fn execute(conn: &mut Connection, request: &Request) -> MpdResult<Option<R
296296 }
297297 Request::LoadAndPlay { name, offset } => {
298298 let start = queue_length(conn).await?;
299− conn.execute_all(&[commands::load(name), commands::play_pos(start + offset)])
299+ let entries = conn.execute(&Command::new("listplaylist").arg(name)).await?;
300+ conn.execute(&commands::load(name)).await?;
301+ // `load` silently skips entries the server cannot resolve, so the
302+ // playlist index is not necessarily the queue offset.
303+ let added = conn
304+ .execute(&Command::new("playlistinfo").arg(format!("{start}:")))
305+ .await?;
306+ let entries: Vec<&str> = files(&entries).collect();
307+ let added: Vec<&str> = files(&added).collect();
308+ let Some(pos) = queued_offset(&entries, &added, *offset as usize) else {
309+ return Ok(None);
310+ };
311+ conn.execute(&commands::play_pos(start + pos as u32))
300312 .await
301− .map(|()| None)
313+ .map(|_| None)
314+ }
315+ }
316+}
317+
318+fn files(pairs: &[Pair]) -> impl Iterator<Item = &str> {
319+ pairs
320+ .iter()
321+ .filter(|(key, _)| key == "file")
322+ .map(|(_, value)| value.as_str())
323+}
324+
325+/// Where playlist entry `index` ended up among the songs `load` appended,
326+/// matching entries in order and skipping the ones that were dropped.
327+fn queued_offset(entries: &[&str], added: &[&str], index: usize) -> Option<usize> {
328+ if entries.len() == added.len() {
329+ return (index < added.len()).then_some(index);
330+ }
331+ let mut pos = 0;
332+ for (i, entry) in entries.iter().enumerate() {
333+ let matched = added.get(pos).is_some_and(|song| same_song(entry, song));
334+ if i == index {
335+ // The entry was skipped: play whatever followed it instead.
336+ return (pos < added.len()).then_some(pos);
337+ }
338+ if matched {
339+ pos += 1;
302340 }
303341 }
342+ None
343+}
344+
345+/// Playlists may store absolute paths, which `load` turns into paths
346+/// relative to the music directory.
347+fn same_song(entry: &str, queued: &str) -> bool {
348+ entry == queued
349+ || entry
350+ .strip_suffix(queued)
351+ .is_some_and(|prefix| prefix.ends_with('/'))
304352 }
305353
306354 async fn queue_length(conn: &mut Connection) -> MpdResult<u32> {
@@ -401,3 +449,27 @@ async fn run_query(conn: &mut Connection, query: &Query) -> MpdResult<Reply> {
401449 }
402450 })
403451 }
452+
453+#[cfg(test)]
454+mod tests {
455+ use super::queued_offset;
456+
457+ #[test]
458+ fn queued_offset_skips_dropped_entries() {
459+ let entries = ["gone.mp3", "a.mp3", "b.mp3", "missing.mp3", "c.mp3"];
460+ let added = ["a.mp3", "b.mp3", "c.mp3"];
461+ assert_eq!(queued_offset(&entries, &added, 1), Some(0));
462+ assert_eq!(queued_offset(&entries, &added, 2), Some(1));
463+ assert_eq!(queued_offset(&entries, &added, 4), Some(2));
464+ // A dropped entry plays the song after it.
465+ assert_eq!(queued_offset(&entries, &added, 0), Some(0));
466+ assert_eq!(queued_offset(&entries, &added, 3), Some(2));
467+ }
468+
469+ #[test]
470+ fn queued_offset_matches_absolute_paths() {
471+ let entries = ["/music/x.mp3", "gone.mp3", "/music/y.mp3"];
472+ let added = ["x.mp3", "y.mp3"];
473+ assert_eq!(queued_offset(&entries, &added, 2), Some(1));
474+ }
475+}