Skip to content

Merge rodio player and librespot crates into MuxPlayer#53

Draft
Ovenoboyo wants to merge 3 commits intobazelfrom
player_store_backend
Draft

Merge rodio player and librespot crates into MuxPlayer#53
Ovenoboyo wants to merge 3 commits intobazelfrom
player_store_backend

Conversation

@Ovenoboyo
Copy link
Copy Markdown
Contributor

@Ovenoboyo Ovenoboyo commented Feb 27, 2026

Summary by Sourcery

Introduce a new backend-centric player module that multiplexes playback between implementations, and migrate the existing Rodio-based playback logic and wiring to this shared core player.

New Features:

  • Add a generic PlayerHandler and MuxPlayer to select and delegate to the appropriate playback backend per song.
  • Introduce a source resolution pipeline that can derive and validate playback URLs or file paths for songs before playback.

Enhancements:

  • Refactor the Rodio player into the new core player crate and adapt it to a generic PlayerExt interface with capability probing.
  • Refine the FFMPEG decoder error type naming and conversions for clearer playback error handling.
  • Extend SongsExt with a string representation helper used for error reporting in the player layer.

Build:

  • Replace the old rodio_player Bazel target with a new player crate, wiring in additional sources and dependencies including the Spotify player, and update tests accordingly.
  • Update the Tauri crate dependencies and module list to use the new core player instead of the removed UI-side Rodio integration.

Tests:

  • Rename and repoint the Rust player tests to target the new core player crate rather than the removed rodio_player implementation.

Chores:

  • Remove the Tauri-side Rodio player module and related state management, consolidating playback control into the backend player handler.

This crates a
- mux player (to abstract away all lower player calls)
- Source resolver (to resolve playback urls if missing or invalid)
@Ovenoboyo Ovenoboyo marked this pull request as draft February 27, 2026 05:55
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai bot commented Feb 27, 2026

Reviewer's Guide

Introduces a new core/player crate that encapsulates a generic, multiplexed player abstraction (MuxPlayer + PlayerHandler) and moves rodio-specific functionality and Tauri wiring to the backend, including new source resolution and error types, while refactoring the FFMPEG decoder error type and simplifying Bazel targets.

Class diagram for new core player abstraction and rodio integration

