Flatten via trie

This commit is contained in:
Antoine Gersant 2024-07-31 18:00:26 -07:00
parent 7a1d433c8a
commit a0624f7968
8 changed files with 130 additions and 33 deletions

31
Cargo.lock generated
View file

@ -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"

View file

@ -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"

View file

@ -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!();
}

View file

@ -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 = {

View file

@ -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,

View file

@ -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(),
};

View file

@ -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 {

View file

@ -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,