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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e825f6987101665dea6ec934c09ec6d721de7bc1bf92248e1d5810c8cd636b77"
|
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]]
|
[[package]]
|
||||||
name = "flate2"
|
name = "flate2"
|
||||||
version = "1.0.27"
|
version = "1.0.27"
|
||||||
|
@ -1178,6 +1188,16 @@ version = "0.4.22"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
|
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]]
|
[[package]]
|
||||||
name = "matches"
|
name = "matches"
|
||||||
version = "0.1.10"
|
version = "0.1.10"
|
||||||
|
@ -1631,6 +1651,7 @@ dependencies = [
|
||||||
"tokio-util",
|
"tokio-util",
|
||||||
"toml 0.8.14",
|
"toml 0.8.14",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
|
"trie-rs",
|
||||||
"ureq 2.10.0",
|
"ureq 2.10.0",
|
||||||
"url",
|
"url",
|
||||||
"winres",
|
"winres",
|
||||||
|
@ -2818,6 +2839,16 @@ dependencies = [
|
||||||
"once_cell",
|
"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]]
|
[[package]]
|
||||||
name = "try-lock"
|
name = "try-lock"
|
||||||
version = "0.2.5"
|
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"] }
|
tokio-util = { version = "0.7.11", features = ["io"] }
|
||||||
toml = "0.8.14"
|
toml = "0.8.14"
|
||||||
tower-http = { version = "0.5.2", features = ["fs"] }
|
tower-http = { version = "0.5.2", features = ["fs"] }
|
||||||
|
trie-rs = { version = "0.4.2", features = ["serde"] }
|
||||||
ureq = "2.10.0"
|
ureq = "2.10.0"
|
||||||
url = "2.3"
|
url = "2.3"
|
||||||
|
|
||||||
|
|
|
@ -14,20 +14,6 @@ impl Browser {
|
||||||
Self { db, vfs_manager }
|
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> {
|
pub async fn search(&self, query: &str) -> Result<Vec<collection::File>, collection::Error> {
|
||||||
todo!();
|
todo!();
|
||||||
}
|
}
|
||||||
|
|
|
@ -10,6 +10,7 @@ use log::{error, info};
|
||||||
use rand::{rngs::ThreadRng, seq::IteratorRandom};
|
use rand::{rngs::ThreadRng, seq::IteratorRandom};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use tokio::task::spawn_blocking;
|
use tokio::task::spawn_blocking;
|
||||||
|
use trie_rs::{Trie, TrieBuilder};
|
||||||
|
|
||||||
use crate::{app::collection, db::DB};
|
use crate::{app::collection, db::DB};
|
||||||
|
|
||||||
|
@ -23,7 +24,7 @@ impl IndexManager {
|
||||||
pub async fn new(db: DB) -> Self {
|
pub async fn new(db: DB) -> Self {
|
||||||
let mut index_manager = Self {
|
let mut index_manager = Self {
|
||||||
db,
|
db,
|
||||||
index: Arc::default(),
|
index: Arc::new(RwLock::new(Index::new())),
|
||||||
};
|
};
|
||||||
if let Err(e) = index_manager.try_restore_index().await {
|
if let Err(e) = index_manager.try_restore_index().await {
|
||||||
error!("Failed to restore index: {}", e);
|
error!("Failed to restore index: {}", e);
|
||||||
|
@ -88,6 +89,19 @@ impl IndexManager {
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.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(
|
pub async fn get_artist(
|
||||||
&self,
|
&self,
|
||||||
artist_key: &ArtistKey,
|
artist_key: &ArtistKey,
|
||||||
|
@ -169,7 +183,7 @@ impl IndexManager {
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub(super) struct IndexBuilder {
|
pub(super) struct IndexBuilder {
|
||||||
directories: HashMap<PathBuf, HashSet<collection::File>>,
|
directories: HashMap<PathBuf, HashSet<collection::File>>,
|
||||||
// filesystem: Trie<>,
|
flattened: TrieBuilder<String>,
|
||||||
songs: HashMap<SongID, collection::Song>,
|
songs: HashMap<SongID, collection::Song>,
|
||||||
artists: HashMap<ArtistID, Artist>,
|
artists: HashMap<ArtistID, Artist>,
|
||||||
albums: HashMap<AlbumID, Album>,
|
albums: HashMap<AlbumID, Album>,
|
||||||
|
@ -190,6 +204,12 @@ impl IndexBuilder {
|
||||||
|
|
||||||
pub fn add_song(&mut self, song: collection::Song) {
|
pub fn add_song(&mut self, song: collection::Song) {
|
||||||
let song_id: SongID = song.song_id();
|
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
|
self.directories
|
||||||
.entry(song.virtual_parent.clone())
|
.entry(song.virtual_parent.clone())
|
||||||
.or_default()
|
.or_default()
|
||||||
|
@ -272,6 +292,7 @@ impl IndexBuilder {
|
||||||
|
|
||||||
Index {
|
Index {
|
||||||
directories: self.directories,
|
directories: self.directories,
|
||||||
|
flattened: self.flattened.build(),
|
||||||
songs: self.songs,
|
songs: self.songs,
|
||||||
artists: self.artists,
|
artists: self.artists,
|
||||||
albums: self.albums,
|
albums: self.albums,
|
||||||
|
@ -280,9 +301,10 @@ impl IndexBuilder {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default, Serialize, Deserialize)]
|
#[derive(Serialize, Deserialize)]
|
||||||
pub(super) struct Index {
|
pub(super) struct Index {
|
||||||
directories: HashMap<PathBuf, HashSet<collection::File>>,
|
directories: HashMap<PathBuf, HashSet<collection::File>>,
|
||||||
|
flattened: Trie<String>,
|
||||||
songs: HashMap<SongID, collection::Song>,
|
songs: HashMap<SongID, collection::Song>,
|
||||||
artists: HashMap<ArtistID, Artist>,
|
artists: HashMap<ArtistID, Artist>,
|
||||||
albums: HashMap<AlbumID, Album>,
|
albums: HashMap<AlbumID, Album>,
|
||||||
|
@ -290,6 +312,17 @@ pub(super) struct Index {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl 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>>(
|
pub(self) fn browse<P: AsRef<Path>>(
|
||||||
&self,
|
&self,
|
||||||
virtual_path: P,
|
virtual_path: P,
|
||||||
|
@ -302,6 +335,30 @@ impl Index {
|
||||||
Ok(files.iter().cloned().collect())
|
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> {
|
pub(self) fn get_artist(&self, artist_id: ArtistID) -> Option<collection::Artist> {
|
||||||
self.artists.get(&artist_id).map(|a| {
|
self.artists.get(&artist_id).map(|a| {
|
||||||
let albums = {
|
let albums = {
|
||||||
|
|
|
@ -2,7 +2,8 @@ use core::clone::Clone;
|
||||||
use sqlx::{Acquire, QueryBuilder, Sqlite};
|
use sqlx::{Acquire, QueryBuilder, Sqlite};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
use crate::app::{collection::Song, vfs};
|
use crate::app::collection::SongKey;
|
||||||
|
use crate::app::vfs;
|
||||||
use crate::db::{self, DB};
|
use crate::db::{self, DB};
|
||||||
|
|
||||||
#[derive(thiserror::Error, Debug)]
|
#[derive(thiserror::Error, Debug)]
|
||||||
|
@ -125,7 +126,7 @@ impl Manager {
|
||||||
&self,
|
&self,
|
||||||
playlist_name: &str,
|
playlist_name: &str,
|
||||||
owner: &str,
|
owner: &str,
|
||||||
) -> Result<Vec<Song>, Error> {
|
) -> Result<Vec<SongKey>, Error> {
|
||||||
let songs = {
|
let songs = {
|
||||||
let mut connection = self.db.connect().await?;
|
let mut connection = self.db.connect().await?;
|
||||||
|
|
||||||
|
@ -319,7 +320,6 @@ mod test {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
|
|
||||||
assert_eq!(songs.len(), 13);
|
assert_eq!(songs.len(), 13);
|
||||||
assert_eq!(songs[0].title, Some("Above The Water".to_owned()));
|
|
||||||
|
|
||||||
let first_song_path: PathBuf = [
|
let first_song_path: PathBuf = [
|
||||||
TEST_MOUNT_NAME,
|
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 {
|
match api_version {
|
||||||
APIMajorVersion::V7 => Json(
|
APIMajorVersion::V7 => Json(
|
||||||
files
|
files
|
||||||
|
@ -291,12 +291,9 @@ fn songs_to_response(files: Vec<collection::Song>, api_version: APIMajorVersion)
|
||||||
.collect::<Vec<dto::v7::Song>>(),
|
.collect::<Vec<dto::v7::Song>>(),
|
||||||
)
|
)
|
||||||
.into_response(),
|
.into_response(),
|
||||||
APIMajorVersion::V8 => Json(
|
APIMajorVersion::V8 => Json(dto::SongList {
|
||||||
files
|
paths: files.into_iter().map(|s| s.virtual_path).collect(),
|
||||||
.into_iter()
|
})
|
||||||
.map(|f| f.into())
|
|
||||||
.collect::<Vec<dto::Song>>(),
|
|
||||||
)
|
|
||||||
.into_response(),
|
.into_response(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -348,9 +345,9 @@ async fn get_browse(
|
||||||
async fn get_flatten_root(
|
async fn get_flatten_root(
|
||||||
_auth: Auth,
|
_auth: Auth,
|
||||||
api_version: APIMajorVersion,
|
api_version: APIMajorVersion,
|
||||||
State(browser): State<collection::Browser>,
|
State(index_manager): State<collection::IndexManager>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let songs = match browser.flatten(std::path::Path::new("")).await {
|
let songs = match index_manager.flatten(PathBuf::new()).await {
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return APIError::from(e).into_response(),
|
Err(e) => return APIError::from(e).into_response(),
|
||||||
};
|
};
|
||||||
|
@ -360,11 +357,10 @@ async fn get_flatten_root(
|
||||||
async fn get_flatten(
|
async fn get_flatten(
|
||||||
_auth: Auth,
|
_auth: Auth,
|
||||||
api_version: APIMajorVersion,
|
api_version: APIMajorVersion,
|
||||||
State(browser): State<collection::Browser>,
|
State(index_manager): State<collection::IndexManager>,
|
||||||
Path(path): Path<String>,
|
Path(path): Path<PathBuf>,
|
||||||
) -> Response {
|
) -> Response {
|
||||||
let path = percent_decode_str(&path).decode_utf8_lossy();
|
let songs = match index_manager.flatten(path).await {
|
||||||
let songs = match browser.flatten(std::path::Path::new(path.as_ref())).await {
|
|
||||||
Ok(s) => s,
|
Ok(s) => s,
|
||||||
Err(e) => return APIError::from(e).into_response(),
|
Err(e) => return APIError::from(e).into_response(),
|
||||||
};
|
};
|
||||||
|
|
|
@ -299,6 +299,27 @@ pub struct Song {
|
||||||
pub label: Option<String>,
|
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 {
|
impl From<collection::Song> for Song {
|
||||||
fn from(s: collection::Song) -> Self {
|
fn from(s: collection::Song) -> Self {
|
||||||
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)]
|
#[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
|
||||||
pub struct BrowserEntry {
|
pub struct BrowserEntry {
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
|
Loading…
Add table
Reference in a new issue