classDiagram
    class PlayerHandler {
        -MuxPlayer mux
        -SourceResolver source_resolver
        +new(f: SourceResolverFn) PlayerHandler
        +load_song(song: Song) Result~void, PlayerError~
        +play() Result~void, PlayerError~
        +pause() Result~void, PlayerError~
        +set_volume(volume: f32) Result~void, PlayerError~
        +seek(position: f64) Result~void, PlayerError~
    }

    class MuxPlayer {
        -Vec~Arc<Box<dyn PlayerExt>>~ players
        -Arc<Box<dyn PlayerExt>> active_player
        +new() MuxPlayer
        +load(song: Song) Result~void, PlayerError~
        +set_active_player(player: Arc<Box<dyn PlayerExt>>) void
        +find_best_player(song: Song) Result~void, PlayerError~
    }

    class PlayerExt {
        <<interface>>
        +play() Result~void, PlayerError~
        +pause() Result~void, PlayerError~
        +stop() Result~void, PlayerError~
        +set_volume(volume: f32) Result~void, PlayerError~
        +seek(position: f64) Result~void, PlayerError~
        +set_src(src: ValidSrc) Result~void, PlayerError~
        +can_play(src: ValidSrc) bool
    }

    class RodioPlayer {
        -Sender~RodioCommand~ tx
        -JoinHandle~()~? handle
        +new() RodioPlayer
        +get_volume() Result~f32, PlayerError~
        +play() Result~void, PlayerError~
        +pause() Result~void, PlayerError~
        +stop() Result~void, PlayerError~
        +set_volume(volume: f32) Result~void, PlayerError~
        +seek(pos: f64) Result~void, PlayerError~
        +set_src(src: ValidSrc) Result~void, PlayerError~
        +can_play(src: ValidSrc) bool
        +set_src_internal(src: String, sink: Arc<rodio::Player>) Result~void, MoosyncError~
    }

    class PlayerError {
        <<enum>>
        NoPlayerFound(details: String)
        NoSrcFound(song: Song)
        PlaybackUrlResolutionFailed(error: Box<dyn Error + Send + Sync>)
        InvalidSong
    }

    class ValidSrc {
        <<enum>>
        Path(path: PathBuf)
        Url(url: String)
        +inner(self) String
    }

    class SourceResolverFn {
        <<type_alias>>
        Fn(song: Song) -> Result~String, Box<dyn Error + Send + Sync>~
    }

    class SourceResolver {
        -SourceResolverFn resolver
        +new(resolver: SourceResolverFn) SourceResolver
        +resolve_playback_url(song: Song) Result~void, PlayerError~
    }

    class Song {
        <<proto_message>>
        +song: Option~InnerSong~
        +get_path() Option~String~
        +get_playback_url() Option~String~
        +to_string() String
    }

    class SongsExt {
        <<trait_implementation>>
        +get_title(self) Option~String~
        +get_path(self) Option~String~
        +get_playback_url(self) Option~String~
        +to_string(self) String
    }

    class DecoderError {
        <<enum>>
        NoAudioStream
        WrongStream(expected: usize, got: i32)
        DurationUnavailable
        Rsmpeg(error: RsmpegError)
        CString(error: NulError)
        AV(code: c_int)
    }

    class FFMPEGDecoder {
        -AVFormatContextInput format_ctx
        -AVCodecContext codec_ctx
        -usize stream_idx
        -Option~SwrContext~ swr_ctx
        -Vec~u8~ current_frame
        -u64 current_frame_idx
        -u64 requested_seek_timestamp
        +open(path: str) Result~FFMPEGDecoder, DecoderError~
        +initialize_swr_context(codec_ctx: AVCodecContext) Result~Option~SwrContext~, DecoderError~
        +convert_and_store_frame(frame: AVFrame) Result~void, DecoderError~
        +decode_next_packet() Result~Option~AVFrame~, DecoderError~
        +process_next_packet() Result~void, DecoderError~
        +resync_after_seek() Result~void, DecoderError~
    }

    PlayerHandler --> MuxPlayer : owns
    PlayerHandler --> SourceResolver : owns

    MuxPlayer --> PlayerExt : uses
    RodioPlayer ..|> PlayerExt
    MuxPlayer --> RodioPlayer : default player

    PlayerHandler --> PlayerError : error_handling
    MuxPlayer --> PlayerError : error_handling
    SourceResolver --> PlayerError : error_handling

    MuxPlayer --> ValidSrc : selection
    RodioPlayer --> ValidSrc : playback_src

    SourceResolver --> SourceResolverFn : wraps
    SourceResolverFn --> Song : input

    Song ..> SongsExt : implements

    FFMPEGDecoder --> DecoderError : error_handling
    RodioPlayer --> FFMPEGDecoder : uses
    DecoderError --> MoosyncError : converts
    DecoderError --> SeekError : converts
Loading

File-Level Changes

Change Details Files
Introduce a new generic player backend (core/player) that multiplexes between multiple concrete players and owns source resolution.
  • Add PlayerHandler as the main backend-facing API that wraps MuxPlayer and SourceResolver, providing load/play/pause/seek/volume operations on songs
  • Implement MuxPlayer to keep a list of PlayerExt implementations (currently RodioPlayer, future Spotify), choose the best player per song via can_play, and delegate playback operations to the active player
  • Add SourceResolver and ValidSrc abstractions to normalize song path/url selection, resolve playback URLs via a pluggable function, and surface PlayerError variants when resolution fails
  • Define PlayerError to capture player- and source-related failure modes (no player, no src, resolution failure, invalid song)
core/player/src/lib.rs
core/player/src/mux_player.rs
core/player/src/source.rs
core/player/src/error.rs
core/player/src/generic.rs
core/player/src/test.rs
Refactor RodioPlayer into the new core/player structure and adapt it to the generic PlayerExt interface and new error/result types.
  • Move RodioPlayer code from core/rodio_player to core/player/src/rodio and update module paths/imports accordingly
  • Change RodioPlayer API from async rodio_* methods to synchronous PlayerExt trait methods (play, pause, stop, seek, set_volume, set_src, can_play) returning Result<_, PlayerError>
  • Add get_volume stub on RodioPlayer returning a PlayerError-aware Result and wire set_src to accept ValidSrc and use its inner string
  • Implement can_play logic that checks filesystem existence for paths and basic scheme checks for URLs
  • Update FFMPEGDecoder::open and related helpers to use a renamed DecoderError type and adjust From conversions and error mapping accordingly
