diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 45ceba00..69b0572f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -90,7 +90,7 @@ jobs: key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - name: Install developer package dependencies - run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev + run: sudo apt-get update && sudo apt install -y libunwind-dev && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev - run: cargo build --workspace --examples - run: cargo test --workspace @@ -231,7 +231,7 @@ jobs: key: ${{ runner.os }}-${{ steps.get-rustc-version.outputs.version }}-${{ hashFiles('Cargo.lock') }} - name: Install developer package dependencies - run: sudo apt-get update && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev + run: sudo apt-get update && sudo apt install -y libunwind-dev && sudo apt-get install libpulse-dev portaudio19-dev libasound2-dev libsdl2-dev gstreamer1.0-dev libgstreamer-plugins-base1.0-dev libavahi-compat-libdnssd-dev - run: cargo install cargo-hack - run: cargo hack --workspace --remove-dev-deps diff --git a/CHANGELOG.md b/CHANGELOG.md index c7ae5e98..1903db1f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -84,6 +84,7 @@ https://github.com/librespot-org/librespot It supports a lot of functionality, including audio previews and image downloads even if librespot doesn't use that for playback itself. - [core] Support downloading of lyrics +- [core] Support parsing `SpotifyId` for local files - [main] Add all player events to `player_event_handler.rs` - [main] Add an event worker thread that runs async to the main thread(s) but sync to itself to prevent potential data races for event consumers diff --git a/core/src/spotify_id.rs b/core/src/spotify_id.rs index 3e913ce1..5057c135 100644 --- a/core/src/spotify_id.rs +++ b/core/src/spotify_id.rs @@ -21,6 +21,7 @@ pub enum SpotifyItemType { Playlist, Show, Track, + Local, Unknown, } @@ -33,6 +34,7 @@ impl From<&str> for SpotifyItemType { "playlist" => Self::Playlist, "show" => Self::Show, "track" => Self::Track, + "local" => Self::Local, _ => Self::Unknown, } } @@ -47,6 +49,7 @@ impl From for &str { SpotifyItemType::Playlist => "playlist", SpotifyItemType::Show => "show", SpotifyItemType::Track => "track", + SpotifyItemType::Local => "local", _ => "unknown", } } @@ -167,24 +170,30 @@ impl SpotifyId { /// /// [Spotify URI]: https://developer.spotify.com/documentation/web-api/#spotify-uris-and-ids pub fn from_uri(src: &str) -> SpotifyIdResult { - let mut uri_parts: Vec<&str> = src.split(':').collect(); - // At minimum, should be `spotify:{type}:{id}` - if uri_parts.len() < 3 { - return Err(SpotifyIdError::InvalidFormat.into()); - } + let (scheme, tail) = src.split_once(':').ok_or(SpotifyIdError::InvalidFormat)?; + let (item_type, id) = tail.split_once(':').ok_or(SpotifyIdError::InvalidFormat)?; - if uri_parts[0] != "spotify" { + if scheme != "spotify" { return Err(SpotifyIdError::InvalidRoot.into()); } - let id = uri_parts.pop().unwrap_or_default(); + let item_type = item_type.into(); + + // Local files have a variable-length ID: https://developer.spotify.com/documentation/general/guides/local-files-spotify-playlists/ + // TODO: find a way to add this local file ID to SpotifyId. + // One possible solution would be to copy the contents of `id` to a new String field in SpotifyId, + // but then we would need to remove the derived Copy trait, which would be a breaking change. + if item_type == SpotifyItemType::Local { + return Ok(Self { item_type, id: 0 }); + } + if id.len() != Self::SIZE_BASE62 { return Err(SpotifyIdError::InvalidId.into()); } Ok(Self { - item_type: uri_parts.pop().unwrap_or_default().into(), + item_type, ..Self::from_base62(id)? }) } @@ -534,7 +543,7 @@ mod tests { raw: &'static [u8], } - static CONV_VALID: [ConversionCase; 4] = [ + static CONV_VALID: [ConversionCase; 5] = [ ConversionCase { id: 238762092608182713602505436543891614649, kind: SpotifyItemType::Track, @@ -575,6 +584,14 @@ mod tests { 154, 27, 28, 251, 198, 242, 68, 86, 154, 224, 53, 108, 119, 187, 233, 216, ], }, + ConversionCase { + id: 0, + kind: SpotifyItemType::Local, + uri: "spotify:local:0000000000000000000000", + base16: "00000000000000000000000000000000", + base62: "0000000000000000000000", + raw: &[0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + }, ]; static CONV_INVALID: [ConversionCase; 3] = [ @@ -676,6 +693,14 @@ mod tests { } } + #[test] + fn from_local_uri() { + let actual = SpotifyId::from_uri("spotify:local:xyz:123").unwrap(); + + assert_eq!(actual.id, 0); + assert_eq!(actual.item_type, SpotifyItemType::Local); + } + #[test] fn to_uri() { for c in &CONV_VALID {