From 5836f671d66d35c150901636efa9695a6c04c7a2 Mon Sep 17 00:00:00 2001 From: skybldev Date: Thu, 29 Oct 2026 21:35:15 -0400 Subject: [PATCH] feat: add basic chunked audio send in /playback endpoint - add `Library::get_track_by_id()` - add futures as a dependency --- Cargo.toml | 1 + src/library.rs | 12 +++++++++++- src/main.rs | 1 + src/net.rs | 49 +++++++++++++++++++++++++++++++++++++++++++++-- src/stream.rs | 52 ++++++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 src/stream.rs diff --git a/Cargo.toml b/Cargo.toml index 3b523cd..b6172b5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,6 +7,7 @@ edition = "2024" diesel = { version = "2.3.14", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] } diesel_migrations = { version = "2.3.2", features = ["sqlite"] } dotenvy = "0.15.7" +futures = "0.3.34" lofty = "0.25.4" ntex = { version = "4.4.0", features = ["tokio"] } serde = { version = "1.0.229", features = ["derive"] } diff --git a/src/library.rs b/src/library.rs index a8d8fc6..88e12bb 100644 --- a/src/library.rs +++ b/src/library.rs @@ -5,7 +5,7 @@ use std::{ sync::{Arc, Mutex}, }; -use diesel::{prelude::*, sqlite::SqliteConnection}; +use diesel::{prelude::*, result::Error::NotFound, sqlite::SqliteConnection}; use lofty::{prelude::*, read_from}; use crate::{ @@ -33,6 +33,16 @@ impl Library { .load::(&mut self.conn) } + pub fn get_track_by_id(&mut self, id: u32) -> QueryResult { + let Ok(id) = i32::try_from(id) else { + return Err(NotFound); + }; + + schema::tracks::table + .find(i32::from(id)) + .first(&mut self.conn) + } + pub fn print(&mut self) -> QueryResult<()> { let artists = schema::artists::table .order(schema::artists::id.asc()) diff --git a/src/main.rs b/src/main.rs index 08774c3..5b665e0 100644 --- a/src/main.rs +++ b/src/main.rs @@ -12,6 +12,7 @@ mod library; mod models; mod net; mod schema; +mod stream; mod types; use library::{Handle, Importer, Library}; diff --git a/src/net.rs b/src/net.rs index 008e843..7f84dcc 100644 --- a/src/net.rs +++ b/src/net.rs @@ -1,14 +1,22 @@ pub mod http { use std::{ - env, io, + env, fs, + io::{self, Read}, sync::{self, atomic}, }; use dotenvy::dotenv; - use ntex::{SharedCfg, server, web}; + use futures::{future, stream, task}; + use ntex::{ + SharedCfg, server, + util::{self, Buf}, + web, + }; + use serde::Deserialize; use crate::library; use crate::net::ws; + use crate::stream::AudioFileStream; // This is per-worker state #[derive(Clone)] @@ -37,6 +45,11 @@ pub mod http { } } + #[derive(Deserialize)] + struct PlaybackQuery { + track_id: u32, + } + impl server::ServerAppConfig for AppBuilder { type State = AppState; @@ -85,6 +98,37 @@ pub mod http { web::HttpResponse::Ok().body("websocket connected") } + async fn playback( + state: &AppState, + _: (), + req: web::HttpRequest, + query: web::types::Query, + ) -> web::HttpResponse { + println!("hit /playback, requesting {}", query.track_id); + + let Ok(track) = state + .library_handle + .clone() + .with_library(|library| library.get_track_by_id(query.track_id)) + else { + return web::HttpResponse::NotFound().build(); + }; + + println!("opening {}", track.file_path); + + let file_handle = + fs::File::open(&track.file_path).expect("whoops so the file isn't there i guess"); + + println!(" opened {}", &track.file_path); + + let file_buffer = io::BufReader::new(file_handle); + let file_stream = AudioFileStream::from_file_buffer(file_buffer); + + web::HttpResponse::Ok() + .content_type("application/ogg") + .streaming(file_stream) + } + #[ntex::main] pub async fn start(library_handle: library::Handle) -> std::io::Result<()> { dotenv().ok(); @@ -107,6 +151,7 @@ pub mod http { web::App::new() .service(web::resource("/").to(index)) .service(web::resource("/ws").to_with_state(websocket)) + .service(web::resource("/playback").to_with_state(playback)) }) .bind((address, port), ntex::SharedCfg::new("STATE"))? .workers(2) diff --git a/src/stream.rs b/src/stream.rs new file mode 100644 index 0000000..5340ff8 --- /dev/null +++ b/src/stream.rs @@ -0,0 +1,52 @@ +use std::{fs, io, pin, task}; + +use futures::stream; +use ntex::util::{self, Buf}; + +pub struct AudioFileStream { + file_buffer: io::BufReader, + poll_count: u64, +} + +impl AudioFileStream { + pub fn from_file_buffer(buffer: io::BufReader) -> Self { + println!( + "creating AudioFileStream from buffer, {}", + buffer.buffer().len() + ); + + Self { + file_buffer: buffer, + poll_count: 0, + } + } +} + +impl stream::Stream for AudioFileStream { + type Item = Result; + + fn poll_next( + mut self: pin::Pin<&mut Self>, + cx: &mut task::Context<'_>, + ) -> task::Poll> { + self.poll_count += 1; + + let Ok(view) = io::BufRead::fill_buf(&mut self.file_buffer) else { + eprintln!("failed to continue filling buffer! poll_count {}", self.poll_count); + return task::Poll::Ready(None); + }; + + let out = util::Bytes::copy_from_slice(view); + + if out.len() == 0 { + return task::Poll::Ready(None); + } + + if let Err(e) = self.file_buffer.seek_relative(8192) { + eprintln!("failed to seek buffer! poll_count {}", self.poll_count); + return task::Poll::Pending; + } + + task::Poll::Ready(Some(Ok(out))) + } +}