diff --git a/Cargo.lock b/Cargo.lock index 03513f7..1085eba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -653,6 +653,16 @@ version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77" +[[package]] +name = "fid-rs" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a6956a1e60e2d1412b44b4169d44a03dae518f8583d3e10090c912c105e48447" +dependencies = [ + "rayon", + "serde", +] + [[package]] name = "flate2" version = "1.0.27" @@ -1178,6 +1188,16 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "louds-rs" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "936de6c22f08e7135a921f8ada907acd0d88880c4f42b5591f634b9f1dd8e07f" +dependencies = [ + "fid-rs", + "serde", +] + [[package]] name = "matches" version = "0.1.10" @@ -1631,6 +1651,7 @@ dependencies = [ "tokio-util", "toml 0.8.14", "tower-http", + "trie-rs", "ureq 2.10.0", "url", "winres", @@ -2818,6 +2839,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "trie-rs" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6f88f4b0a1ebd6c3d16be3e45eb0e8089372ccadd88849b7ca162ba64b5e6f6" +dependencies = [ + "louds-rs", + "serde", +] + [[package]] name = "try-lock" version = "0.2.5" diff --git a/Cargo.toml b/Cargo.toml index 14eb951..f9423f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -43,6 +43,7 @@ tokio = { version = "1.38", features = ["macros", "rt-multi-thread"] } tokio-util = { version = "0.7.11", features = ["io"] } toml = "0.8.14" tower-http = { version = "0.5.2", features = ["fs"] } +trie-rs = { version = "0.4.2", features = ["serde"] } ureq = "2.10.0" url = "2.3" diff --git a/src/app/collection/browser.rs b/src/app/collection/browser.rs index e932a18..add8c51 100644 --- a/src/app/collection/browser.rs +++ b/src/app/collection/browser.rs @@ -14,20 +14,6 @@ impl Browser { Self { db, vfs_manager } } - pub async fn browse

(&self, path: P) -> Result, collection::Error> - where - P: AsRef, - { - todo!(); - } - - pub async fn flatten

(&self, path: P) -> Result, collection::Error> - where - P: AsRef, - { - todo!(); - } - pub async fn search(&self, query: &str) -> Result, collection::Error> { todo!(); } diff --git a/src/app/collection/index.rs b/src/app/collection/index.rs index 7d1c2a3..eae8a0e 100644 --- a/src/app/collection/index.rs +++ b/src/app/collection/index.rs @@ -10,6 +10,7 @@ use log::{error, info}; use rand::{rngs::ThreadRng, seq::IteratorRandom}; use serde::{Deserialize, Serialize}; use tokio::task::spawn_blocking; +use trie_rs::{Trie, TrieBuilder}; use crate::{app::collection, db::DB}; @@ -23,7 +24,7 @@ impl IndexManager { pub async fn new(db: DB) -> Self { let mut index_manager = Self { db, - index: Arc::default(), + index: Arc::new(RwLock::new(Index::new())), }; if let Err(e) = index_manager.try_restore_index().await { error!("Failed to restore index: {}", e); @@ -88,6 +89,19 @@ impl IndexManager { .await .unwrap() } + + pub async fn flatten(&self, virtual_path: PathBuf) -> Result, collection::Error> { + spawn_blocking({ + let index_manager = self.clone(); + move || { + let index = index_manager.index.read().unwrap(); + index.flatten(virtual_path) + } + }) + .await + .unwrap() + } + pub async fn get_artist( &self, artist_key: &ArtistKey, @@ -169,7 +183,7 @@ impl IndexManager { #[derive(Default)] pub(super) struct IndexBuilder { directories: HashMap>, - // filesystem: Trie<>, + flattened: TrieBuilder, songs: HashMap, artists: HashMap, albums: HashMap, @@ -190,6 +204,12 @@ impl IndexBuilder { pub fn add_song(&mut self, song: collection::Song) { let song_id: SongID = song.song_id(); + self.flattened.push( + song.virtual_path + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>(), + ); self.directories .entry(song.virtual_parent.clone()) .or_default() @@ -272,6 +292,7 @@ impl IndexBuilder { Index { directories: self.directories, + flattened: self.flattened.build(), songs: self.songs, artists: self.artists, albums: self.albums, @@ -280,9 +301,10 @@ impl IndexBuilder { } } -#[derive(Default, Serialize, Deserialize)] +#[derive(Serialize, Deserialize)] pub(super) struct Index { directories: HashMap>, + flattened: Trie, songs: HashMap, artists: HashMap, albums: HashMap, @@ -290,6 +312,17 @@ pub(super) struct Index { } impl Index { + pub fn new() -> Self { + Self { + directories: HashMap::new(), + flattened: TrieBuilder::new().build(), + songs: HashMap::new(), + artists: HashMap::new(), + albums: HashMap::new(), + recent_albums: Vec::new(), + } + } + pub(self) fn browse>( &self, virtual_path: P, @@ -302,6 +335,30 @@ impl Index { Ok(files.iter().cloned().collect()) } + pub(self) fn flatten>( + &self, + virtual_path: P, + ) -> Result, collection::Error> { + let path_components = virtual_path + .as_ref() + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>(); + + if !self.flattened.is_prefix(&path_components) { + return Err(collection::Error::DirectoryNotFound( + virtual_path.as_ref().to_owned(), + )); + } + + Ok(self + .flattened + .predictive_search(path_components) + .map(|c: Vec| -> PathBuf { c.join(std::path::MAIN_SEPARATOR_STR).into() }) + .map(|s| SongKey { virtual_path: s }) + .collect::>()) + } + pub(self) fn get_artist(&self, artist_id: ArtistID) -> Option { self.artists.get(&artist_id).map(|a| { let albums = { diff --git a/src/app/playlist.rs b/src/app/playlist.rs index 9cbe084..ca70b92 100644 --- a/src/app/playlist.rs +++ b/src/app/playlist.rs @@ -2,7 +2,8 @@ use core::clone::Clone; use sqlx::{Acquire, QueryBuilder, Sqlite}; use std::path::PathBuf; -use crate::app::{collection::Song, vfs}; +use crate::app::collection::SongKey; +use crate::app::vfs; use crate::db::{self, DB}; #[derive(thiserror::Error, Debug)] @@ -125,7 +126,7 @@ impl Manager { &self, playlist_name: &str, owner: &str, - ) -> Result, Error> { + ) -> Result, Error> { let songs = { let mut connection = self.db.connect().await?; @@ -319,7 +320,6 @@ mod test { .unwrap(); assert_eq!(songs.len(), 13); - assert_eq!(songs[0].title, Some("Above The Water".to_owned())); let first_song_path: PathBuf = [ TEST_MOUNT_NAME, diff --git a/src/server/axum/api.rs b/src/server/axum/api.rs index 32f8e39..0b766d1 100644 --- a/src/server/axum/api.rs +++ b/src/server/axum/api.rs @@ -282,7 +282,7 @@ fn collection_files_to_response( } } -fn songs_to_response(files: Vec, api_version: APIMajorVersion) -> Response { +fn songs_to_response(files: Vec, api_version: APIMajorVersion) -> Response { match api_version { APIMajorVersion::V7 => Json( files @@ -291,12 +291,9 @@ fn songs_to_response(files: Vec, api_version: APIMajorVersion) .collect::>(), ) .into_response(), - APIMajorVersion::V8 => Json( - files - .into_iter() - .map(|f| f.into()) - .collect::>(), - ) + APIMajorVersion::V8 => Json(dto::SongList { + paths: files.into_iter().map(|s| s.virtual_path).collect(), + }) .into_response(), } } @@ -348,9 +345,9 @@ async fn get_browse( async fn get_flatten_root( _auth: Auth, api_version: APIMajorVersion, - State(browser): State, + State(index_manager): State, ) -> Response { - let songs = match browser.flatten(std::path::Path::new("")).await { + let songs = match index_manager.flatten(PathBuf::new()).await { Ok(s) => s, Err(e) => return APIError::from(e).into_response(), }; @@ -360,11 +357,10 @@ async fn get_flatten_root( async fn get_flatten( _auth: Auth, api_version: APIMajorVersion, - State(browser): State, - Path(path): Path, + State(index_manager): State, + Path(path): Path, ) -> Response { - let path = percent_decode_str(&path).decode_utf8_lossy(); - let songs = match browser.flatten(std::path::Path::new(path.as_ref())).await { + let songs = match index_manager.flatten(path).await { Ok(s) => s, Err(e) => return APIError::from(e).into_response(), }; diff --git a/src/server/dto/v7.rs b/src/server/dto/v7.rs index 40310bf..5eaa0c8 100644 --- a/src/server/dto/v7.rs +++ b/src/server/dto/v7.rs @@ -299,6 +299,27 @@ pub struct Song { pub label: Option, } +impl From for Song { + fn from(song_key: collection::SongKey) -> Self { + Self { + path: song_key.virtual_path, + track_number: None, + disc_number: None, + title: None, + artist: None, + album_artist: None, + year: None, + album: None, + artwork: None, + duration: None, + lyricist: None, + composer: None, + genre: None, + label: None, + } + } +} + impl From for Song { fn from(s: collection::Song) -> Self { Self { diff --git a/src/server/dto/v8.rs b/src/server/dto/v8.rs index b8b7cff..c158746 100644 --- a/src/server/dto/v8.rs +++ b/src/server/dto/v8.rs @@ -280,6 +280,11 @@ impl From for Song { } } +#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct SongList { + pub paths: Vec, +} + #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)] pub struct BrowserEntry { pub path: PathBuf,