Flatten via trie
This commit is contained in:
parent
7a1d433c8a
commit
a0624f7968
8 changed files with 130 additions and 33 deletions
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -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"
|
||||
|
|
|
@ -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"
|
||||
|
||||
|
|
|
@ -14,20 +14,6 @@ impl Browser {
|
|||
Self { db, vfs_manager }
|
||||
}
|
||||
|
||||
pub async fn browse<P>(&self, path: P) -> Result<Vec<collection::File>, collection::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
todo!();
|
||||
}
|
||||
|
||||
pub async fn flatten<P>(&self, path: P) -> Result<Vec<collection::Song>, collection::Error>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
todo!();
|
||||
}
|
||||
|
||||
pub async fn search(&self, query: &str) -> Result<Vec<collection::File>, collection::Error> {
|
||||
todo!();
|
||||
}
|
||||
|
|
|
@ -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<Vec<SongKey>, 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<PathBuf, HashSet<collection::File>>,
|
||||
// filesystem: Trie<>,
|
||||
flattened: TrieBuilder<String>,
|
||||
songs: HashMap<SongID, collection::Song>,
|
||||
artists: HashMap<ArtistID, Artist>,
|
||||
albums: HashMap<AlbumID, Album>,
|
||||
|
@ -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::<Vec<_>>(),
|
||||
);
|
||||
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<PathBuf, HashSet<collection::File>>,
|
||||
flattened: Trie<String>,
|
||||
songs: HashMap<SongID, collection::Song>,
|
||||
artists: HashMap<ArtistID, Artist>,
|
||||
albums: HashMap<AlbumID, Album>,
|
||||
|
@ -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<P: AsRef<Path>>(
|
||||
&self,
|
||||
virtual_path: P,
|
||||
|
@ -302,6 +335,30 @@ impl Index {
|
|||
Ok(files.iter().cloned().collect())
|
||||
}
|
||||
|
||||
pub(self) fn flatten<P: AsRef<Path>>(
|
||||
&self,
|
||||
virtual_path: P,
|
||||
) -> Result<Vec<SongKey>, collection::Error> {
|
||||
let path_components = virtual_path
|
||||
.as_ref()
|
||||
.components()
|
||||
.map(|c| c.as_os_str().to_string_lossy().to_string())
|
||||
.collect::<Vec<String>>();
|
||||
|
||||
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<String>| -> PathBuf { c.join(std::path::MAIN_SEPARATOR_STR).into() })
|
||||
.map(|s| SongKey { virtual_path: s })
|
||||
.collect::<Vec<_>>())
|
||||
}
|
||||
|
||||
pub(self) fn get_artist(&self, artist_id: ArtistID) -> Option<collection::Artist> {
|
||||
self.artists.get(&artist_id).map(|a| {
|
||||
let albums = {
|
||||
|
|
|
@ -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<Vec<Song>, Error> {
|
||||
) -> Result<Vec<SongKey>, 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,
|
||||
|
|
|
@ -282,7 +282,7 @@ fn collection_files_to_response(
|
|||
}
|
||||
}
|
||||
|
||||
fn songs_to_response(files: Vec<collection::Song>, api_version: APIMajorVersion) -> Response {
|
||||
fn songs_to_response(files: Vec<collection::SongKey>, api_version: APIMajorVersion) -> Response {
|
||||
match api_version {
|
||||
APIMajorVersion::V7 => Json(
|
||||
files
|
||||
|
@ -291,12 +291,9 @@ fn songs_to_response(files: Vec<collection::Song>, api_version: APIMajorVersion)
|
|||
.collect::<Vec<dto::v7::Song>>(),
|
||||
)
|
||||
.into_response(),
|
||||
APIMajorVersion::V8 => Json(
|
||||
files
|
||||
.into_iter()
|
||||
.map(|f| f.into())
|
||||
.collect::<Vec<dto::Song>>(),
|
||||
)
|
||||
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<collection::Browser>,
|
||||
State(index_manager): State<collection::IndexManager>,
|
||||
) -> 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<collection::Browser>,
|
||||
Path(path): Path<String>,
|
||||
State(index_manager): State<collection::IndexManager>,
|
||||
Path(path): Path<PathBuf>,
|
||||
) -> 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(),
|
||||
};
|
||||
|
|
|
@ -299,6 +299,27 @@ pub struct Song {
|
|||
pub label: Option<String>,
|
||||
}
|
||||
|
||||
impl From<collection::SongKey> 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<collection::Song> for Song {
|
||||
fn from(s: collection::Song) -> Self {
|
||||
Self {
|
||||
|
|
|
@ -280,6 +280,11 @@ impl From<collection::Song> for Song {
|
|||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct SongList {
|
||||
pub paths: Vec<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub struct BrowserEntry {
|
||||
pub path: PathBuf,
|
||||
|
|
Loading…
Add table
Reference in a new issue