core/player/src/rodio/mod.rs
core/player/src/rodio/decoder.rs
Update build configuration to use the new player crate instead of the old rodio_player crate and clean up obsolete targets.
  • Rename the Bazel rust_library from rodio_player to player, move sources into core/player, and include new modules (error, generic, mux_player, queue, rodio, source, test)
  • Add a cargo_build_script target for the new player library and remove the old rodio_player-specific build/test/binary targets
  • Switch tauri crate dependencies from //core/rodio_player to //core/player and remove references to tauri/src/rodio
core/rodio_player/BUILD
core/player/BUILD
tauri/BUILD
Detach the Tauri frontend from direct rodio control by removing rodio-specific commands and managed state.
  • Remove rodio_* Tauri commands (rodio_load, rodio_play, rodio_pause, rodio_stop, rodio_seek, rodio_set_volume, rodio_get_volume) from the run() command registration list
  • Stop creating and managing rodio_state in the Tauri app state
  • Delete the tauri/src/rodio module which previously exposed rodio player commands to the UI
tauri/src/lib.rs
tauri/src/rodio/mod.rs
Extend Song helper traits to better support error messages and source resolution.
  • Add a to_string helper to SongsExt that formats artists and title for logging/error messages
  • Use SongsExt in the player backend (e.g., for NoPlayerFound diagnostics and source selection)
core/types/src/lib.rs

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • In SourceResolver::resolve_playback_url you always return Err(PlayerError::InvalidSong) even after successfully resolving and setting the playback URL, which means PlayerHandler::load_song will never reach the second mux.load call as intended; this should return Ok(()) (or a more specific result) on success instead.
  • ValidSrc::inner takes ownership of self and Display works around that by cloning, and Path is converted with to_str().unwrap(), which can panic; consider changing inner to take &self and return a Cow<str> or Result<String, _> so you avoid unnecessary cloning and handle non‑UTF8 paths safely.
  • MuxPlayer stores players as Vec<Arc<Box<dyn PlayerExt>>>, which adds an extra layer of indirection; you can simplify this to something like Vec<Arc<dyn PlayerExt + Send + Sync>> (or similar bounds needed by your use sites) to reduce allocation/indirection overhead and make the type easier to work with.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `SourceResolver::resolve_playback_url` you always return `Err(PlayerError::InvalidSong)` even after successfully resolving and setting the playback URL, which means `PlayerHandler::load_song` will never reach the second `mux.load` call as intended; this should return `Ok(())` (or a more specific result) on success instead.
- `ValidSrc::inner` takes ownership of `self` and `Display` works around that by cloning, and `Path` is converted with `to_str().unwrap()`, which can panic; consider changing `inner` to take `&self` and return a `Cow<str>` or `Result<String, _>` so you avoid unnecessary cloning and handle non‑UTF8 paths safely.
- `MuxPlayer` stores players as `Vec<Arc<Box<dyn PlayerExt>>>`, which adds an extra layer of indirection; you can simplify this to something like `Vec<Arc<dyn PlayerExt + Send + Sync>>` (or similar bounds needed by your use sites) to reduce allocation/indirection overhead and make the type easier to work with.

## Individual Comments

### Comment 1
<location path="core/player/src/source.rs" line_range="53-60" />
<code_context>
+    }
+
+    // This method will ignore any existing playback url and path and try to find a new one
+    pub fn resolve_playback_url(&self, song: &mut Song) -> Result<(), PlayerError> {
+        let playback_url =
+            (self.resolver)(song).map_err(|e| PlayerError::PlaybackUrlResolutionFailed(e))?;
+
+        if let Some(inner_song) = song.song.as_mut() {
+            inner_song.playback_url = Some(playback_url);
+        };
+        Err(PlayerError::InvalidSong)
+    }
+}
</code_context>
<issue_to_address>
**issue (bug_risk):** resolve_playback_url always returns an error, even after successfully setting playback_url.

The function currently sets `playback_url` (when `song.song` is `Some`) but then always returns `Err(PlayerError::InvalidSong)`, so `PlayerHandler::load_song` will always treat resolution as a failure.

This should return `Ok(())` when a URL is successfully set, and only return `InvalidSong` when `song.song` is `None` (or other validation fails). For example:

