diff --git a/src/rust/Cargo.lock b/src/rust/Cargo.lock index 5314aaf704d5fbe48d8b6b86764dcc54391a8dfe..79624569c87ab6a1231400e6ae134b88e2a1ba09 100644 --- a/src/rust/Cargo.lock +++ b/src/rust/Cargo.lock @@ -1765,6 +1765,7 @@ dependencies = [ "lektor_db", "reqwest", "serde", + "serde_json", "smallstring", "tokio", ] diff --git a/src/rust/Cargo.toml b/src/rust/Cargo.toml index 5c914887aef9e6b5fd033ed2cd2795066a412fd1..a2765b896091871a57d7ff67ecc2c3a55cad7187 100644 --- a/src/rust/Cargo.toml +++ b/src/rust/Cargo.toml @@ -63,7 +63,6 @@ serde = { version = "^1", default-features = false, features = [ reqwest = { version = "0.11", default-features = false, features = [ "rustls-tls", "rustls-tls-native-roots", - "json", ] } # Async stuff diff --git a/src/rust/kurisu_api/src/route.rs b/src/rust/kurisu_api/src/route.rs index c497855d685896c49691c75553bb5da5140f5ea8..a666601260a1c2644401e29ef2342415505f5b7a 100644 --- a/src/rust/kurisu_api/src/route.rs +++ b/src/rust/kurisu_api/src/route.rs @@ -39,6 +39,12 @@ macro_rules! route { const COUNT: usize = $crate::route!(count_args $($expr),*); $crate::route::build_request::<$route, COUNT>([$($expr),*]) }}; + + ($route: ident -> $base: expr ; $($expr: expr),*) => {{ + let mut route = $crate::route!($route -> $($expr),*); + route.insert_str(0, &$base); + route + }}; } /// Create a new route. diff --git a/src/rust/kurisu_api/src/v1.rs b/src/rust/kurisu_api/src/v1.rs index 31af93179b6c66abaacd506c37fec28ddc6bee1e..f28ca2d07116d8752ba9089d39a80ae7988bf9a3 100644 --- a/src/rust/kurisu_api/src/v1.rs +++ b/src/rust/kurisu_api/src/v1.rs @@ -18,6 +18,9 @@ pub struct Kara<'a> { pub popularity: i64, pub unix_timestamp: i64, pub size: i64, + + /// Still need to be present on kurisu... + pub file_hash: &'a str, } #[derive(Debug, Deserialize)] @@ -41,6 +44,19 @@ pub enum MaybeKaraList<'a> { Multi(Vec<Kara<'a>>), } +impl<'a> IntoIterator for MaybeKaraList<'a> { + type Item = Kara<'a>; + type IntoIter = std::vec::IntoIter<Kara<'a>>; + + fn into_iter(self) -> Self::IntoIter { + match self { + MaybeKaraList::Empty => vec![].into_iter(), + MaybeKaraList::Single(kara) => vec![kara].into_iter(), + MaybeKaraList::Multi(karas) => karas.into_iter(), + } + } +} + impl<'a, 'de> Deserialize<'de> for MaybeKaraList<'a> where 'de: 'a, diff --git a/src/rust/kurisu_api/src/v2.rs b/src/rust/kurisu_api/src/v2.rs index 0ba6f985eb5d5d43b1dea48dfaf5f9dd79e13ea5..d6d5bb44991da3992d9954c7bc09c5eabf2bf8be 100644 --- a/src/rust/kurisu_api/src/v2.rs +++ b/src/rust/kurisu_api/src/v2.rs @@ -42,8 +42,34 @@ pub enum SongOrigin { Autre, } +impl SongType { + pub fn as_str(&self) -> &str { + match self { + SongType::OP => "OP", + SongType::ED => "ED", + SongType::IS => "IS", + SongType::MV => "MV", + SongType::OT => "OT", + } + } +} + +impl SongOrigin { + pub fn as_str(&self) -> &str { + match self { + SongOrigin::Anime => "anime", + SongOrigin::VN => "vn", + SongOrigin::Game => "game", + SongOrigin::Music => "music", + SongOrigin::Autre => "autre", + } + } +} + #[derive(Debug, Deserialize, PartialEq, Eq)] pub struct Kara<'a> { + pub id: i64, + #[serde(rename = "title")] pub song_title: &'a str, @@ -66,6 +92,8 @@ pub struct Kara<'a> { pub last_modified_epoch: i64, pub creation_epoch: i64, + pub file_hash: &'a str, + #[serde(flatten)] pub tags: HashMap<&'a str, Vec<&'a str>>, } @@ -99,7 +127,8 @@ pub struct PlaylistList<'a> { new_route! { GetRepo :: "/" #0 -> Repo } -new_route! { GetKaras :: "/kara" #0 -> KaraList } +new_route! { GetAllKaras :: "/kara" #0 -> KaraList } +new_route! { GetKaras :: "/kara/@" #1 -> KaraList } new_route! { GetKara :: "/kara/@" #1 -> Kara } new_route! { GetKaraDl :: "/kara/dl/@" #1 -> KaraDl } @@ -199,6 +228,7 @@ mod test { , "karamakers": [ "Viieux", "Totoro" ] , "is_virtual": false , "type": "op" + , "id": 42 , "origin": "anime" , "languages": [ "jp" ] , "last_modified_epoch": 1 @@ -216,6 +246,7 @@ mod test { , "is_virtual": false , "tag_1": [ "1", "2" ] , "type": "op" + , "id": 42 , "origin": "anime" , "languages": [ "jp" ] , "tag_2": [] diff --git a/src/rust/lektor_db/src/connexion.rs b/src/rust/lektor_db/src/connexion.rs index 65551092ed8709ea280a3da9e1b60cfd200dc3c7..11af90b9395eb8dbe6d66ddce33663cf24288ec3 100644 --- a/src/rust/lektor_db/src/connexion.rs +++ b/src/rust/lektor_db/src/connexion.rs @@ -107,6 +107,15 @@ impl LktDatabaseConnection { Ok(Self { sqlite }) } + pub fn get_repo_id(&mut self, repo_name: impl AsRef<str>) -> LktDatabaseResult<i64> { + with_dsl!(repo => repo + .select(id) + .filter(name.is(repo_name.as_ref())) + .first::<i64>(&mut self.sqlite) + .map_err(LktDatabaseError::DieselResult) + ) + } + /// Get a tag id by its name. If the tag doesn't exists, create it. fn get_tag_id_by_name(&mut self, tag_name: impl AsRef<str>) -> LktDatabaseResult<i64> { with_dsl!(tag => self.sqlite.exclusive_transaction(|conn| { @@ -203,6 +212,10 @@ impl LktDatabaseConnection { }) } + pub fn make_kara_available<'a>(&mut self, remote_id: RemoteKaraId) -> LktDatabaseResult<()> { + todo!() + } + /// Create a series of models from a kara signature from Kurisu's V1 API. pub fn new_kara_v1<'a>( &mut self, @@ -237,7 +250,7 @@ impl LktDatabaseConnection { song_type: kara.song_type, song_origin: kara.category, source_name: kara.source_name, - file_hash: format!("{}", kara.unix_timestamp), + file_hash: kara.file_hash, }; Ok((id, kara, kara_maker, vec![lang], tags)) } diff --git a/src/rust/lektor_db/src/error.rs b/src/rust/lektor_db/src/error.rs index 05e562c974237ba747fdc9ee7ae6592dde399194..bbbff5e8325e7bcf8e4c9b3dc858fca30cc0ae74 100644 --- a/src/rust/lektor_db/src/error.rs +++ b/src/rust/lektor_db/src/error.rs @@ -14,6 +14,9 @@ pub enum LktDatabaseError { #[error("database error: {0}")] Str(&'static str), + + #[error("database error: not found!")] + NotFound, } #[derive(Debug, thiserror::Error)] diff --git a/src/rust/lektor_db/src/lib.rs b/src/rust/lektor_db/src/lib.rs index add72f281086040fe052ad7b4ec92f3242be49d6..be1412471b52f9d3291d05928b38566e3f75ed99 100644 --- a/src/rust/lektor_db/src/lib.rs +++ b/src/rust/lektor_db/src/lib.rs @@ -11,8 +11,9 @@ pub mod uri; pub(self) mod schema; +pub use error::*; + pub(self) use diesel::prelude::*; -pub(self) use error::*; pub(self) use std::{collections::VecDeque, ops::Range, path::Path}; /// All the information needed to add a kara recieved from a repo! diff --git a/src/rust/lektor_db/src/models.rs b/src/rust/lektor_db/src/models.rs index b42f25edebc82387fb70732815dc6d20747e5065..f07ed82013bc797f2a57b076c84c6a5ce4d4ffb6 100644 --- a/src/rust/lektor_db/src/models.rs +++ b/src/rust/lektor_db/src/models.rs @@ -18,6 +18,13 @@ pub struct KaraId { pub local_kara_id: i64, } +#[derive(Debug, Queryable, Selectable)] +#[diesel(table_name = repo_kara)] +pub struct RemoteKaraId { + pub repo_id: i64, + pub repo_kara_id: i64, +} + #[derive(Debug, Insertable, Queryable, Selectable)] #[diesel(table_name = history)] pub struct HistoryRecord { @@ -43,7 +50,7 @@ pub struct NewKara<'a> { pub song_type: &'a str, pub song_origin: &'a str, pub source_name: &'a str, - pub file_hash: String, + pub file_hash: &'a str, } #[derive(Debug, Insertable)] diff --git a/src/rust/lektor_repo/Cargo.toml b/src/rust/lektor_repo/Cargo.toml index 1d11b3ab08c9e2fbb8437ff8885f1c801124f5f3..fd0872121ae94209d8e5bb5dad2eb9284aece13b 100644 --- a/src/rust/lektor_repo/Cargo.toml +++ b/src/rust/lektor_repo/Cargo.toml @@ -14,6 +14,7 @@ tokio.workspace = true futures.workspace = true reqwest.workspace = true hashbrown.workspace = true +serde_json.workspace = true lektor_c_compat = { path = "../lektor_c_compat" } lektor_config = { path = "../lektor_config" } diff --git a/src/rust/lektor_repo/src/download.rs b/src/rust/lektor_repo/src/download.rs index 20a01f616b4b141d58a04db054020215b73d8291..7008c955c2ace05e64d4afbf1525a5ffc65903bb 100644 --- a/src/rust/lektor_repo/src/download.rs +++ b/src/rust/lektor_repo/src/download.rs @@ -1,7 +1,6 @@ -use std::path::PathBuf; - use crate::*; use futures::stream::{self, StreamExt}; +use std::path::{Path, PathBuf}; pub struct DownloadBuilder { hosts: Vec<RepoConfig>, @@ -49,20 +48,6 @@ fn formatter_for_api_version(version: RepoApiVersion) -> fn(&LktUri) -> Result<S } } -/// Download the metadata of the kara, should be a json thing. Deserialize it latter depending on -/// the API version. -fn download_metadata<'a>( - _api: RepoApiVersion, - _base: &'a str, - _url: &str, -) -> Result<(&'a str, String), String> { - todo!() -} - -fn download_kara(search_urls: &[String], destination: PathBuf) -> Result<(), String> { - todo!() -} - impl Download { pub fn builder(queue: LktCQueue, hosts: Vec<RepoConfig>, db: LktLockDbPtr) -> DownloadBuilder { DownloadBuilder { @@ -74,30 +59,134 @@ impl Download { } } + /// Download the metadata of the kara. We update the database with the downloaded metadata. The + /// function only returns the karas' id viewed from the repo. + async fn download_metadata<'a>( + db: LktLockDbPtr, + api: RepoApiVersion, + base: &'a str, + repo_id: i64, + uri: &str, + ) -> Result<(&'a str, Vec<i64>), String> { + use kurisu_api::route; + match api { + RepoApiVersion::V1 => { + use kurisu_api::v1::*; + let content = reqwest::get(route!(GetKara -> base; uri)) + .await + .map_err(|err| format!("{err}"))? + .text() + .await + .map_err(|err| format!("{err}"))?; + let (new_karas, errors): (Vec<_>, Vec<_>) = + serde_json::from_str::<MaybeKaraList>(&content) + .map_err(|err| format!("{err}"))? + .into_iter() + .map(|kara_v1| { + let mut db = db.lock().expect("failed to lock the database..."); + let new_kara = db.new_kara_v1(repo_id, kara_v1)?; + let repo_kara_id = new_kara.0.repo_kara_id; + db.add_kara_from_request(new_kara)?; + Ok::<i64, lektor_db::LktDatabaseError>(repo_kara_id) + }) + .partition(Result::is_ok); + for error in errors.into_iter().map(Result::unwrap_err) { + log::error!(target: "REPO", "failed to download metadata for kara from {base}: {error}"); + } + either!(new_karas.is_empty() + => Err(format!("no kara found in {base} for uri: {uri}")) + ; Ok((base, new_karas.into_iter().map(Result::unwrap).collect()))) + } + + RepoApiVersion::V2 => { + todo!() + } + } + } + + /// Build the destination file path for the kara. + fn build_destination_file_path( + _db: LktLockDbPtr, + _repo_id: i64, + _repo_kara_id: i64, + ) -> Result<(i64, PathBuf), String> { + todo!() + } + + /// Download the kara file. + async fn download_kara( + _db: LktLockDbPtr, + _api: RepoApiVersion, + _search_urls: &[&str], + _repo_id: i64, + _repo_kara_id: i64, + _destination: impl AsRef<Path>, + ) -> Result<(), String> { + todo!() + } + pub async fn download(self) { + let db = &self.db; let uri = &self.uri; + let repo_id = { + let mut db = self.db.lock().expect("failed to lock the database..."); + let repo_id = + self.hosts + .iter() + .find_map(|RepoConfig { name, .. }| match db.get_repo_id(name) { + Ok(repo_id) => Some(repo_id), + Err(err) => { + log::error!(target: "REPO", "no id found for repo: {err}"); + None + } + }); + match repo_id { + Some(repo_id) => repo_id, + None => return, + } + }; + stream::iter(&self.hosts) .for_each_concurrent(2, |RepoConfig { name, api, urls }| async move { - let uri = match formatter_for_api_version(*api)(uri) { + let uri = match formatter_for_api_version(*api)(&uri) { Ok(uri) => uri, Err(err) => { log::error!(target: "REPO", "{err}"); return; } }; - let ok_urls = stream::iter(urls.iter().map(|base| (base, format!("{base}/{uri}")))) - .filter_map(|(base, url)| async move { - download_metadata(*api, base, &url).map_err(|err| log::error!(target: "REPO", "failed to download metadata with url `{url}`: {err}")).ok() + let uri = &uri; + let ok_urls = stream::iter(urls.iter()) + .filter_map(|base | async move { + Download::download_metadata(db.clone(), *api, base, repo_id, &uri) + .await + .map_err(|err| log::error!(target: "REPO", "failed to download metadata with uri `{uri}`: {err}")) + .ok() }); - let Some((base, content)) = Box::pin(ok_urls).next().await else { + let Some((base, karas_id)) = Box::pin(ok_urls).next().await else { log::error!("can't find uri {uri} in repo {name}"); return; }; - - let search_urls: Vec<_> = Some(base).into_iter().chain(urls.iter().filter_map(|url| (url != base).then_some(url.as_str()))).collect(); - - // Find the first url with an Ok status, then get the id and dl the kara if not dry. - // If the dl was not successfull, try the next url + let other_urls = urls.iter().filter_map(|url| (url != base).then_some(url.as_str())); + let search_urls: Vec<_> = Some(base).into_iter().chain(other_urls).collect(); + let (karas_id, errors): (Vec<_>, Vec<_>) = karas_id + .into_iter() + .map(|repo_kara_id| Download::build_destination_file_path(db.clone(), repo_id, repo_kara_id)) + .partition(Result::is_ok); + for error in errors.into_iter().map(Result::unwrap_err) { + log::error!(target: "REPO", "failed to build a file path for kara from repo {name}: {error}"); + } + for (repo_kara_id, destination) in karas_id.into_iter().map(Result::unwrap) { + if let Err(err) = Download::download_kara(db.clone(), *api, &search_urls[..], repo_id, repo_kara_id, &destination).await { + log::error!(target: "REPO", "failed to download file `{}` for kara {repo_kara_id}: {err}", destination.to_string_lossy()); + return; + } + if let Err(err) = db.lock().expect("failed to lock the database...") + .make_kara_available(lektor_db::models::RemoteKaraId { repo_id, repo_kara_id }) { + log::error!(target: "REPO", "failed to make kara `{repo_kara_id}` from {name} available: {err}"); + return; + } + } }) .await; }