refactor: organize models and library, include detailed types in track JSON representation
- add basic searching query generators to `Artist`, `Collection`, `Track` - add detailed variants of models - split websocket message types to input/output-specific messages to constrain lifetimes - organize imports
This commit is contained in:
@@ -1,11 +1,12 @@
|
|||||||
use std::{
|
use std::{
|
||||||
error::Error,
|
error::Error,
|
||||||
fs::{self, File},
|
fs,
|
||||||
path::{Path, PathBuf},
|
path::Path,
|
||||||
sync::{Arc, Mutex},
|
sync::{Arc, Mutex},
|
||||||
};
|
};
|
||||||
|
|
||||||
use diesel::{prelude::*, result::Error::NotFound, sqlite::SqliteConnection};
|
use diesel::{prelude::*, sqlite::SqliteConnection};
|
||||||
|
|
||||||
use lofty::{prelude::*, read_from};
|
use lofty::{prelude::*, read_from};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -26,35 +27,14 @@ impl Library {
|
|||||||
Self { conn }
|
Self { conn }
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: implement similar limit/offset/count behavior
|
pub fn conn(&mut self) -> &mut SqliteConnection {
|
||||||
pub fn get_tracks(&mut self) -> QueryResult<Vec<Track>> {
|
&mut self.conn
|
||||||
schema::tracks::table
|
|
||||||
.order(schema::tracks::id.asc())
|
|
||||||
.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 = Artist::all().load(&mut self.conn)?;
|
||||||
.order(schema::artists::id.asc())
|
let collections = Collection::all().load(&mut self.conn)?;
|
||||||
.load::<Artist>(&mut self.conn)?;
|
let tracks = Track::all().load(&mut self.conn)?;
|
||||||
|
|
||||||
let collections = schema::collections::table
|
|
||||||
.order(schema::collections::id.asc())
|
|
||||||
.load::<Collection>(&mut self.conn)?;
|
|
||||||
|
|
||||||
let tracks = schema::tracks::table
|
|
||||||
.order(schema::tracks::id.asc())
|
|
||||||
.load::<Track>(&mut self.conn)?;
|
|
||||||
|
|
||||||
println!("Artists:\n{artists:#?}");
|
println!("Artists:\n{artists:#?}");
|
||||||
println!("Collections:\n{collections:#?}");
|
println!("Collections:\n{collections:#?}");
|
||||||
@@ -131,7 +111,7 @@ impl Importer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn read_metadata(path: &Path) -> Result<ScannedTrackMetadata, TrackScanError> {
|
fn read_metadata(path: &Path) -> Result<ScannedTrackMetadata, TrackScanError> {
|
||||||
let mut file = File::open(path).map_err(|source| TrackScanError::FileOpen {
|
let mut file = fs::File::open(path).map_err(|source| TrackScanError::FileOpen {
|
||||||
path: path.to_path_buf(),
|
path: path.to_path_buf(),
|
||||||
source,
|
source,
|
||||||
})?;
|
})?;
|
||||||
|
|||||||
@@ -1,8 +1,4 @@
|
|||||||
use std::{
|
use std::path::Path;
|
||||||
fs::metadata,
|
|
||||||
path::{Path, PathBuf},
|
|
||||||
string,
|
|
||||||
};
|
|
||||||
|
|
||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
|
|||||||
@@ -1,7 +1,13 @@
|
|||||||
use diesel::prelude::*;
|
use diesel::prelude::*;
|
||||||
|
use diesel::dsl::{AsSelect, Filter, Select, Eq, auto_type};
|
||||||
|
use diesel::sqlite::Sqlite;
|
||||||
|
use futures::task::waker;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::schema::{artists, collections, tracks};
|
use crate::schema::{artists, collections, tracks};
|
||||||
|
use crate::library::Library;
|
||||||
|
|
||||||
|
type All<S, M> = Select<S, AsSelect<M, Sqlite>>;
|
||||||
|
|
||||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||||
#[diesel(table_name = artists)]
|
#[diesel(table_name = artists)]
|
||||||
@@ -12,6 +18,26 @@ pub struct Artist {
|
|||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Artist {
|
||||||
|
pub fn all() -> All<artists::table, Self> {
|
||||||
|
artists::table.select(Self::as_select())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn by_id(id: i32) -> _ {
|
||||||
|
let all: All<artists::table, Self> = Self::all();
|
||||||
|
let filter = artists::id.eq(id);
|
||||||
|
all.filter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn by_original_name(name: &str) -> _ {
|
||||||
|
let all: All<artists::table, Self> = Self::all();
|
||||||
|
let filter = artists::original_name.eq(name);
|
||||||
|
all.filter(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
#[derive(Debug, Clone, Queryable, Selectable, Identifiable)]
|
||||||
#[diesel(table_name = collections)]
|
#[diesel(table_name = collections)]
|
||||||
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
#[diesel(check_for_backend(diesel::sqlite::Sqlite))]
|
||||||
@@ -23,7 +49,26 @@ pub struct Collection {
|
|||||||
pub display_name: String,
|
pub display_name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: make this more ergonomic (nested artist object instead of artist ID, etc)
|
impl Collection {
|
||||||
|
pub fn all() -> All<collections::table, Self> {
|
||||||
|
collections::table.select(Self::as_select())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn by_id(id: i32) -> _ {
|
||||||
|
let all: All<collections::table, Self> = Self::all();
|
||||||
|
let filter = collections::id.eq(id);
|
||||||
|
all.filter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn by_original_name(name: &str) -> _ {
|
||||||
|
let all: All<collections::table, Self> = Self::all();
|
||||||
|
let filter = collections::original_name.eq(name);
|
||||||
|
all.filter(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(
|
#[derive(
|
||||||
Debug, Clone, Queryable, Selectable, Identifiable, Associations, Serialize, Deserialize,
|
Debug, Clone, Queryable, Selectable, Identifiable, Associations, Serialize, Deserialize,
|
||||||
)]
|
)]
|
||||||
@@ -44,6 +89,58 @@ pub struct Track {
|
|||||||
pub length_sec: i32,
|
pub length_sec: i32,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Track {
|
||||||
|
pub fn all() -> All<tracks::table, Self> {
|
||||||
|
tracks::table.select(Self::as_select())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn by_id(id: i32) -> _ {
|
||||||
|
let all: All<tracks::table, Self> = Self::all();
|
||||||
|
let filter = tracks::id.eq(id);
|
||||||
|
all.filter(filter)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[auto_type(no_type_alias)]
|
||||||
|
pub fn by_original_title(title: &str) -> _ {
|
||||||
|
let all: All<tracks::table, Self> = Self::all();
|
||||||
|
let filter = tracks::original_title.eq(title);
|
||||||
|
all.filter(filter)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DetailedArtist {
|
||||||
|
pub id: i32,
|
||||||
|
pub original_name: String,
|
||||||
|
pub display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DetailedCollection<'a> {
|
||||||
|
pub id: i32,
|
||||||
|
pub artist: Option<&'a DetailedArtist>,
|
||||||
|
pub collection_type: String,
|
||||||
|
pub original_name: String,
|
||||||
|
pub display_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Serialize)]
|
||||||
|
pub struct DetailedTrack<'a> {
|
||||||
|
pub id: i32,
|
||||||
|
pub file_path: String,
|
||||||
|
pub artist: Option<&'a DetailedArtist>,
|
||||||
|
// a collection could be under another artist (e.g. mixtapes)
|
||||||
|
pub collection: Option<&'a DetailedCollection<'a>>,
|
||||||
|
pub original_title: Option<String>,
|
||||||
|
pub display_title: Option<String>,
|
||||||
|
pub track_number: Option<i32>,
|
||||||
|
pub genre: Option<String>,
|
||||||
|
pub track_datetime: Option<String>,
|
||||||
|
pub length_sec: i32,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Insertable)]
|
#[derive(Debug, Clone, Insertable)]
|
||||||
#[diesel(table_name = artists)]
|
#[diesel(table_name = artists)]
|
||||||
pub struct NewArtist<'a> {
|
pub struct NewArtist<'a> {
|
||||||
|
|||||||
163
src/net.rs
163
src/net.rs
@@ -1,22 +1,17 @@
|
|||||||
pub mod http {
|
pub mod http {
|
||||||
use std::{
|
use std::{
|
||||||
env, fs,
|
env, fs, io,
|
||||||
io::{self, Read},
|
sync::atomic::{AtomicUsize, Ordering},
|
||||||
sync::{self, atomic},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use diesel::RunQueryDsl;
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use futures::{future, stream, task};
|
use ntex::{server, web};
|
||||||
use ntex::{
|
|
||||||
SharedCfg, server,
|
|
||||||
util::{self, Buf},
|
|
||||||
web,
|
|
||||||
};
|
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
use crate::library;
|
|
||||||
use crate::net::ws;
|
use crate::net::ws;
|
||||||
use crate::stream::AudioFileStream;
|
use crate::stream::AudioFileStream;
|
||||||
|
use crate::{library, models, schema};
|
||||||
|
|
||||||
// This is per-worker state
|
// This is per-worker state
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
@@ -31,7 +26,7 @@ pub mod http {
|
|||||||
|
|
||||||
// This is global application state
|
// This is global application state
|
||||||
struct AppBuilder {
|
struct AppBuilder {
|
||||||
next_worker_id: atomic::AtomicUsize,
|
next_worker_id: AtomicUsize,
|
||||||
library_handle: library::Handle,
|
library_handle: library::Handle,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,7 +34,7 @@ pub mod http {
|
|||||||
// This runs when creating the application, so we pass the database handle into it (which will be cloned later)
|
// This runs when creating the application, so we pass the database handle into it (which will be cloned later)
|
||||||
fn new(library_handle: library::Handle) -> Self {
|
fn new(library_handle: library::Handle) -> Self {
|
||||||
Self {
|
Self {
|
||||||
next_worker_id: atomic::AtomicUsize::new(0),
|
next_worker_id: AtomicUsize::new(0),
|
||||||
library_handle,
|
library_handle,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -55,7 +50,7 @@ pub mod http {
|
|||||||
|
|
||||||
// This runs whenever a worker is created, and uses the global state created on application creation
|
// This runs whenever a worker is created, and uses the global state created on application creation
|
||||||
async fn create(&self) -> io::Result<Self::State> {
|
async fn create(&self) -> io::Result<Self::State> {
|
||||||
let worker_id = self.next_worker_id.fetch_add(1, atomic::Ordering::Relaxed);
|
let worker_id = self.next_worker_id.fetch_add(1, Ordering::Relaxed);
|
||||||
println!("Creating state for worker {worker_id}");
|
println!("Creating state for worker {worker_id}");
|
||||||
|
|
||||||
Ok(AppState {
|
Ok(AppState {
|
||||||
@@ -106,10 +101,15 @@ pub mod http {
|
|||||||
) -> web::HttpResponse {
|
) -> web::HttpResponse {
|
||||||
println!("hit /playback, requesting {}", query.track_id);
|
println!("hit /playback, requesting {}", query.track_id);
|
||||||
|
|
||||||
|
let Ok(track_id) = i32::try_from(query.track_id) else {
|
||||||
|
return web::HttpResponse::BadRequest()
|
||||||
|
.body("track_id exceeds 32-bit unsigned integer limits");
|
||||||
|
};
|
||||||
|
|
||||||
let Ok(track) = state
|
let Ok(track) = state
|
||||||
.library_handle
|
.library_handle
|
||||||
.clone()
|
.clone()
|
||||||
.with_library(|library| library.get_track_by_id(query.track_id))
|
.with_library(|lib| models::Track::by_id(track_id).first(lib.conn()))
|
||||||
else {
|
else {
|
||||||
return web::HttpResponse::NotFound().build();
|
return web::HttpResponse::NotFound().build();
|
||||||
};
|
};
|
||||||
@@ -161,27 +161,50 @@ pub mod http {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod ws {
|
mod ws {
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use diesel::{RunQueryDsl, result::Error as DbError};
|
||||||
use ntex::{util::Bytes, web, web::ws::Message as WsMessage};
|
use ntex::{util::Bytes, web, web::ws::Message as WsMessage};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::{library, models};
|
use crate::{
|
||||||
|
library,
|
||||||
|
models::{self, Artist, Collection, DetailedArtist, DetailedTrack, Track},
|
||||||
|
};
|
||||||
|
|
||||||
// TODO: custom deserializer that tries the id if possible in order to send potential errors back to the correct device
|
// TODO: custom deserializer that tries the id if possible in order to send potential errors back to the correct device
|
||||||
// taking notes from https://github.com/clangen/musikcube/wiki/remote-api-documentation
|
// taking notes from https://github.com/clangen/musikcube/wiki/remote-api-documentation
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Deserialize)]
|
||||||
struct Message {
|
struct InMessage {
|
||||||
pub id: i32,
|
pub id: i32,
|
||||||
pub direction: MessageDirection,
|
pub direction: MessageDirection,
|
||||||
pub params: MessageParams,
|
pub params: InMessageParams,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize)]
|
||||||
|
struct OutMessage<'a> {
|
||||||
|
pub id: i32,
|
||||||
|
pub direction: MessageDirection,
|
||||||
|
pub params: OutMessageParams<'a>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
#[serde(tag = "msg_type")]
|
#[serde(tag = "msg_type")]
|
||||||
enum MessageParams {
|
enum InMessageParams {
|
||||||
Error(ErrorParams),
|
Error(ErrorParams),
|
||||||
|
|
||||||
GetTracks(GetTracks),
|
GetTracks(GetTracks),
|
||||||
GetTracksResponse(GetTracksResponse),
|
|
||||||
|
Ping(()),
|
||||||
|
Pong(()),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
#[serde(tag = "msg_type")]
|
||||||
|
enum OutMessageParams<'a> {
|
||||||
|
Error(ErrorParams),
|
||||||
|
|
||||||
|
GetTracksResponse(GetTracksResponse<'a>),
|
||||||
|
|
||||||
Ping(()),
|
Ping(()),
|
||||||
Pong(()),
|
Pong(()),
|
||||||
@@ -201,12 +224,12 @@ mod ws {
|
|||||||
|
|
||||||
// doesn't take any params for now and queries all tracks period
|
// doesn't take any params for now and queries all tracks period
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
struct GetTracks {}
|
struct GetTracks;
|
||||||
|
|
||||||
// TODO: implement similar limit/offset/count behavior
|
// TODO: implement similar limit/offset/count behavior
|
||||||
#[derive(Serialize, Deserialize)]
|
#[derive(Serialize)]
|
||||||
struct GetTracksResponse {
|
struct GetTracksResponse<'a> {
|
||||||
items: Vec<models::Track>,
|
items: Vec<models::DetailedTrack<'a>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn handle_message(library_handle: &library::Handle, msg_raw: Bytes) -> Option<WsMessage> {
|
pub fn handle_message(library_handle: &library::Handle, msg_raw: Bytes) -> Option<WsMessage> {
|
||||||
@@ -224,25 +247,25 @@ mod ws {
|
|||||||
|
|
||||||
// this only handles incoming message types
|
// this only handles incoming message types
|
||||||
match msg.params {
|
match msg.params {
|
||||||
MessageParams::Ping(_) => generate_pong_response(&msg),
|
InMessageParams::Ping(_) => generate_pong_response(&msg),
|
||||||
MessageParams::GetTracks(ref p) => get_tracks(library_handle, &msg, &p),
|
InMessageParams::GetTracks(ref p) => get_tracks(library_handle, &msg, &p),
|
||||||
_ => generate_error_response("invalid msg_type", None),
|
_ => generate_error_response("invalid msg_type", None),
|
||||||
}
|
}
|
||||||
|
|
||||||
//Some(web::ws::Message::Text("ok".into()))
|
//Some(web::ws::Message::Text("ok".into()))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_message(msg_raw: Bytes) -> Result<Message, serde_json::Error> {
|
fn decode_message(msg_raw: Bytes) -> Result<InMessage, serde_json::Error> {
|
||||||
let msg_str = String::from_utf8_lossy(&msg_raw);
|
let msg_str = String::from_utf8_lossy(&msg_raw);
|
||||||
|
|
||||||
serde_json::from_str::<Message>(&msg_str)
|
serde_json::from_str::<InMessage>(&msg_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_pong_response(msg: &Message) -> Option<WsMessage> {
|
fn generate_pong_response(msg: &InMessage) -> Option<WsMessage> {
|
||||||
let msg = Message {
|
let msg = OutMessage {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
direction: MessageDirection::Response,
|
direction: MessageDirection::Response,
|
||||||
params: MessageParams::Pong(()),
|
params: OutMessageParams::Pong(()),
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload = serde_json::to_string(&msg)
|
let payload = serde_json::to_string(&msg)
|
||||||
@@ -255,10 +278,10 @@ mod ws {
|
|||||||
fn generate_error_response(error_text: &str, id: Option<i32>) -> Option<WsMessage> {
|
fn generate_error_response(error_text: &str, id: Option<i32>) -> Option<WsMessage> {
|
||||||
let id = id.unwrap_or(-1);
|
let id = id.unwrap_or(-1);
|
||||||
|
|
||||||
let msg = Message {
|
let msg = OutMessage {
|
||||||
id,
|
id,
|
||||||
direction: MessageDirection::Response,
|
direction: MessageDirection::Response,
|
||||||
params: MessageParams::Error(ErrorParams {
|
params: OutMessageParams::Error(ErrorParams {
|
||||||
reason: error_text.into(),
|
reason: error_text.into(),
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
@@ -271,20 +294,82 @@ mod ws {
|
|||||||
|
|
||||||
fn get_tracks(
|
fn get_tracks(
|
||||||
library_handle: &library::Handle,
|
library_handle: &library::Handle,
|
||||||
msg: &Message,
|
msg: &InMessage,
|
||||||
params: &GetTracks,
|
params: &GetTracks,
|
||||||
) -> Option<WsMessage> {
|
) -> Option<WsMessage> {
|
||||||
// TODO: perhaps more descriptive errors
|
// TODO: perhaps more descriptive errors
|
||||||
let tracks = library_handle.with_library(|lib| lib.get_tracks());
|
// TODO: this is currently a stub that returns all tracks, artists, and collections, but
|
||||||
|
// eventually should only apply detail additions on top of the filters. it should query
|
||||||
|
// only the artists/collections that appear in the matched tracks/collections this could
|
||||||
|
// probably be separated into its own function as the end of a chain of a post search.
|
||||||
|
let query_results = library_handle.with_library(
|
||||||
|
|lib| -> Result<(Vec<Artist>, Vec<Collection>, Vec<Track>), DbError> {
|
||||||
|
Ok((
|
||||||
|
Artist::all().load(lib.conn())?,
|
||||||
|
Collection::all().load(lib.conn())?,
|
||||||
|
Track::all().load(lib.conn())?,
|
||||||
|
))
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
let Ok(tracks) = tracks else {
|
let Ok((artists, collections, tracks)) = query_results else {
|
||||||
return generate_error_response("error getting tracks from database", Some(msg.id));
|
return generate_error_response("error getting tracks from database", Some(msg.id));
|
||||||
};
|
};
|
||||||
|
|
||||||
let msg = Message {
|
let artists: HashMap<i32, models::DetailedArtist> =
|
||||||
|
HashMap::from_iter(artists.into_iter().map(|artist| {
|
||||||
|
(
|
||||||
|
artist.id,
|
||||||
|
models::DetailedArtist {
|
||||||
|
id: artist.id,
|
||||||
|
original_name: artist.original_name,
|
||||||
|
display_name: artist.display_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
let collections: HashMap<i32, models::DetailedCollection> =
|
||||||
|
HashMap::from_iter(collections.into_iter().map(|collection| {
|
||||||
|
let artist = collection.artist_id.map(|id| artists.get(&id)).flatten();
|
||||||
|
|
||||||
|
(
|
||||||
|
collection.id,
|
||||||
|
models::DetailedCollection {
|
||||||
|
id: collection.id,
|
||||||
|
artist,
|
||||||
|
collection_type: collection.collection_type,
|
||||||
|
original_name: collection.original_name,
|
||||||
|
display_name: collection.display_name,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}));
|
||||||
|
|
||||||
|
let tracks = tracks
|
||||||
|
.into_iter()
|
||||||
|
.map(|track| -> models::DetailedTrack {
|
||||||
|
let artist = track.artist_id.map(|id| artists.get(&id)).flatten();
|
||||||
|
|
||||||
|
let collection = track.collection_id.map(|id| collections.get(&id)).flatten();
|
||||||
|
|
||||||
|
models::DetailedTrack {
|
||||||
|
id: track.id,
|
||||||
|
file_path: track.file_path,
|
||||||
|
artist,
|
||||||
|
collection,
|
||||||
|
original_title: track.original_title,
|
||||||
|
display_title: track.display_title,
|
||||||
|
track_number: track.track_number,
|
||||||
|
genre: track.genre,
|
||||||
|
track_datetime: track.track_datetime,
|
||||||
|
length_sec: track.length_sec,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
let msg = OutMessage {
|
||||||
id: msg.id,
|
id: msg.id,
|
||||||
direction: MessageDirection::Response,
|
direction: MessageDirection::Response,
|
||||||
params: MessageParams::GetTracksResponse(GetTracksResponse { items: tracks }),
|
params: OutMessageParams::GetTracksResponse(GetTracksResponse { items: tracks }),
|
||||||
};
|
};
|
||||||
|
|
||||||
let payload = serde_json::to_string(&msg)
|
let payload = serde_json::to_string(&msg)
|
||||||
|
|||||||
Reference in New Issue
Block a user