```rust
pub fn resolve_playback_url(&self, song: &mut Song) -> Result<(), PlayerError> {
    let playback_url = (self.resolver)(song)
        .map_err(PlayerError::PlaybackUrlResolutionFailed)?;

    if let Some(inner_song) = song.song.as_mut() {
        inner_song.playback_url = Some(playback_url);
        Ok(())
    } else {
        Err(PlayerError::InvalidSong)
    }
}
```
</issue_to_address>

### Comment 2
<location path="core/player/src/source.rs" line_range="21-24" />
<code_context>
+}
+
+impl ValidSrc {
+    pub(crate) fn inner(self) -> String {
+        match self {
+            ValidSrc::Path(path) => path.to_str().unwrap().to_string(),
+            ValidSrc::Url(url) => url,
+        }
+    }
</code_context>
<issue_to_address>
**suggestion (bug_risk):** ValidSrc::inner consumes self, forcing clones in Display and panicking on non-UTF8 paths.

Because `inner` takes ownership, the `Display` impl has to call `self.clone().inner()`, which adds unnecessary cloning for a read-only operation. In addition, `PathBuf::to_str().unwrap()` can panic on non-UTF8 paths.

You can avoid both issues by borrowing and using a lossless conversion:
```rust
pub(crate) fn inner(&self) -> String {
    match self {
        ValidSrc::Path(path) => path.to_string_lossy().into_owned(),
        ValidSrc::Url(url) => url.clone(),
    }
}

impl Display for ValidSrc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner())
    }
}
```

Suggested implementation:

```rust
impl Display for ValidSrc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner())
    }
}

impl ValidSrc {

```

```rust
impl Display for ValidSrc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner())
    }
}

impl ValidSrc {
    pub(crate) fn inner(&self) -> String {
        match self {
            ValidSrc::Path(path) => path.to_string_lossy().into_owned(),
            ValidSrc::Url(url) => url.clone(),
        }
    }

```
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +53 to +60
pub fn resolve_playback_url(&self, song: &mut Song) -> Result<(), PlayerError> {
let playback_url =
(self.resolver)(song).map_err(|e| PlayerError::PlaybackUrlResolutionFailed(e))?;

if let Some(inner_song) = song.song.as_mut() {
inner_song.playback_url = Some(playback_url);
};
Err(PlayerError::InvalidSong)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): resolve_playback_url always returns an error, even after successfully setting playback_url.

The function currently sets playback_url (when song.song is Some) but then always returns Err(PlayerError::InvalidSong), so PlayerHandler::load_song will always treat resolution as a failure.

This should return Ok(()) when a URL is successfully set, and only return InvalidSong when song.song is None (or other validation fails). For example:

pub fn resolve_playback_url(&self, song: &mut Song) -> Result<(), PlayerError> {
    let playback_url = (self.resolver)(song)
        .map_err(PlayerError::PlaybackUrlResolutionFailed)?;

    if let Some(inner_song) = song.song.as_mut() {
        inner_song.playback_url = Some(playback_url);
        Ok(())
    } else {
        Err(PlayerError::InvalidSong)
    }
}

Comment on lines +21 to +24
pub(crate) fn inner(self) -> String {
match self {
ValidSrc::Path(path) => path.to_str().unwrap().to_string(),
ValidSrc::Url(url) => url,
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (bug_risk): ValidSrc::inner consumes self, forcing clones in Display and panicking on non-UTF8 paths.

Because inner takes ownership, the Display impl has to call self.clone().inner(), which adds unnecessary cloning for a read-only operation. In addition, PathBuf::to_str().unwrap() can panic on non-UTF8 paths.

You can avoid both issues by borrowing and using a lossless conversion:

pub(crate) fn inner(&self) -> String {
    match self {
        ValidSrc::Path(path) => path.to_string_lossy().into_owned(),
        ValidSrc::Url(url) => url.clone(),
    }
}

impl Display for ValidSrc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner())
    }
}

Suggested implementation:

impl Display for ValidSrc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner())
    }
}

impl ValidSrc {
impl Display for ValidSrc {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}", self.inner())
    }
}

impl ValidSrc {
    pub(crate) fn inner(&self) -> String {
        match self {
            ValidSrc::Path(path) => path.to_string_lossy().into_owned(),
            ValidSrc::Url(url) => url.clone(),
        }
    }

@Ovenoboyo Ovenoboyo changed the title Move player store from UI to backend Merge rodio player and librespot crates into MuxPlayer Mar 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant