From 4cb7e6d5d154ea6d3d56a98c3e02a601850a69cb Mon Sep 17 00:00:00 2001 From: Kubat <maelle.martin@proton.me> Date: Fri, 18 Oct 2024 18:35:54 +0200 Subject: [PATCH] ALL: Continue implementations - Let the TagKey be public, it simplify things - Can query the epochs and change the way the thing is handle - Remove no longer used file - Update Amadeus accordingly --- amadeus/i18n/en/amadeus.ftl | 1 + amadeus/i18n/es-ES/amadeus.ftl | 1 + amadeus/i18n/fr-FR/amadeus.ftl | 1 + amadeus/src/app.rs | 159 +++++++++----------- amadeus/src/app/bottom_bar.rs | 6 +- amadeus/src/app/context_pages/about.rs | 19 ++- amadeus/src/app/kard.rs | 4 +- amadeus/src/app/subscriptions/updates.rs | 16 +- amadeus/src/connection.rs | 177 +++++++++++++++++++++++ amadeus/src/lib.rs | 2 + amadeus/src/store.rs | 5 + lektor_lib/src/config.rs | 5 +- lektor_lib/src/requests.rs | 29 ++-- lektor_nkdb/src/kara/tags.rs | 43 +++--- lektor_nkdb/src/lib.rs | 7 +- lektor_nkdb/src/playlists/mod.rs | 2 +- lektor_payloads/src/action.rs | 75 ++++++++++ lektor_payloads/src/error.rs | 40 ----- lektor_payloads/src/lib.rs | 97 +------------ lektor_payloads/src/status.rs | 23 +++ lektord/src/app/mod.rs | 16 +- lektord/src/app/routes.rs | 17 ++- lkt/src/config.rs | 12 -- lkt/src/lib.rs | 11 +- 24 files changed, 466 insertions(+), 302 deletions(-) create mode 100644 amadeus/src/connection.rs create mode 100644 lektor_payloads/src/action.rs delete mode 100644 lektor_payloads/src/error.rs create mode 100644 lektor_payloads/src/status.rs diff --git a/amadeus/i18n/en/amadeus.ftl b/amadeus/i18n/en/amadeus.ftl index 64b16d88..aaaeaad6 100644 --- a/amadeus/i18n/en/amadeus.ftl +++ b/amadeus/i18n/en/amadeus.ftl @@ -28,6 +28,7 @@ playback-clear = Clear menu-queue = Queue home = Home +database = Database queue = Queue search = Search playlists = Playlists diff --git a/amadeus/i18n/es-ES/amadeus.ftl b/amadeus/i18n/es-ES/amadeus.ftl index 53619d63..0aacad98 100644 --- a/amadeus/i18n/es-ES/amadeus.ftl +++ b/amadeus/i18n/es-ES/amadeus.ftl @@ -28,6 +28,7 @@ playback-clear = Vaciar menu-queue = Cola de reprod. home = Inicio +database = Base de datos queue = Cola de reproducción search = Buscar playlists = Listas de reproducción diff --git a/amadeus/i18n/fr-FR/amadeus.ftl b/amadeus/i18n/fr-FR/amadeus.ftl index 8d581be6..8e1af80c 100644 --- a/amadeus/i18n/fr-FR/amadeus.ftl +++ b/amadeus/i18n/fr-FR/amadeus.ftl @@ -28,6 +28,7 @@ playback-clear = Effacer menu-queue = Queue home = Accueil +database = Base de donnée queue = Queue search = Chercher playlists = Listes de lecture diff --git a/amadeus/src/app.rs b/amadeus/src/app.rs index 359180a2..dbf2fdca 100644 --- a/amadeus/src/app.rs +++ b/amadeus/src/app.rs @@ -12,9 +12,13 @@ use crate::{ app::{ context_pages::ContextPage, menu::MenuAction, - pages::{search::Filter, Page}, + pages::{ + search::{Filter, FilterAtomId}, + Page, + }, }, config::{Config, LogLevel}, + connection::LektordState, fl, store::Store, }; @@ -28,12 +32,9 @@ use futures::{ prelude::*, stream::{self, FuturesUnordered}, }; -use lektor_lib::{requests, ConnectConfig}; -use lektor_payloads::{ - KId, Kara, PlayStateWithCurrent, Priority, SearchFrom, PRIORITY_LENGTH, PRIORITY_VALUES, -}; +use lektor_lib::*; +use lektor_payloads::{Epochs, KId, Kara, Priority, SearchFrom, PRIORITY_LENGTH, PRIORITY_VALUES}; use lektor_utils::{config::SocketScheme, open}; -use pages::search::FilterAtomId; use std::{ borrow::Cow, collections::HashMap, @@ -90,53 +91,6 @@ pub struct AppModel { tmp_remote_token: String, } -/// The state of lektord that we queried. -#[derive(Debug, Clone, Default)] -enum LektordState { - /// Lektord is disconnected. - #[default] - Disconnected, - - /// Lektord is connected. - Connected { - /// A version string, which is the build string from git. - version: String, - - /// The DB epoch. - last_epoch: Option<u64>, - - /// The state of the playback. - state: Option<lektor_payloads::PlayStateWithCurrent>, - }, -} - -impl LektordState { - /// Get the current kara we are playing, if any. - pub fn current_kid(&self) -> Option<&KId> { - match self { - LektordState::Disconnected => None, - LektordState::Connected { state, .. } => { - (state.as_ref()).and_then(|PlayStateWithCurrent { current, .. }| { - current.as_ref().map(|(kid, ..)| kid) - }) - } - } - } - - /// Get the times, first the current time in the kara, then the duration of the current kara, - /// if any. - pub fn current_times(&self) -> Option<(f32, f32)> { - match self { - LektordState::Disconnected => None, - LektordState::Connected { state, .. } => { - (state.as_ref()).and_then(|PlayStateWithCurrent { current, .. }| { - (current.as_ref()).map(|(_, elapse, duration)| (*elapse, *duration)) - }) - } - } - } -} - /// A command to send to the lektord instance. #[derive(Debug, Clone)] pub enum LektordCommand { @@ -175,6 +129,9 @@ pub enum LektordCommand { // Misc stuff DownloadKaraInfo(KId), DownloadKarasInfo(Vec<KId>), + + // Refresh all the karas. + DatabaseGet, } /// Something changed with the config. @@ -198,6 +155,7 @@ pub enum LektordMessage { Disconnected, Connected(lektor_payloads::Infos), PlaybackUpdate(lektor_payloads::PlayStateWithCurrent), + EpochUpdate(Epochs), DownloadedKaraInfo(Kara), DownloadedKarasInfo(Vec<Kara>), @@ -457,9 +415,8 @@ impl Application for AppModel { cosmic::command::future(async move { requests::search_karas(&*config.read().await, SearchFrom::Database, filters) .await - .map(|matches| { - cosmic::app::message::app(Message::QueryWithFiltersResults(matches)) - }) + .map(Message::QueryWithFiltersResults) + .map(cosmic::app::message::app) .unwrap_or_else(|err| { log::error!("failed to query with filters: {err}"); cosmic::app::message::none() @@ -603,46 +560,50 @@ impl AppModel { /// Handle updates from lektord. fn handle_lektord_message(&mut self, message: LektordMessage) -> Command<Message> { - use LektordMessage::*; match message { // Downloaded metadata informations. - DownloadedKaraInfo(kara) => self.store.set(kara), - DownloadedKarasInfo(karas) => karas.into_iter().for_each(|kara| self.store.set(kara)), + LektordMessage::DownloadedKaraInfo(kara) => { + self.store.set(kara); + Command::none() + } + LektordMessage::DownloadedKarasInfo(karas) => { + karas.into_iter().for_each(|kara| self.store.set(kara)); + Command::none() + } // Disconnected, if any query failed we set the disconnected status - Disconnected => { + LektordMessage::Disconnected => { if let LektordState::Connected { .. } = mem::take(&mut self.lektord_state) { log::error!("disconnected from lektord instance"); } + Command::none() } // Initial connection, done by the update subscription. It is needed for most update to // be taken into account. - Connected(lektor_payloads::Infos { - version, - last_epoch, - }) => { + LektordMessage::Connected(infos) => { log::info!("connected to lektord instance"); - self.lektord_state = LektordState::Connected { - version, - last_epoch, - state: None, - }; + self.lektord_state.connect_with_infos(infos).into_commands() } + // We got an update for epochs of lektord's subsystems. + LektordMessage::EpochUpdate(new) => self + .lektord_state + .map_mut(|_, epochs, _| epochs.update(new).into_commands()) + .unwrap_or_else(Command::none), + // Playback update messages from subscription. Will be ignored while the update // subscription don't send us the connect message. - PlaybackUpdate(state) => { - if let LektordState::Connected { state: ptr, .. } = &mut self.lektord_state { - *ptr = Some(state) - } + LektordMessage::PlaybackUpdate(state) => { + _ = self.lektord_state.map_mut(|_, _, ptr| *ptr = Some(state)); + Command::none() } // Down here, got updates from lektord. - ChangedAvailablePlaylists(names) => { + LektordMessage::ChangedAvailablePlaylists(names) => { let config = self.connect_config.clone(); let playlists = self.store.keep_playlists(names); - return cosmic::command::future(async move { + cosmic::command::future(async move { let updated_playlists = stream::iter(playlists) .zip(stream::repeat_with(move || config.clone())) .then(|(id, config)| async move { @@ -653,30 +614,44 @@ impl AppModel { }) .collect::<FuturesUnordered<_>>() .await; - Message::LektordUpdate(ChangedPlaylistsContent( + Message::LektordUpdate(LektordMessage::ChangedPlaylistsContent( updated_playlists.into_iter().flatten().collect::<Vec<_>>(), )) - }); + }) } - ChangedHistory(kids) => self.store.set_history(kids), + LektordMessage::ChangedHistory(kids) => { + self.store.set_history(kids); + Command::none() + } - ChangedQueueLevel(lvl, kids) => self.store.set_queue_level(lvl, kids), - ChangedQueue(mut queue) => PRIORITY_VALUES.iter().for_each(|&level| { - let kids = mem::take(&mut queue[level.index()]); - self.store.set_queue_level(level, kids); - }), + LektordMessage::ChangedQueueLevel(lvl, kids) => { + self.store.set_queue_level(lvl, kids); + Command::none() + } + LektordMessage::ChangedQueue(mut queue) => { + PRIORITY_VALUES.iter().for_each(|&level| { + let kids = mem::take(&mut queue[level.index()]); + self.store.set_queue_level(level, kids); + }); + Command::none() + } - ChangedPlaylistContent(name, plt) => self.store.set_playlist_content(name, plt), - ChangedPlaylistsContent(changes) => changes - .into_iter() - .for_each(|(name, plt)| self.store.set_playlist_content(name, plt)), + LektordMessage::ChangedPlaylistContent(name, plt) => { + self.store.set_playlist_content(name, plt); + Command::none() + } + LektordMessage::ChangedPlaylistsContent(changes) => { + changes + .into_iter() + .for_each(|(name, plt)| self.store.set_playlist_content(name, plt)); + Command::none() + } } - Command::none() } /// Send commands to lektord. - fn send_command(&self, cmd: LektordCommand) -> Command<Message> { + fn send_command(&mut self, cmd: LektordCommand) -> Command<Message> { let config = self.connect_config.clone(); use lektor_payloads::*; macro_rules! msg { @@ -739,6 +714,12 @@ impl AppModel { msg!(ChangedHistory(history)) }), + LektordCommand::DatabaseGet => { + cosmic::app::command::message(cosmic::app::message::app(Message::SendCommand( + LektordCommand::DownloadKarasInfo(self.store.take_kara_ids()), + ))) + } + LektordCommand::PlaylistGetContent(id) => cmd!(get_playlist_content(id), content => { msg!(ChangedPlaylistContent(id, content)) }), diff --git a/amadeus/src/app/bottom_bar.rs b/amadeus/src/app/bottom_bar.rs index 0510670c..890815d5 100644 --- a/amadeus/src/app/bottom_bar.rs +++ b/amadeus/src/app/bottom_bar.rs @@ -15,7 +15,7 @@ use cosmic::{ style, theme, widget::{self, tooltip::Position}, }; -use lektor_payloads::{KId, Kara, Tags}; +use lektor_payloads::{KId, Kara, TagKey}; fn view_right_part<'a>(kara: &Kara) -> Element<'a, Message> { let Spacing { @@ -75,7 +75,7 @@ fn view_left_part<'a>(kara: &Kara) -> Element<'a, Message> { )) .wrap(Wrap::None); - let source = (kara.tags.get_value(Tags::key_number())) + let source = (kara.tags.get_value(TagKey::Number)) .map(|num| format!("{}{num} - {}", kara.song_type, kara.song_source)) .unwrap_or_else(|| format!("{} - {}", kara.song_type, kara.song_source)) .apply(widget::text::title4) @@ -122,7 +122,7 @@ fn view_kara_id<'a>(kid: KId) -> Element<'a, Message> { pub fn view<'a>(app: &AppModel) -> Element<'a, Message> { match app.lektord_state.current_kid() { None => return widget::row().into(), - Some(&kid) => match app.store.get(kid) { + Some(kid) => match app.store.get(kid) { KaraOrId::Kara(kara) => vec![view_left_part(kara), view_right_part(kara)], KaraOrId::Id(kid) => vec![view_kara_id(kid)], } diff --git a/amadeus/src/app/context_pages/about.rs b/amadeus/src/app/context_pages/about.rs index da39f353..db5c1b39 100644 --- a/amadeus/src/app/context_pages/about.rs +++ b/amadeus/src/app/context_pages/about.rs @@ -4,7 +4,8 @@ use crate::{ }; use cosmic::{ iced::{Alignment, Length}, - theme, widget, Apply as _, Element, + prelude::*, + theme, widget, }; /// Show more informations about Amadeus and the connected Lektord instance (if connected.) @@ -44,9 +45,7 @@ pub fn view(app: &AppModel) -> Element<Message> { widget::text::body(fl!("disconnected")), )), LektordState::Connected { - version, - last_epoch, - .. + version, epochs, .. } => lektord_section .add(widget::settings::item( fl!("status"), @@ -57,8 +56,16 @@ pub fn view(app: &AppModel) -> Element<Message> { widget::text::body(version), )) .add(widget::settings::item( - fl!("epoch", what = "DB"), - widget::text::body(last_epoch.unwrap_or_default().to_string()), + fl!("epoch", what = fl!("database")), + widget::text::body(epochs.database.unwrap_or_default().to_string()), + )) + .add(widget::settings::item( + fl!("epoch", what = fl!("queue")), + widget::text::body(epochs.queue.to_string()), + )) + .add(widget::settings::item( + fl!("epoch", what = fl!("playlists")), + widget::text::body(epochs.playlists.to_string()), )), }; diff --git a/amadeus/src/app/kard.rs b/amadeus/src/app/kard.rs index 4889cac5..e6dc1cc4 100644 --- a/amadeus/src/app/kard.rs +++ b/amadeus/src/app/kard.rs @@ -13,7 +13,7 @@ use cosmic::{ style, theme, widget::{self, tooltip::Position}, }; -use lektor_payloads::{Kara, Tags}; +use lektor_payloads::{Kara, TagKey}; const KARD_HEIGHT: f32 = 50.0; @@ -21,7 +21,7 @@ fn kara_title<'a>(kara: &Kara) -> Element<'a, Message> { widget::column::with_children(vec![ widget::text::title4(kara.song_title.clone()).into(), kara.tags - .get_value(Tags::key_number()) + .get_value(TagKey::Number) .map(|num| format!("{}{num} - {}", kara.song_type, kara.song_source)) .unwrap_or_else(|| format!("{} - {}", kara.song_type, kara.song_source)) .apply(widget::text::text) diff --git a/amadeus/src/app/subscriptions/updates.rs b/amadeus/src/app/subscriptions/updates.rs index 1ddc792d..09cbfa23 100644 --- a/amadeus/src/app/subscriptions/updates.rs +++ b/amadeus/src/app/subscriptions/updates.rs @@ -1,4 +1,7 @@ -use crate::app::{LektordMessage, Message}; +use crate::{ + app::{LektordMessage, Message}, + connection::Epochs, +}; use cosmic::iced::{subscription, Subscription}; use futures::SinkExt; use lektor_lib::{requests, ConnectConfig}; @@ -29,10 +32,15 @@ impl Suscription { }; _ = channel.send(LektordUpdate(Connected(infos))).await; + let mut previous_epochs = Epochs::default(); loop { - log::debug!("here we want to query updates for the queue, history, etc…"); - log::debug!("on comm error we want to loop to 'connect here"); - tokio::time::sleep(Duration::from_secs(5)).await; + match requests::get_epochs(config.read().await.as_ref()).await { + Ok(epochs) if previous_epochs.update(epochs).is_any() => { + _ = channel.send(LektordUpdate(EpochUpdate(epochs))).await; + tokio::time::sleep(Duration::from_secs(5)).await; + } + _ => continue 'connect, + } } } }) diff --git a/amadeus/src/connection.rs b/amadeus/src/connection.rs new file mode 100644 index 00000000..ecd430a2 --- /dev/null +++ b/amadeus/src/connection.rs @@ -0,0 +1,177 @@ +use crate::app::Message; +use cosmic::{app::Command, Apply as _}; +use lektor_payloads::{KId, PlayStateWithCurrent}; +use std::{mem, ops}; + +/// Tells which subsystems of lektord to query because their epoch changed. +#[derive(Debug, Clone, Copy, Default)] +pub struct LektordQueryChanges { + pub database: bool, + pub queue: bool, + pub playlists: bool, +} + +impl LektordQueryChanges { + /// We need to query all the subsystems of lektord. + pub fn all() -> Self { + Self { + database: true, + queue: true, + playlists: true, + } + } + + /// Do we need any update here? + pub fn is_any(&self) -> bool { + let Self { + database, + queue, + playlists, + } = *self; + database || queue || playlists + } + + /// Get the commands to send to do the refresh. + pub fn into_commands(self) -> Command<Message> { + use {crate::app::LektordCommand::*, Message::*}; + match self.is_any() { + false => Command::none(), + true => [ + self.queue.then_some(SendCommand(QueueGet)), + self.queue.then_some(SendCommand(HistoryGet)), + self.playlists.then_some(SendCommand(PlaylistsGet)), + self.database.then_some(SendCommand(DatabaseGet)), + ] + .into_iter() + .flatten() + .map(cosmic::app::message::app) + .map(cosmic::app::command::message) + .apply(Command::batch), + } + } +} + +#[derive(Debug, Clone, Copy, Default)] +pub struct Epochs(lektor_payloads::Epochs); + +impl Epochs { + pub fn update(&mut self, new: impl Into<Epochs>) -> LektordQueryChanges { + let Epochs(old) = mem::replace(self, new.into()); + LektordQueryChanges { + queue: self.queue > old.queue, + playlists: self.playlists > old.playlists, + database: match (old.database, self.database) { + (None, None) | (Some(_), None) => false, + (None, Some(_)) => true, + (Some(old), Some(new)) => new > old, + }, + } + } +} + +impl From<lektor_payloads::Epochs> for Epochs { + fn from(value: lektor_payloads::Epochs) -> Self { + Self(value) + } +} + +impl ops::Deref for Epochs { + type Target = lektor_payloads::Epochs; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +/// The state of lektord that we queried. +#[derive(Debug, Clone, Default)] +pub enum LektordState { + /// Lektord is disconnected. + #[default] + Disconnected, + + /// Lektord is connected. + Connected { + /// A version string, which is the build string from git. + version: String, + + /// The different epochs of the subsystems of lektord. + epochs: Epochs, + + /// The state of the playback. + state: Option<PlayStateWithCurrent>, + }, +} + +impl LektordState { + pub fn map<T>( + &self, + cb: impl FnOnce(&str, Epochs, Option<&PlayStateWithCurrent>) -> T, + ) -> Option<T> { + match self { + LektordState::Disconnected => None, + LektordState::Connected { + version, + epochs, + state, + } => Some(cb(version, *epochs, state.as_ref())), + } + } + + pub fn map_mut<T>( + &mut self, + cb: impl FnOnce(&mut String, &mut Epochs, &mut Option<PlayStateWithCurrent>) -> T, + ) -> Option<T> { + match self { + LektordState::Disconnected => None, + LektordState::Connected { + version, + epochs, + state, + } => Some(cb(version, epochs, state)), + } + } + + /// Get the current kara we are playing, if any. + pub fn current_kid(&self) -> Option<KId> { + self.map(|_, _, state| { + (state.as_ref()).and_then(|PlayStateWithCurrent { current, .. }| { + current.as_ref().map(|(kid, ..)| *kid) + }) + })? + } + + /// Get the times, first the current time in the kara, then the duration of the current kara, + /// if any. + pub fn current_times(&self) -> Option<(f32, f32)> { + self.map(|_, _, state| { + (state.as_ref()).and_then(|PlayStateWithCurrent { current, .. }| { + (current.as_ref()).map(|(_, elapse, duration)| (*elapse, *duration)) + }) + })? + } + + /// If we have [lektor_payloads::Infos], then we have a connection to the lektord instance. + /// Simply convert it into [LektordState::Connected] status. If we are already connected, + /// update the status. + /// + /// Returns which epochs as been updated, to tell amadeus to query the changes. + pub fn connect_with_infos(&mut self, infos: lektor_payloads::Infos) -> LektordQueryChanges { + match self { + LektordState::Disconnected => { + *self = Self::Connected { + version: infos.version, + epochs: Epochs(infos.epochs), + state: None, + }; + LektordQueryChanges::all() + } + LektordState::Connected { + epochs, version, .. + } => { + *version = infos.version; + epochs.update(infos.epochs) + } + } + } +} diff --git a/amadeus/src/lib.rs b/amadeus/src/lib.rs index eef7803a..b0f036be 100644 --- a/amadeus/src/lib.rs +++ b/amadeus/src/lib.rs @@ -1,6 +1,8 @@ pub mod app; pub mod config; pub mod i18n; + +mod connection; mod icons; mod playlist; mod store; diff --git a/amadeus/src/store.rs b/amadeus/src/store.rs index 6c1c7fba..68338c7b 100644 --- a/amadeus/src/store.rs +++ b/amadeus/src/store.rs @@ -73,6 +73,11 @@ impl Store { pub fn set(&mut self, kara: Kara) { let _ = self.karas.insert(kara.id, kara); } + + /// Take all the kara ids of the database, emptying it in the process. + pub fn take_kara_ids(&mut self) -> Vec<KId> { + mem::take(&mut self.karas).into_keys().collect() + } } impl Store { diff --git a/lektor_lib/src/config.rs b/lektor_lib/src/config.rs index 41445774..d09b3d85 100644 --- a/lektor_lib/src/config.rs +++ b/lektor_lib/src/config.rs @@ -1,7 +1,7 @@ use lektor_utils::config::SocketScheme; use std::{ net::{IpAddr, Ipv4Addr, SocketAddr}, - sync::Arc, time::Duration, + time::Duration, }; /// The config to use to connect to the remote. @@ -14,9 +14,6 @@ pub struct ConnectConfig { pub retry: Duration, } -/// Ref-counted thingy. -pub type ConnectConfigPtr = Arc<ConnectConfig>; - impl Default for ConnectConfig { fn default() -> Self { Self { diff --git a/lektor_lib/src/requests.rs b/lektor_lib/src/requests.rs index fc66474a..8db36c8a 100644 --- a/lektor_lib/src/requests.rs +++ b/lektor_lib/src/requests.rs @@ -10,7 +10,7 @@ use reqwest::{ header::{self, HeaderValue}, Body, ClientBuilder, Method, Request, Url, }; -use std::sync::Arc; +use std::{sync::Arc, thread, time::Duration}; const USER_AGENT: &str = concat!(env!("CARGO_PKG_NAME"), "/", env!("CARGO_PKG_VERSION")); @@ -66,22 +66,33 @@ pub async fn get_infos(config: impl AsRef<ConnectConfig>) -> Result<Infos> { request!(config; GET @ "/" => Infos) } +pub async fn get_epochs(config: impl AsRef<ConnectConfig>) -> Result<Epochs> { + request!(config; GET @ "/epochs" => Epochs) +} + pub async fn get_status(config: impl AsRef<ConnectConfig>) -> Result<PlayStateWithCurrent> { request!(config; GET @ "/playback/state" => PlayStateWithCurrent) } +/// Note that we will poll karas by a predefined chunk size, sleeping a bit between each size to +/// mitigate big downloads of kara informations… pub async fn get_karas_by_kid( config: impl AsRef<ConnectConfig>, kids: Vec<KId>, ) -> Result<Vec<Kara>> { - let config = config.as_ref(); - Ok(stream::iter(kids) - .then(|kid| async move { get_kara_by_kid(config, kid).await }) - .collect::<FuturesUnordered<_>>() - .await - .into_iter() - .flatten() - .collect()) + let mut res = Vec::with_capacity(kids.len()); + for kids in kids.chunks(10) { + res.extend( + stream::iter(kids.iter().copied()) + .then(|kid| get_kara_by_kid(&config, kid)) + .collect::<FuturesUnordered<_>>() + .await + .into_iter() + .flatten(), + ); + thread::sleep(Duration::from_secs_f32(0.1)); + } + Ok(res) } pub async fn get_kara_by_kid(config: impl AsRef<ConnectConfig>, kid: KId) -> Result<Kara> { diff --git a/lektor_nkdb/src/kara/tags.rs b/lektor_nkdb/src/kara/tags.rs index 203248b9..0c8edfe0 100644 --- a/lektor_nkdb/src/kara/tags.rs +++ b/lektor_nkdb/src/kara/tags.rs @@ -9,20 +9,17 @@ use std::{borrow, mem, sync::Arc}; /// /// Note that the keys are always in lowercase english. It is up to the frontend to traduce them if /// needed and title case them. -#[derive(Default, Debug, Clone, Serialize, Deserialize)] +#[derive(Debug, Clone, Serialize, Deserialize)] pub struct Tags(HashMap<TagKey, TagValue>); -impl Tags { - /// Get the key for the `Number` tag. - pub fn key_number() -> &'static str { - TagKey::Number.as_str() - } - - /// Get the key for the `Version` tag. - pub fn key_version() -> &'static str { - TagKey::Version.as_str() +impl Default for Tags { + fn default() -> Self { + const TAGS_DEFAULT_CAPACITY: usize = 5; + Self(HashMap::with_capacity(TAGS_DEFAULT_CAPACITY)) } +} +impl Tags { /// Tells wether a tag is present or not. pub fn has_tag(&self, tag: impl AsRef<str>) -> bool { self.0.contains_key(tag.as_ref()) @@ -56,8 +53,7 @@ impl Tags { /// Iterate over the values of a tag. pub fn iter_values(&self, tag: impl AsRef<str>) -> impl Iterator<Item = &str> { - self.0 - .get(tag.as_ref()) + (self.0.get(tag.as_ref())) .map(TagValueIter::from) .into_iter() .flatten() @@ -246,7 +242,7 @@ impl<'a> Iterator for TagValueIter<'a> { /// The key of a tag. Don't leak this type. Note that the keys are always in lowercase. #[derive(Clone, Display, Hash, PartialEq, Eq, Debug, Serialize, Deserialize)] #[display("{}", self.as_str())] -enum TagKey { +pub enum TagKey { /// Can only have one number per kara. Number, @@ -271,13 +267,8 @@ impl borrow::Borrow<str> for TagKey { impl From<&str> for TagKey { fn from(value: &str) -> Self { - if TagKey::Number.as_str() == value { - TagKey::Number - } else if TagKey::Version.as_str() == value { - TagKey::Version - } else { - TagKey::Other(Arc::from(value.to_lowercase())) - } + TagKey::try_new_without_allocation(value) + .unwrap_or_else(|| TagKey::Other(Arc::from(value.to_lowercase()))) } } @@ -292,7 +283,17 @@ impl From<TagKey> for Arc<str> { } impl TagKey { - fn as_str(&self) -> &str { + fn try_new_without_allocation(value: &str) -> Option<Self> { + if TagKey::Number.as_str() == value { + Some(TagKey::Number) + } else if TagKey::Version.as_str() == value { + Some(TagKey::Version) + } else { + None + } + } + + pub fn as_str(&self) -> &str { match self { TagKey::Number => "number", TagKey::Version => "version", diff --git a/lektor_nkdb/src/lib.rs b/lektor_nkdb/src/lib.rs index b744897d..16ab58f9 100644 --- a/lektor_nkdb/src/lib.rs +++ b/lektor_nkdb/src/lib.rs @@ -3,7 +3,12 @@ pub use crate::{ database::{epoch::Epoch, update::UpdateHandler}, id::{KId, RemoteKId}, - kara::{status::KaraStatus, tags::Tags, timestamps::KaraTimeStamps, Kara}, + kara::{ + status::KaraStatus, + tags::{TagKey, Tags}, + timestamps::KaraTimeStamps, + Kara, + }, playlists::playlist::{Playlist, PlaylistInfo}, storage::{DatabaseDiskStorage, DatabaseStorage}, }; diff --git a/lektor_nkdb/src/playlists/mod.rs b/lektor_nkdb/src/playlists/mod.rs index 94595a47..eda67c68 100644 --- a/lektor_nkdb/src/playlists/mod.rs +++ b/lektor_nkdb/src/playlists/mod.rs @@ -30,7 +30,7 @@ impl<'a, Storage: DatabaseStorage + Sized> PlaylistsHandle<'a, Storage> { } /// Get the number of modifications since startup of the [Playlist], the epoch. - pub async fn epoch(&self) -> u64 { + pub fn epoch(&self) -> u64 { self.playlists.epoch.load(Ordering::Acquire) } diff --git a/lektor_payloads/src/action.rs b/lektor_payloads/src/action.rs new file mode 100644 index 00000000..d5c871f1 --- /dev/null +++ b/lektor_payloads/src/action.rs @@ -0,0 +1,75 @@ +use crate::{KaraBy, KaraFilter, Priority}; +use anyhow::{anyhow, ensure}; +use serde::{Deserialize, Serialize}; + +/// Actions possible to update a sub-range of the database queue. +#[derive(Debug, Deserialize, Serialize)] +pub enum QueueUpdateAction { + /// Shuffle, but kara won't change priorities. + Shuffle, + + /// Delete the range from the queue. + Delete, + + /// Move the range after the kara, inheriting the priority of the kara to move after. + MoveAfter(usize), +} + +/// Update a playlist with an action. +#[derive(Debug, Serialize, Deserialize)] +pub enum PlaylistUpdate { + RemoveQuery(String), + AddQuery(String), + RemoveId(u64), + AddId(u64), +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AddArguments { + pub level: Priority, + pub query: KaraBy, +} + +/// Add to the queue. We add at a certain priority, some times we can decide to shuffle the set of +/// kara/the playlist before adding it to the queue. +#[derive(Debug, Serialize, Deserialize)] +pub struct QueueAddAction { + pub priority: Priority, + pub action: KaraFilter, +} + +/// Update actions on playlists. +#[derive(Debug, Serialize, Deserialize)] +pub enum PlaylistUpdateAction { + /// Delete the playlist. + Delete, + + /// Give the playlist to another user. + GiveTo(Box<str>), + + /// Rename the playlist. + Rename(String), + + /// Add a kara to the playlist. + Add(KaraFilter), + + /// Remove a kara from the playlist. + Remove(KaraFilter), +} + +impl std::str::FromStr for AddArguments { + type Err = anyhow::Error; + fn from_str(s: &str) -> Result<Self, Self::Err> { + ensure!(!s.is_empty(), "the string is empty"); + match s.split_once(' ') { + Some((lvl, query)) => Ok(AddArguments { + level: lvl.parse().map_err(|err| anyhow!("{err}"))?, + query: query.parse()?, + }), + None => Ok(AddArguments { + level: Priority::Add, + query: s.parse()?, + }), + } + } +} diff --git a/lektor_payloads/src/error.rs b/lektor_payloads/src/error.rs deleted file mode 100644 index e1b63cc6..00000000 --- a/lektor_payloads/src/error.rs +++ /dev/null @@ -1,40 +0,0 @@ -use axum::{http::StatusCode, response::IntoResponse}; - -#[derive(Debug, thiserror::Error)] -pub enum RangeError { - #[error("invalid range, should at least contains '..'")] - InvalidRange, - - #[error("invalid bound: {0}")] - InvalidBound(#[from] std::num::ParseIntError), -} - -#[derive(Debug, thiserror::Error)] -pub enum UserIdError { - #[error("failed to convert header to string: {0}")] - ToStr(#[from] axum::http::header::ToStrError), - - #[error("the header didn't contains informations needed for the user identification & authentification")] - HeadersNotFound, - - #[error("the user is not authentified")] - NotAuthentified, - - #[error("the user is not an admin")] - NotAnAdmin, - - #[error("the received header is invalid")] - InvalidHeader, -} - -impl IntoResponse for RangeError { - fn into_response(self) -> axum::response::Response { - (StatusCode::NOT_ACCEPTABLE, format!("{self}")).into_response() - } -} - -impl IntoResponse for UserIdError { - fn into_response(self) -> axum::response::Response { - (StatusCode::NOT_ACCEPTABLE, format!("{self}")).into_response() - } -} diff --git a/lektor_payloads/src/lib.rs b/lektor_payloads/src/lib.rs index 46dac1d7..c7cafdf2 100644 --- a/lektor_payloads/src/lib.rs +++ b/lektor_payloads/src/lib.rs @@ -1,109 +1,20 @@ //! Crate containing structs/enums that are used as payloads to communicate with the lektord //! daemon. Some things are re-exports. +mod action; mod filter; mod play_state; mod priority; mod range; mod search; +mod status; mod userid; pub use crate::{ - filter::*, - play_state::*, - priority::{Priority, PRIORITY_LENGTH, PRIORITY_VALUES}, - range::*, - search::*, + action::*, filter::*, play_state::*, priority::*, range::*, search::*, status::*, userid::LektorUser, }; pub use lektor_nkdb::{ KId, Kara, KaraStatus, KaraTimeStamps, Playlist, PlaylistInfo, RemoteKId, SongOrigin, SongType, - Tags, SONGORIGIN_LENGTH, SONGTYPE_LENGTH, + TagKey, Tags, SONGORIGIN_LENGTH, SONGTYPE_LENGTH, }; - -use anyhow::{anyhow, ensure}; -use serde::{Deserialize, Serialize}; - -#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone)] -pub struct Infos { - pub version: String, - pub last_epoch: Option<u64>, -} - -#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] -pub struct PlayStateWithCurrent { - pub state: PlayState, - pub current: Option<(KId, f32, f32)>, -} - -/// Actions possible to update a sub-range of the database queue. -#[derive(Debug, Deserialize, Serialize)] -pub enum QueueUpdateAction { - /// Shuffle, but kara won't change priorities. - Shuffle, - - /// Delete the range from the queue. - Delete, - - /// Move the range after the kara, inheriting the priority of the kara to move after. - MoveAfter(usize), -} - -/// Update a playlist with an action. -#[derive(Debug, Serialize, Deserialize)] -pub enum PlaylistUpdate { - RemoveQuery(String), - AddQuery(String), - RemoveId(u64), - AddId(u64), -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct AddArguments { - pub level: Priority, - pub query: KaraBy, -} - -/// Add to the queue. We add at a certain priority, some times we can decide to shuffle the set of -/// kara/the playlist before adding it to the queue. -#[derive(Debug, Serialize, Deserialize)] -pub struct QueueAddAction { - pub priority: Priority, - pub action: KaraFilter, -} - -/// Update actions on playlists. -#[derive(Debug, Serialize, Deserialize)] -pub enum PlaylistUpdateAction { - /// Delete the playlist. - Delete, - - /// Give the playlist to another user. - GiveTo(Box<str>), - - /// Rename the playlist. - Rename(String), - - /// Add a kara to the playlist. - Add(KaraFilter), - - /// Remove a kara from the playlist. - Remove(KaraFilter), -} - -impl std::str::FromStr for AddArguments { - type Err = anyhow::Error; - fn from_str(s: &str) -> Result<Self, Self::Err> { - ensure!(!s.is_empty(), "the string is empty"); - match s.split_once(' ') { - Some((lvl, query)) => Ok(AddArguments { - level: lvl.parse().map_err(|err| anyhow!("{err}"))?, - query: query.parse()?, - }), - None => Ok(AddArguments { - level: Priority::Add, - query: s.parse()?, - }), - } - } -} diff --git a/lektor_payloads/src/status.rs b/lektor_payloads/src/status.rs new file mode 100644 index 00000000..07e82028 --- /dev/null +++ b/lektor_payloads/src/status.rs @@ -0,0 +1,23 @@ +//! Contains structs about getting informations about the lektord instance. + +use crate::{KId, PlayState}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Serialize, Deserialize, Clone)] +pub struct Infos { + pub version: String, + pub epochs: Epochs, +} + +#[derive(Default, Debug, Serialize, Deserialize, Clone, Copy)] +pub struct Epochs { + pub database: Option<u64>, + pub queue: u64, + pub playlists: u64, +} + +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Default)] +pub struct PlayStateWithCurrent { + pub state: PlayState, + pub current: Option<(KId, f32, f32)>, +} diff --git a/lektord/src/app/mod.rs b/lektord/src/app/mod.rs index be5a06b9..a9ad1295 100644 --- a/lektord/src/app/mod.rs +++ b/lektord/src/app/mod.rs @@ -17,8 +17,8 @@ use axum::{ routing::{delete, get, post, put}, }; use lektor_mpris::MPRISAdapter; -use lektor_nkdb::{Database, DatabaseDiskStorage}; -use lektor_payloads::LektorUser; +use lektor_nkdb::{Database, DatabaseDiskStorage, Epoch}; +use lektor_payloads::{Epochs, LektorUser}; use lektor_repo::Repo; use lektor_utils::config::LektorDatabaseConfig; use play_state::StoredPlayState; @@ -70,7 +70,8 @@ pub async fn app( .await .with_context(|| "failed to build lektord state")?; let route = router! { - "/" -> get: routes::root + "/" -> get: routes::root + ; "/epochs" -> get: routes::epochs // Control the playback ; "/playback/state" -> get: routes::get_state @@ -234,6 +235,15 @@ impl LektorState { user } + /// Get the differents epochs of the different subsystems. + async fn get_epochs(&self) -> Epochs { + Epochs { + database: self.database.last_epoch().await.map(Epoch::num), + queue: self.queue().epoch(), + playlists: self.database.playlists().epoch(), + } + } + /// Get a handle to read or write the queue, alongside the playstate. pub(crate) fn queue(&self) -> QueueHandle { QueueHandle::new(&self.queue, &self.playstate) diff --git a/lektord/src/app/routes.rs b/lektord/src/app/routes.rs index c4f09ecd..b158691e 100644 --- a/lektord/src/app/routes.rs +++ b/lektord/src/app/routes.rs @@ -13,7 +13,6 @@ use axum::{ response::{IntoResponse, Response}, Json, }; -use futures::prelude::*; use lektor_nkdb::*; use lektor_payloads::*; use lektor_utils::decode_base64_json; @@ -26,14 +25,16 @@ use tokio::task::LocalSet; pub(crate) async fn root(State(state): State<LektorStatePtr>) -> Json<Infos> { Json(Infos { version: crate::version().to_string(), - last_epoch: state - .database - .last_epoch() - .map(|epoch| epoch.map(Epoch::num)) - .await, + epochs: state.get_epochs().await, }) } +/// Get epochs abount the lektord server. +#[axum::debug_handler(state = LektorStatePtr)] +pub(crate) async fn epochs(State(state): State<LektorStatePtr>) -> Json<Epochs> { + Json(state.get_epochs().await) +} + /// Get the play state of the server. Be aware of race conditions... #[axum::debug_handler(state = LektorStatePtr)] pub(crate) async fn get_state(State(state): State<LektorStatePtr>) -> Json<PlayStateWithCurrent> { @@ -43,10 +44,10 @@ pub(crate) async fn get_state(State(state): State<LektorStatePtr>) -> Json<PlayS .await; let current = current.map(|id| { let elapsed = c_wrapper::player_get_elapsed() - .map_err(|err| log::error!("{err}")) + .map_err(|err| log::error!("failed to get state: {err}")) .unwrap_or_default(); let duration = c_wrapper::player_get_duration() - .map_err(|err| log::error!("{err}")) + .map_err(|err| log::error!("failed to get state: {err}")) .unwrap_or_default(); (id, elapsed as f32, duration as f32) }); diff --git a/lkt/src/config.rs b/lkt/src/config.rs index 43f63164..90bb5fb1 100644 --- a/lkt/src/config.rs +++ b/lkt/src/config.rs @@ -40,18 +40,6 @@ impl From<&LktConfig> for lektor_lib::ConnectConfig { } } -impl From<LktConfig> for lektor_lib::ConnectConfigPtr { - fn from(value: LktConfig) -> Self { - lektor_lib::ConnectConfig::from(value).into() - } -} - -impl From<&LktConfig> for lektor_lib::ConnectConfigPtr { - fn from(value: &LktConfig) -> Self { - lektor_lib::ConnectConfig::from(value).into() - } -} - impl Default for LktConfig { fn default() -> Self { Self { diff --git a/lkt/src/lib.rs b/lkt/src/lib.rs index db3d322c..051637ef 100644 --- a/lkt/src/lib.rs +++ b/lkt/src/lib.rs @@ -65,10 +65,7 @@ pub async fn exec_lkt(config: LktConfig, cmd: SubCommand) -> Result<()> { // Display the status of lektord Playback { status: true, .. } => { - let Infos { - version, - last_epoch, - } = requests::get_infos(config).await?; + let Infos { version, epochs } = requests::get_infos(config).await?; let PlayStateWithCurrent { state, current } = requests::get_status(config).await?; let current = match current { None => None, @@ -80,7 +77,7 @@ pub async fn exec_lkt(config: LktConfig, cmd: SubCommand) -> Result<()> { }; let queue_counts = requests::get_queue_count(config).await?; let history_count = requests::get_history_count(config).await?; - let last_epoch = match last_epoch { + let db_epoch = match epochs.database { Some(num) => num.to_string().into(), None => Cow::Borrowed("None"), }; @@ -94,7 +91,9 @@ pub async fn exec_lkt(config: LktConfig, cmd: SubCommand) -> Result<()> { } println!("Karas in Queue: {queue_counts:?}"); println!("Karas in Hustory: {history_count}"); - println!("Database epoch: {last_epoch}"); + println!("Database epoch: {db_epoch}"); + println!("Queue epoch: {}", epochs.queue); + println!("Playlists epoch: {}", epochs.playlists); Ok(()) } -- GitLab