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,