feat: implement rustify-core media player library#1
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Remove duplicate SeekMode/SeekTo imports in player.rs - Extract AUDIO_EXTENSIONS constant to types.rs (shared by playlist + scanner) - Add Drop impl for Player to send Shutdown on drop - Log audio stream creation failures instead of silently swallowing Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 627584c07b
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| let stream = device | ||
| .build_output_stream( | ||
| &config, | ||
| move |data: &mut [f32], _: &cpal::OutputCallbackInfo| { |
There was a problem hiding this comment.
Handle non-f32 output sample formats
The output callback is hard-coded to &mut [f32], but the stream config is taken from default_output_config() and converted to StreamConfig without branching on the device sample format. On systems where the default output format is i16/u16 (common with ALSA backends), build_output_stream will fail, so playback silently has no audio even though the player instance is created.
Useful? React with 👍 / 👎.
| event_tx | ||
| .send(InternalEvent::Error(format!("open: {e}"))) | ||
| .ok(); | ||
| return; |
There was a problem hiding this comment.
Emit terminal state when track open fails
When opening a track fails, the decode thread sends an Error event and returns, but no terminal event (TrackEnded/state update) is emitted. Since handle_play already set state to Playing, the command loop can remain stuck in Playing with no active decoder, and subsequent play() calls become no-ops until an explicit stop/reset.
Useful? React with 👍 / 👎.
| fn handle_seek(&mut self, ms: u64) { | ||
| if let Some(ref handle) = self.decode_handle { | ||
| handle.control_tx.send(DecodeControl::Seek(ms)).ok(); |
There was a problem hiding this comment.
Flush buffered audio when seeking
handle_seek only forwards DecodeControl::Seek to the decode thread and does not trigger buffer clearing. Because the output callback flushes stale samples only when clear_buffer is set (currently on start/stop), seeking while playing can continue outputting pre-seek audio from the queue for a noticeable interval before the new position is heard.
Useful? React with 👍 / 👎.
On Windows, Path::is_absolute() returns false for /music/song.mp3
(needs drive letter like C:\). Add line.starts_with('/') check so
M3U files with Unix paths work cross-platform. Update test assertions
to use ends_with() instead of exact URI matching.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The decode thread finishes faster than real-time (especially for WAV), so TrackEnded fires while audio is still buffered. Previously, stop_decode() cleared the buffer, cutting off playback. Now when reaching end of tracklist, we let the cpal output thread drain remaining audio naturally. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The output callback was writing stereo samples one-by-one into the device buffer, which scrambles audio on devices with >2 channels (e.g. Yeti X with 8 channels). Now properly maps decoded stereo (L/R) to device channels per-frame, silencing extra channels. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Add #[allow(dead_code)] on Player.music_dirs (used by Python layer) - Replace match with if-let for single-pattern event handling - Use std::io::Error::other() instead of Error::new(ErrorKind::Other) - Allow clippy::should_implement_trait on Tracklist::next() - Remove debug examples and test fixtures - Add Cargo.lock Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ate, seek buffer 1. Force f32 StreamConfig instead of trusting device default format. Devices with i16/u16 defaults (common ALSA) would fail silently. 2. Add DecodeFailed event for early decode thread exits. Previously the player stayed stuck in Playing state with no active decoder when file open/probe/codec init failed. 3. Clear audio buffer on seek so pre-seek samples don't continue playing from the ring buffer after the seek completes. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Summary
rustify-core(pure Rust) +bindings/python(PyO3 cdylib), pyproject.toml, CI workflowRustifyClientpyclass with full transport, tracklist, library, playlist, metadata, and callback APIplay.rsfor hardware testing on Pi~1,900 LOC Rust + 56 unit tests across 8 modules. Replaces Mopidy + GStreamer (~70-80MB) with a single
.sotargeting ~3-4MB RAM.Test plan
cargo test --workspacepasses on dev machinecargo clippy -- -D warningscleanmaturin develop && python -c "from rustify import RustifyClient"smoke testcargo run --example play -- song.mp3🤖 Generated with Claude Code