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