feat: add basic chunked audio send in /playback endpoint
- add `Library::get_track_by_id()` - add futures as a dependency
This commit is contained in:
@@ -7,6 +7,7 @@ edition = "2024"
|
|||||||
diesel = { version = "2.3.14", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] }
|
diesel = { version = "2.3.14", features = ["sqlite", "returning_clauses_for_sqlite_3_35"] }
|
||||||
diesel_migrations = { version = "2.3.2", features = ["sqlite"] }
|
diesel_migrations = { version = "2.3.2", features = ["sqlite"] }
|
||||||
dotenvy = "0.15.7"
|
dotenvy = "0.15.7"
|
||||||
|
futures = "0.3.34"
|
||||||
lofty = "0.25.4"
|
lofty = "0.25.4"
|
||||||
ntex = { version = "4.4.0", features = ["tokio"] }
|
ntex = { version = "4.4.0", features = ["tokio"] }
|
||||||
serde = { version = "1.0.229", features = ["derive"] }
|
serde = { version = "1.0.229", features = ["derive"] }
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use std::{
|
|||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use diesel::{prelude::*, sqlite::SqliteConnection};
|
use diesel::{prelude::*, result::Error::NotFound, sqlite::SqliteConnection};
|
||||||
use lofty::{prelude::*, read_from};
|
use lofty::{prelude::*, read_from};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -33,6 +33,16 @@ impl Library {
|
|||||||
.load::<Track>(&mut self.conn)
|
.load::<Track>(&mut self.conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_track_by_id(&mut self, id: u32) -> QueryResult<Track> {
|
||||||
|
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<()> {
|
pub fn print(&mut self) -> QueryResult<()> {
|
||||||
let artists = schema::artists::table
|
let artists = schema::artists::table
|
||||||
.order(schema::artists::id.asc())
|
.order(schema::artists::id.asc())
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ mod library;
|
|||||||
mod models;
|
mod models;
|
||||||
mod net;
|
mod net;
|
||||||
mod schema;
|
mod schema;
|
||||||
|
mod stream;
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
use library::{Handle, Importer, Library};
|
use library::{Handle, Importer, Library};
|
||||||
|
|||||||
49
src/net.rs
49
src/net.rs
@@ -1,14 +1,22 @@
|
|||||||
pub mod http {
|
pub mod http {
|
||||||
use std::{
|
use std::{
|
||||||
env, io,
|
env, fs,
|
||||||
|
io::{self, Read},
|
||||||
sync::{self, atomic},
|
sync::{self, atomic},
|
||||||
};
|
};
|
||||||
|
|
||||||
use dotenvy::dotenv;
|
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::library;
|
||||||
use crate::net::ws;
|
use crate::net::ws;
|
||||||
|
use crate::stream::AudioFileStream;
|
||||||
|
|
||||||
// This is per-worker state
|
// This is per-worker state
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -37,6 +45,11 @@ pub mod http {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct PlaybackQuery {
|
||||||
|
track_id: u32,
|
||||||
|
}
|
||||||
|
|
||||||
impl server::ServerAppConfig for AppBuilder {
|
impl server::ServerAppConfig for AppBuilder {
|
||||||
type State = AppState;
|
type State = AppState;
|
||||||
|
|
||||||
@@ -85,6 +98,37 @@ pub mod http {
|
|||||||
web::HttpResponse::Ok().body("websocket connected")
|
web::HttpResponse::Ok().body("websocket connected")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn playback(
|
||||||
|
state: &AppState,
|
||||||
|
_: (),
|
||||||
|
req: web::HttpRequest,
|
||||||
|
query: web::types::Query<PlaybackQuery>,
|
||||||
|
) -> 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]
|
#[ntex::main]
|
||||||
pub async fn start(library_handle: library::Handle) -> std::io::Result<()> {
|
pub async fn start(library_handle: library::Handle) -> std::io::Result<()> {
|
||||||
dotenv().ok();
|
dotenv().ok();
|
||||||
@@ -107,6 +151,7 @@ pub mod http {
|
|||||||
web::App::new()
|
web::App::new()
|
||||||
.service(web::resource("/").to(index))
|
.service(web::resource("/").to(index))
|
||||||
.service(web::resource("/ws").to_with_state(websocket))
|
.service(web::resource("/ws").to_with_state(websocket))
|
||||||
|
.service(web::resource("/playback").to_with_state(playback))
|
||||||
})
|
})
|
||||||
.bind((address, port), ntex::SharedCfg::new("STATE"))?
|
.bind((address, port), ntex::SharedCfg::new("STATE"))?
|
||||||
.workers(2)
|
.workers(2)
|
||||||
|
|||||||
52
src/stream.rs
Normal file
52
src/stream.rs
Normal file
@@ -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<fs::File>,
|
||||||
|
poll_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AudioFileStream {
|
||||||
|
pub fn from_file_buffer(buffer: io::BufReader<fs::File>) -> Self {
|
||||||
|
println!(
|
||||||
|
"creating AudioFileStream from buffer, {}",
|
||||||
|
buffer.buffer().len()
|
||||||
|
);
|
||||||
|
|
||||||
|
Self {
|
||||||
|
file_buffer: buffer,
|
||||||
|
poll_count: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl stream::Stream for AudioFileStream {
|
||||||
|
type Item = Result<util::Bytes, io::Error>;
|
||||||
|
|
||||||
|
fn poll_next(
|
||||||
|
mut self: pin::Pin<&mut Self>,
|
||||||
|
cx: &mut task::Context<'_>,
|
||||||
|
) -> task::Poll<Option<Self::Item>> {
|
||||||
|
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)))
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user