Skip to content

Commit

Permalink
get queue API working, add first tests
Browse files Browse the repository at this point in the history
- Make the queue API return the current queue
- Add JSON errors for e.g. 404
- Add once_cell and refactor build_test_app to fix Sled locking problems
- Write first queue API tests, needs some more.
  • Loading branch information
werkshy committed Apr 16, 2024
1 parent d018b60 commit 8d99e72
Show file tree
Hide file tree
Showing 12 changed files with 123 additions and 53 deletions.
1 change: 1 addition & 0 deletions rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions rs/Cargo.toml
Expand Up @@ -20,6 +20,7 @@ walkdir = "^2"
rand = "^0"
regex = "^1"
lazy_static = "^1"
once_cell = "1.19.0"

[dev-dependencies]
assert_matches = "^1"
Expand Down
3 changes: 2 additions & 1 deletion rs/src/api/list.rs
Expand Up @@ -4,6 +4,7 @@ use serde::Serialize;

use crate::app_state::AppState;

// TODO fill this out
#[derive(Debug, Serialize)]
struct ApiCategory {
name: String,
Expand All @@ -20,7 +21,7 @@ pub struct ApiTrack {
}

impl ApiTrack {
fn from_track(track: &crate::filemanager::model::Track) -> Self {
pub fn from_track(track: &crate::filemanager::model::Track) -> Self {
ApiTrack {
id: track.id.clone(),
title: track.name.clone(),
Expand Down
50 changes: 22 additions & 28 deletions rs/src/api/queue.rs
@@ -1,28 +1,11 @@
use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::{
get, http::StatusCode, post, web, Responder, Result,
};
use serde::{Deserialize, Serialize};

use crate::{api::list::ApiTrack, app_state::AppState, filemanager::model::Track};
use crate::error::AppError;
use crate::filemanager::dto::CollectionLocation;

fn get_first_track(app_state: &AppState) -> &Track {
// TODO for now let's just look for the first track, it seems to work on our demo music
return app_state
.collection
.values()
.next()
.unwrap()
.artists
.values()
.next()
.unwrap()
.albums
.values()
.next()
.unwrap()
.tracks
.first()
.unwrap();
}
use crate::{api::list::ApiTrack, app_state::AppState};

// TODO extract 'ApiCollectionLocaltion out of here
#[derive(Deserialize, Debug)]
Expand All @@ -43,18 +26,24 @@ struct ApiQueueResponse {
}

#[post("/queue/add")]
pub async fn add(data: web::Data<AppState>, input: web::Json<ApiQueueInput>) -> impl Responder {
pub async fn add(
data: web::Data<AppState>,
input: web::Json<ApiQueueInput>,
) -> Result<impl Responder, AppError> {
let collection_location = CollectionLocation {
category: input.category.clone(),
artist: input.artist.clone(),
album: input.album.clone(),
disc: input.track.clone(),
disc: input.disc.clone(),
track: input.track.clone(),
};

let maybe_tracks = data.collection.get_tracks_under(&collection_location);
if maybe_tracks.is_none() {
return HttpResponse::NotFound().body("Not found");
return Err(AppError::new(
"No matching music found in the collection",
StatusCode::NOT_FOUND,
));
}
let tracks = maybe_tracks.unwrap();

Expand All @@ -64,12 +53,17 @@ pub async fn add(data: web::Data<AppState>, input: web::Json<ApiQueueInput>) ->
queue.clear();
}
queue.add_tracks(tracks.into_iter().cloned().collect());
HttpResponse::Ok().body("ok")
Ok(web::Json(ApiQueueResponse {
tracks: queue.tracks.iter().map(ApiTrack::from_track).collect(),
position: queue.position,
}))
}

#[get("/queue")]
pub async fn get_queue(data: web::Data<AppState>) -> impl Responder {
let queue = data.queue.lock().unwrap();
queue.print_tracks();
HttpResponse::Ok().body("ok")
web::Json(ApiQueueResponse {
tracks: queue.tracks.iter().map(ApiTrack::from_track).collect(),
position: queue.position,
})
}
31 changes: 31 additions & 0 deletions rs/src/error.rs
@@ -0,0 +1,31 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError};
use std::fmt;

#[derive(Debug)]
pub struct AppError {
pub msg: String,
pub status: StatusCode,
}

impl AppError {
pub fn new(msg: &str, status: StatusCode) -> AppError {
AppError {
msg: msg.to_string(),
status,
}
}
}

impl fmt::Display for AppError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{:?}", self)
}
}

impl ResponseError for AppError {
// builds the actual response to send back when an error occurs
fn error_response(&self) -> HttpResponse {
let err_json = serde_json::json!({ "error": self.msg });
HttpResponse::build(self.status).json(err_json)
}
}
19 changes: 5 additions & 14 deletions rs/src/filemanager/collection.rs
@@ -1,22 +1,13 @@
use std::{
collections::{BTreeMap},
};



use std::collections::BTreeMap;

use super::{
cache,
collection_builder::{build},
collection_builder::build,
dto::CollectionLocation,
model::{Category, Track},
options::CollectionOptions,
};

const DEFAULT_CATEGORY: &str = "Music";
const CATEGORY_PREFIX: &str = "_";
const CD_REGEX_STR: &str = r"(?i)(cd|dis(c|k)) ?\d";

pub struct Collection {
pub categories: BTreeMap<String, Category>,
}
Expand Down Expand Up @@ -107,7 +98,8 @@ impl Collection {
return maybe_tracks.and_then(|tracks| {
tracks
.iter()
.find(|track| track.name == *location.track.as_ref().unwrap()).map(|track| vec![track.clone()])
.find(|track| track.name == *location.track.as_ref().unwrap())
.map(|track| vec![*track])
});
}
}
Expand All @@ -116,9 +108,8 @@ impl Collection {
mod tests {
use super::*;
use crate::filemanager::model::factories::*;

use factori::create;


#[test]
fn test_add_category() {
Expand Down
6 changes: 1 addition & 5 deletions rs/src/filemanager/collection_builder.rs
@@ -1,8 +1,4 @@
use std::{
borrow::Cow,
collections::{VecDeque},
path::PathBuf,
};
use std::{borrow::Cow, collections::VecDeque, path::PathBuf};

use lazy_static::lazy_static;
use regex::Regex;
Expand Down
2 changes: 1 addition & 1 deletion rs/src/filemanager/list.rs
Expand Up @@ -41,7 +41,7 @@ fn list_album(album: &Album, indent: usize) {
let space = " ".repeat(indent);
log::info!("{space}[al]{}", album.name);

for (_disc_name, disc) in &album.discs {
for disc in album.discs.values() {
list_album(disc, indent + 2);
}

Expand Down
1 change: 0 additions & 1 deletion rs/src/filemanager/model.rs
Expand Up @@ -149,7 +149,6 @@ pub mod factories {
});

pub fn random_string() -> String {

thread_rng()
.sample_iter(&Alphanumeric)
.take(30)
Expand Down
1 change: 1 addition & 0 deletions rs/src/lib.rs
@@ -1,5 +1,6 @@
pub mod api;
pub mod app_state;
pub mod error;
pub mod filemanager;
pub mod player;
pub mod queue;
Expand Down
12 changes: 9 additions & 3 deletions rs/tests/helpers.rs
Expand Up @@ -4,7 +4,10 @@ use actix_web::{
web::Data,
App, Error,
};
use pickup::{build_app, build_app_state, filemanager::options::CollectionOptions, ServeOptions};
use pickup::{
app_state::AppState, build_app, build_app_state, filemanager::options::CollectionOptions,
ServeOptions,
};

pub fn build_test_app() -> App<
impl ServiceFactory<
Expand All @@ -15,6 +18,10 @@ pub fn build_test_app() -> App<
Error = Error,
>,
> {
build_app(build_test_app_state())
}

pub fn build_test_app_state() -> Data<AppState> {
let options = ServeOptions {
collection_options: CollectionOptions {
dir: String::from("../music"),
Expand All @@ -23,6 +30,5 @@ pub fn build_test_app() -> App<
port: 3001,
};

let app_state = Data::new(build_app_state(&options));
build_app(app_state)
Data::new(build_app_state(&options))
}
49 changes: 49 additions & 0 deletions rs/tests/queue_e2e.rs
@@ -0,0 +1,49 @@
use actix_web::{
test::{self, read_body_json},
web::Data,
};

mod helpers;

use helpers::build_test_app_state;
use once_cell::sync::Lazy;
use pickup::{app_state::AppState, build_app};
use serde_json::{self, json};

// Make a shared app state for use in all tests, because Sled won't let us open multiple instances of the DB file
// from differet threads.
static APP_STATE: Lazy<Data<AppState>> = Lazy::new(build_test_app_state);

#[actix_web::test]
async fn test_not_found() {
let app = test::init_service(build_app((*APP_STATE).clone())).await;
let req = test::TestRequest::post()
.uri("/queue/add")
.set_json(json!({"category": "DoesNotExist"}))
.to_request();

let resp = test::call_service(&app, req).await;

assert_eq!(resp.status(), actix_web::http::StatusCode::NOT_FOUND);
let resp_json: serde_json::Value = read_body_json(resp).await;
assert_eq!(
resp_json,
json!({ "error": "No matching music found in the collection" })
);
}

#[actix_web::test]
async fn test_add_artist() {
let app = test::init_service(build_app((*APP_STATE).clone())).await;
let req = test::TestRequest::post()
.uri("/queue/add")
.set_json(json!({
"category": "Music",
}))
.to_request();

let resp: serde_json::Value = test::call_and_read_body_json(&app, req).await;

assert_eq!(resp.get("position").unwrap().as_u64().unwrap(), 0);
assert_eq!(resp.get("tracks").unwrap().as_array().unwrap().len(), 5);
}

0 comments on commit 8d99e72

Please sign in to comment.