diff --git a/CMakeLists.txt b/CMakeLists.txt
index 0421d3368bbf1ffc58230409ae81c941d842a641..66bad3c783d8daaefb315b3393446ad7a412505e 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -334,7 +334,7 @@ ExternalProject_Add(amadeus_rs
     DOWNLOAD_COMMAND  ""
     CONFIGURE_COMMAND ""
     INSTALL_COMMAND   ""
-    SOURCE_DIR        "${CMAKE_SOURCE_DIR}/src/rust/amadeus-rs"
+    SOURCE_DIR        "${CMAKE_SOURCE_DIR}/src/rust/amadeus-next"
     BUILD_COMMAND     ${RUST_BUILD_CMD}
     COMMAND           ${RUST_BUILD_CMD}
     BUILD_BYPRODUCTS  "${CMAKE_SOURCE_DIR}/src/rust/target/${RUST_BUILD_TYPE}/amadeus"
diff --git a/README.md b/README.md
index 5258599c7aec12d6ddccd4d20a9ac816b9bd34c0..4c81b61fdf9b64f09cee4b939fd1ff91e4f2a42b 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# lektor mk7
+# Lektor mk7
 
 [![pipeline status](https://git.iiens.net/martin2018/lektor/badges/master/pipeline.svg)](https://git.iiens.net/martin2018/lektor/-/commits/master)
 [![matrix chat](https://img.shields.io/badge/matrix-%23baka--dev--lektor%3Aiiens.net-brightgreen)](https://matrix.to/#/#baka-dev-lektor:iiens.net)
@@ -93,7 +93,7 @@ inside the lektor's config file.
 ### Launch instructions
 
 To run lektor, you can simply run the binary like: `./lektord`. If lektord did
-not exited normally (i.e. without the `lkt adm kill` command), the database will
+not exit normally (i.e. without the `lkt adm kill` command), the database will
 still store the fact that lektord is running. To by-pass it, you will need to
 launch lektord with the `-F` (forced) option, like `lektord -F`. It is not
 recommended to launch always lektord with the `-F` option, because that way you
diff --git a/lektor.code-workspace b/lektor.code-workspace
index 73cb939728b97d2a0e214fd1589442fcf92ef68b..92cad9d639e75851c03645ed9c8f0d43153f9748 100644
--- a/lektor.code-workspace
+++ b/lektor.code-workspace
@@ -20,6 +20,10 @@
             "path": "src/rust/amadeus-rs",
             "name": "Amadeus RS Sources"
         },
+        {
+            "path": "src/rust/amadeus-next",
+            "name": "Amadeus Next RS Sources"
+        },
     ],
     "settings": {
         "ltex.language": "en",
diff --git a/src/rust/amadeus-next/Cargo.toml b/src/rust/amadeus-next/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..c6675a90c1242b1ff3a303f580caee3e22eaa7f8
--- /dev/null
+++ b/src/rust/amadeus-next/Cargo.toml
@@ -0,0 +1,31 @@
+[profile.release]
+strip = true
+lto = true
+opt-level = "s"
+codegen-units = 1
+
+[workspace]
+resolver = "2"
+members = ["amadeus", "amalib", "commons", "lkt-rs"]
+
+[workspace.package]
+edition = "2021"
+authors = ["Maƫl MARTIN"]
+version = "0.1.0"
+license = "MIT"
+
+[workspace.dependencies]
+log = "0.4"
+lazy_static = "1"
+serde = { version = "^1", default-features = false, features = [
+    "std",
+    "derive",
+] }
+tokio = { version = "1", default-features = false, features = [
+    "rt",
+    "net",
+    "time",
+    "macros",
+    "sync",
+    "io-util",
+] }
diff --git a/src/rust/amadeus-next/amadeus/Cargo.toml b/src/rust/amadeus-next/amadeus/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..804d6c43ab7ba2bbacee8369ca165ef583154f5a
--- /dev/null
+++ b/src/rust/amadeus-next/amadeus/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "amadeus"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+log.workspace = true
+serde.workspace = true
+tokio.workspace = true
diff --git a/src/rust/amadeus-next/amadeus/src/main.rs b/src/rust/amadeus-next/amadeus/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f328e4d9d04c31d0d70d16d21a07d1613be9d577
--- /dev/null
+++ b/src/rust/amadeus-next/amadeus/src/main.rs
@@ -0,0 +1 @@
+fn main() {}
diff --git a/src/rust/amadeus-next/amalib/Cargo.toml b/src/rust/amadeus-next/amalib/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..001f251e0515ce6e620d33e8ee8c836239ed6d2f
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/Cargo.toml
@@ -0,0 +1,13 @@
+[package]
+name = "amalib"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+log.workspace = true
+serde.workspace = true
+tokio.workspace = true
+
+commons = { path = "../commons" }
diff --git a/src/rust/amadeus-next/amalib/src/connexion.rs b/src/rust/amadeus-next/amalib/src/connexion.rs
new file mode 100644
index 0000000000000000000000000000000000000000..29de6762f7efd20209b47211dcef936e10982ee7
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/src/connexion.rs
@@ -0,0 +1,298 @@
+use crate::*;
+use tokio::net::TcpStream;
+
+pub struct LektorConnexion {
+    version: String,
+    stream: TcpStream,
+}
+
+pub struct LektorIdleConnexion {
+    version: String,
+    stream: TcpStream,
+    idle_list: Vec<String>,
+}
+
+/// Write a string into the socket. The buffer being send must be valid,
+/// i.e. must not exed the [constants::LKT_MESSAGE_MAX] counting the
+/// ending new line `\n`.
+async fn write_string(
+    stream: &mut TcpStream,
+    buffer: impl AsRef<str>,
+) -> StackedResult<(), LektorCommError> {
+    use LektorCommError::*;
+    let buffer = buffer.as_ref();
+    let size = buffer.len();
+    if size >= constants::LKT_MESSAGE_MAX {
+        err_report!(err BufferTooBig(size))
+    } else if !buffer.ends_with('\n') {
+        err_report!(err BufferDontEndWithLF)
+    } else {
+        loop {
+            stream.writable().await.map_err(Io)?;
+            match stream.try_write(buffer.as_bytes()) {
+                Ok(n) if n != size => break err_report!(err IncorrectWriteSize(size, n)),
+                Ok(_) => break Ok(()),
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
+                Err(e) => break err_report!(err Io(e)),
+            }
+        }
+    }
+}
+
+/// Read replies from the lektord server.
+async fn read_replies(
+    stream: &mut TcpStream,
+) -> StackedResult<(Vec<String>, Option<usize>), LektorCommError> {
+    let (mut ret, mut continuation) = (Vec::new(), None);
+    loop {
+        stream
+                .readable().await
+                .map_err(|e| err_report!(LektorCommError::Io(e) => [ format!("failed to get MPD version from {:?}", stream.peer_addr()) ]))?;
+        let mut line = [0; constants::LKT_MESSAGE_MAX];
+        match stream.try_read(&mut line) {
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
+            Err(e) => return err_report!(err LektorCommError::Io(e)),
+            Ok(0) => {
+                return err_report!(err LektorCommError::Io(std::io::Error::new(std::io::ErrorKind::Other, "recieved empty line")))
+            }
+            Ok(size) => {
+                let msg = std::str::from_utf8(&line[..size])
+                        .map_err(|e| err_report!(LektorCommError::Utf8(e) => [ format!("the lektord server at {:?} returned an invalid utf8 string", stream.peer_addr()) ]))?
+                        .trim();
+                match LektorQueryLineType::from_str(msg) {
+                    Ok(LektorQueryLineType::Ok) => return Ok((ret, continuation)),
+                    Ok(LektorQueryLineType::Ack) => {
+                        return err_report!(err LektorCommError::Io(std::io::Error::from(
+                            std::io::ErrorKind::Other
+                        )))
+                    }
+                    Ok(LektorQueryLineType::Data) => ret.push(msg.to_string()),
+                    Ok(LektorQueryLineType::ListOk) => continue,
+                    Ok(LektorQueryLineType::Continuation(cont)) => continuation = Some(cont),
+                    Err(()) => {
+                        return err_report!(err LektorCommError::QueryError => [ "unknown query line type" ])
+                    }
+                }
+            }
+        }
+    }
+}
+
+/// Read replies from the lektord server. If nothing is available returns
+/// nothing! This function do the same thing as [read_replies] but breaks on
+/// [std::io::ErrorKind::WouldBlock].
+async fn read_maybe_replies(
+    stream: &mut TcpStream,
+) -> StackedResult<Option<(Vec<String>, Option<usize>)>, LektorCommError> {
+    let (mut ret, mut continuation) = (Vec::new(), None);
+    loop {
+        stream
+                .readable().await
+                .map_err(|e| err_report!(LektorCommError::Io(e) => [ format!("failed to get MPD version from {:?}", stream.peer_addr()) ]))?;
+        let mut line = [0; constants::LKT_MESSAGE_MAX];
+        match stream.try_read(&mut line) {
+            Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => return Ok(None),
+            Err(e) => return err_report!(err LektorCommError::Io(e)),
+            Ok(0) => {
+                return err_report!(err LektorCommError::Io(std::io::Error::new(std::io::ErrorKind::Other, "recieved empty line")))
+            }
+            Ok(size) => {
+                let msg = std::str::from_utf8(&line[..size])
+                        .map_err(|e| err_report!(LektorCommError::Utf8(e) => [ format!("the lektord server at {:?} returned an invalid utf8 string", stream.peer_addr()) ]))?
+                        .trim();
+                match LektorQueryLineType::from_str(msg) {
+                    Ok(LektorQueryLineType::Ok) => return Ok(Some((ret, continuation))),
+                    Ok(LektorQueryLineType::Ack) => {
+                        return err_report!(err LektorCommError::Io(std::io::Error::from(
+                            std::io::ErrorKind::Other
+                        )))
+                    }
+                    Ok(LektorQueryLineType::Data) => ret.push(msg.to_string()),
+                    Ok(LektorQueryLineType::ListOk) => continue,
+                    Ok(LektorQueryLineType::Continuation(cont)) => continuation = Some(cont),
+                    Err(()) => {
+                        return err_report!(err LektorCommError::QueryError => [ "unknown query line type" ])
+                    }
+                }
+            }
+        }
+    }
+}
+
+declare_err_type!(pub LektorCommError {
+    #[error("lektord server didn't return OK code")]
+    MpdAck,
+
+    #[error("got an io error: {0}")]
+    Io(std::io::Error),
+
+    #[error("invalid utf8 string found: {0}")]
+    Utf8(std::str::Utf8Error),
+
+    #[error("invalid buffer of length {0}: too big")]
+    BufferTooBig(usize),
+
+    #[error("invalid buffer: don't end with LF `\\n`")]
+    BufferDontEndWithLF,
+
+    #[error("failed to write all the buffer of size {0}, only wrote {1} bytes")]
+    IncorrectWriteSize(usize, usize),
+
+    #[error("query failed validation test")]
+    InvalidQuery,
+
+    #[error("failed to convert the unformated response into a formated one")]
+    ToFormatedResponse,
+
+    #[error("the lektord server returned an error with the sent query(ies)")]
+    QueryError,
+});
+
+impl LektorConnexion {
+    /// Create a new connexion to a lektord server. If the versions mismatch we
+    /// log an error but continue...
+    pub async fn new(hostname: impl AsRef<str>, port: i16) -> StackedResult<Self, LektorCommError> {
+        let addr = format!("{}:{port}", hostname.as_ref());
+        let stream: TcpStream = TcpStream::connect(&addr).await.map_err(
+            |e| err_report!(LektorCommError::Io(e) => [ format!("faild to connect to {addr}") ]),
+        )?;
+        info!(
+            "connected to {addr} from local address {:?}",
+            stream.local_addr().map_err(
+                |e| err_report!(LektorCommError::Io(e) => [ format!("faild to connect to {addr}") ])
+            )?
+        );
+
+        let version: String = loop {
+            stream.readable().await.map_err(|e| err_report!(LektorCommError::Io(e) => [ format!("failed to get MPD version from {addr}") ]))?;
+            let mut version = [0; constants::LKT_MESSAGE_MAX];
+            match stream.try_read(&mut version) {
+                Ok(n) => {
+                    break std::str::from_utf8(&version[..n])
+                        .map_err(|e| err_report!(LektorCommError::Utf8(e) => [ format!("failed to get MPD version from {addr}") ]))?
+                        .trim()
+                        .to_string()
+                }
+                Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => continue,
+                Err(e) => return err_report!(err LektorCommError::Io(e)),
+            }
+        };
+
+        either!(version == format!("OK MPD {}", constants::MPD_VERSION)
+            => info!("connecting to lektord with MPD version {version}")
+            ;  error!("got MPD version {version} from {addr}, but amalib is compatible with {}", constants::MPD_VERSION)
+        );
+
+        Ok(Self { version, stream })
+    }
+
+    pub fn version(&self) -> &str {
+        &self.version
+    }
+
+    /// Send a query to the lektord server.
+    pub async fn send(
+        &mut self,
+        query: LektorQuery,
+    ) -> StackedResult<LektorResponse, LektorCommError> {
+        let mut res: Vec<String> = Vec::new();
+        query
+            .verify()
+            .map_err(|e| err_report!(LektorCommError::InvalidQuery => [ e ]))?;
+        let builder = query.get_response_type();
+
+        self.send_query_inner(query, &mut res).await?;
+        let formated = LektorFormatedResponse::try_from(res)
+            .map_err(|e| err_report!(LektorCommError::QueryError => [ e ]))?;
+        builder(formated).map_err(|e| err_report!(LektorCommError::ToFormatedResponse => [ e ]))
+    }
+
+    /// The inner helper function to send queries to the lektord server.
+    async fn send_query_inner(
+        &mut self,
+        query: LektorQuery,
+        previous_ret: &mut Vec<String>,
+    ) -> StackedResult<(), LektorCommError> {
+        write_string(&mut self.stream, query.to_string()).await?;
+        loop {
+            match read_replies(&mut self.stream).await {
+                Err(e) => return Err(e),
+                Ok((res, _)) if res.is_empty() => return Ok(()),
+                Ok((res, None)) => {
+                    previous_ret.extend(res);
+                    return Ok(());
+                }
+                Ok((res, Some(cont))) => {
+                    previous_ret.extend(res);
+                    write_string(
+                        &mut self.stream,
+                        LektorQuery::create_continuation(query.clone(), cont).to_string(),
+                    )
+                    .await?;
+                }
+            }
+        }
+    }
+}
+
+impl LektorIdleConnexion {
+    pub async fn idle(
+        connexion: LektorConnexion,
+        idle_list: impl IntoIterator<Item = LektorIdleNotification>,
+    ) -> StackedResult<Self, LektorCommError> {
+        let idle_list: Vec<_> = idle_list
+            .into_iter()
+            .map(|notification| format!("{notification}"))
+            .collect();
+        let idle_list_buffer = format!("idle {}\n", idle_list.join(" "));
+        let LektorConnexion {
+            version,
+            mut stream,
+        } = connexion;
+
+        write_string(&mut stream, idle_list_buffer).await?;
+
+        let (mut reply, _) = read_replies(&mut stream).await?;
+        either!(reply.is_empty()
+            => Ok(Self { version, stream, idle_list })
+            ; {
+                reply.iter_mut().for_each(|msg| msg.insert_str(0, " - "));
+                reply.insert(0, "failed to idle the connexion, got the following messages from the server:".to_string());
+                err_report!(err LektorCommError::MpdAck => reply)
+            }
+        )
+    }
+
+    pub async fn get_notifications(&mut self) -> Vec<LektorIdleNotification> {
+        match read_maybe_replies(&mut self.stream).await {
+            Ok(None) => vec![],
+            Ok(Some((notifications, _))) => notifications
+                .iter()
+                .filter_map(|msg| msg.strip_prefix("changed: "))
+                .flat_map(|notifications| notifications.split(&[',', ' '][..]))
+                .filter_map(
+                    |notification| match LektorIdleNotification::try_from(notification) {
+                        Ok(notification) => Some(notification),
+                        Err(()) => {
+                            error!("the notification `{notification}` is invalid...");
+                            None
+                        }
+                    },
+                )
+                .collect(),
+            Err(e) => {
+                let e = err_attach!(err_ctx!(e => LektorCommError::MpdAck) => [ "failed to read idle replies" ]);
+                error!("{e}");
+                vec![]
+            }
+        }
+    }
+
+    pub fn version(&self) -> &str {
+        &self.version
+    }
+
+    pub fn idle_list(&self) -> &[String] {
+        &self.idle_list
+    }
+}
diff --git a/src/rust/amadeus-next/amalib/src/constants.rs b/src/rust/amadeus-next/amalib/src/constants.rs
new file mode 100644
index 0000000000000000000000000000000000000000..505f0af8e8e8bc5773341ca8fc6496988cb76508
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/src/constants.rs
@@ -0,0 +1,22 @@
+//! Contains standard constants from lektord, must be updated manually if they
+//! change in lektord...
+
+#![allow(dead_code)]
+
+/// MPD commands are at most 32 character long.
+pub(crate) const LKT_COMMAND_LEN_MAX: usize = 32;
+
+/// At most 32 words in a command are supported.
+pub(crate) const LKT_MESSAGE_ARGS_MAX: usize = 32;
+
+/// A message is at most <defined> chars long
+pub(crate) const LKT_MESSAGE_MAX: usize = 2048;
+
+/// At most 64 commands per client.
+pub(crate) const COMMAND_LIST_MAX: usize = 64;
+
+/// At most 16 messages per client.
+pub(crate) const BUFFER_OUT_MAX: usize = 16;
+
+/// Expected version from the lektord daemin.
+pub(crate) const MPD_VERSION: &str = "0.21.16";
diff --git a/src/rust/amadeus-next/amalib/src/lib.rs b/src/rust/amadeus-next/amalib/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..aef81465077bb16212f1578f62a55c0bf1770516
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/src/lib.rs
@@ -0,0 +1,98 @@
+//! The base library used for lektor's clients. It is composed of elements to
+//! communicate with the lektord server and elements to store and organise the
+//! queried informations.
+
+mod connexion;
+mod constants;
+mod query;
+mod response;
+mod uri;
+
+pub use connexion::*;
+pub use query::*;
+pub use response::*;
+pub use uri::*;
+
+pub(crate) use commons::*;
+pub(crate) use log::*;
+pub(crate) use std::str::FromStr;
+
+/// The playback state of the lektord server.
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Copy)]
+pub enum LektorState {
+    Stopped,
+    Play(usize),
+    Pause(usize),
+}
+
+impl Default for LektorState {
+    fn default() -> Self {
+        Self::Stopped
+    }
+}
+
+/// The possible notifications that an idle connexion can listen to.
+#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq, Copy)]
+pub enum LektorIdleNotification {
+    Database,
+    Update,
+    StoredPlaylist,
+    Playlist,
+    Player,
+    Mixer,
+    Output,
+    Options,
+    Partition,
+    Sticker,
+    Subscription,
+    Message,
+}
+
+impl std::fmt::Display for LektorIdleNotification {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.write_str(<LektorIdleNotification as AsRef<str>>::as_ref(self))
+    }
+}
+
+impl TryFrom<&str> for LektorIdleNotification {
+    type Error = ();
+
+    fn try_from(value: &str) -> Result<Self, Self::Error> {
+        use LektorIdleNotification::*;
+        Ok(match value {
+            "database" => Database,
+            "update" => Update,
+            "stored_playlist" => StoredPlaylist,
+            "playlist" => Playlist,
+            "player" => Player,
+            "mixer" => Mixer,
+            "output" => Output,
+            "options" => Options,
+            "partition" => Partition,
+            "sticker" => Sticker,
+            "subscription" => Subscription,
+            "message" => Message,
+            _ => return Err(()),
+        })
+    }
+}
+
+impl AsRef<str> for LektorIdleNotification {
+    fn as_ref(&self) -> &str {
+        use LektorIdleNotification::*;
+        match self {
+            Database => "database",
+            Update => "update",
+            StoredPlaylist => "stored_playlist",
+            Playlist => "playlist",
+            Player => "player",
+            Mixer => "mixer",
+            Output => "output",
+            Options => "options",
+            Partition => "partition",
+            Sticker => "sticker",
+            Subscription => "subscription",
+            Message => "message",
+        }
+    }
+}
diff --git a/src/rust/amadeus-next/amalib/src/query.rs b/src/rust/amadeus-next/amalib/src/query.rs
new file mode 100644
index 0000000000000000000000000000000000000000..bae0d763e9fa370f3db1c5e4827469352a511efd
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/src/query.rs
@@ -0,0 +1,185 @@
+//! Contains files to create and build queries to send latter to lektord.
+
+use crate::*;
+use std::string::ToString;
+
+pub(crate) enum LektorQueryLineType {
+    Ok,
+    ListOk,
+    Ack,
+    Continuation(usize),
+    Data,
+}
+
+#[derive(Debug, Clone)]
+pub enum LektorQuery {
+    Ping,
+    Close,
+    KillServer,
+    ConnectAsUser(String, Box<LektorQuery>),
+
+    CurrentKara,
+    PlaybackStatus,
+
+    PlayNext,
+    PlayPrevious,
+    ShuffleQueue,
+
+    ListAllPlaylists,
+    ListPlaylist(String),
+    SearchKara(LektorUri),
+
+    FindAddKara(LektorUri),
+    InsertKara(LektorUri),
+    AddKara(LektorUri),
+
+    Continuation(usize, Box<LektorQuery>),
+}
+
+impl std::str::FromStr for LektorQueryLineType {
+    type Err = ();
+
+    fn from_str(line: &str) -> Result<Self, Self::Err> {
+        if Self::is_line_ok(line) {
+            Ok(Self::Ok)
+        } else if Self::is_line_ack(line) {
+            Ok(Self::Ack)
+        } else if Self::is_line_list_ok(line) {
+            Ok(Self::ListOk)
+        } else if let Some(cont) = Self::is_line_continuation(line) {
+            Ok(Self::Continuation(cont))
+        } else {
+            Ok(Self::Data)
+        }
+    }
+}
+
+impl LektorQueryLineType {
+    fn is_line_continuation(line: &str) -> Option<usize> {
+        if !line.starts_with("continue:") {
+            return None;
+        }
+        match line.trim_start_matches("continue:").trim().parse::<usize>() {
+            Ok(cont) => Some(cont),
+            Err(_) => None,
+        }
+    }
+
+    fn is_line_list_ok(line: &str) -> bool {
+        (line == "list_OK\n") || (line == "list_OK")
+    }
+
+    fn is_line_ok(line: &str) -> bool {
+        (line == "OK\n") || (line == "OK")
+    }
+
+    fn is_line_ack(line: &str) -> bool {
+        line.starts_with("ACK: ")
+    }
+}
+
+/// The type of the function to use to produce a typed response from the
+/// formated response.
+type QueryToTypeResponseBuilder = fn(LektorFormatedResponse) -> Result<LektorResponse, String>;
+
+impl LektorQuery {
+    /// Get the function to use to produce the typed response from the formated
+    /// response, to automatically create the correct thing.
+    pub fn get_response_type(&self) -> QueryToTypeResponseBuilder {
+        use LektorQuery::*;
+        match self {
+            Ping | Close | KillServer | PlayNext | PlayPrevious | ShuffleQueue | InsertKara(_)
+            | AddKara(_) => LektorEmptyResponse::from_formated,
+
+            ListAllPlaylists => LektorPlaylistSetResponse::from_formated,
+            PlaybackStatus => LektorPlaybackStatusResponse::from_formated,
+            CurrentKara => LektorCurrentKaraResponse::from_formated,
+
+            ConnectAsUser(_, cmd) | Continuation(_, cmd) => cmd.get_response_type(),
+
+            ListPlaylist(_) | SearchKara(_) | FindAddKara(_) => {
+                LektorKaraSetResponse::from_formated
+            }
+        }
+    }
+
+    /// Create a continued query out of another one. If the query is already a
+    /// continuation query then the underlying query is reused.
+    pub fn create_continuation(query: Self, cont: usize) -> Self {
+        match query {
+            Self::Continuation(_, query) => Self::Continuation(cont, query),
+            _ => Self::Continuation(cont, Box::new(query)),
+        }
+    }
+
+    /// Verify that a query is Ok.
+    pub fn verify(&self) -> Result<(), String> {
+        use LektorQuery::*;
+        match self {
+            // User commands
+            SearchKara(_) | FindAddKara(_) | InsertKara(_) | AddKara(_) | PlaybackStatus
+            | PlayNext | PlayPrevious | ShuffleQueue | ListAllPlaylists | ListPlaylist(_)
+            | CurrentKara | Ping => Ok(()),
+
+            // Should be admin commands
+            Close => Err("close is an admin command".to_string()),
+            KillServer => Err("kill server is an admin command".to_string()),
+
+            // Admin commands
+            ConnectAsUser(_, cmd) => match cmd.as_ref() {
+                Close | KillServer => Ok(()),
+                _ => Err(format!("not an admin command: {cmd:?}")),
+            },
+
+            // Continuation commands
+            Continuation(_, cmd) => match cmd.as_ref() {
+                ListAllPlaylists | FindAddKara(_) | SearchKara(_) | ListPlaylist(_) => Ok(()),
+                _ => Err(format!("not a continuable command: {cmd:?}")),
+            },
+        }
+    }
+}
+
+impl std::fmt::Display for LektorQuery {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        macro_rules! lkt_str {
+            ($lit:literal) => {
+                f.write_str(concat!($lit, '\n'))
+            };
+        }
+
+        use LektorQuery::*;
+        match self {
+            Ping => lkt_str!("ping"),
+            Close => lkt_str!("close"),
+            KillServer => lkt_str!("kill"),
+            ConnectAsUser(password, cmd) => write!(
+                f,
+                concat!(
+                    "command_list_ok_begin\n",
+                    "password {}\n",
+                    "{}\n",
+                    "command_list_end\n",
+                ),
+                password, cmd
+            ),
+
+            CurrentKara => lkt_str!("currentsong"),
+            PlaybackStatus => lkt_str!("status"),
+
+            PlayNext => lkt_str!("next"),
+            PlayPrevious => lkt_str!("previous"),
+            ShuffleQueue => lkt_str!("shuffle"),
+
+            ListAllPlaylists => lkt_str!("listplaylists"),
+            ListPlaylist(plt_name) => writeln!(f, "listplaylist {plt_name}"),
+            SearchKara(uri) => writeln!(f, "find {uri}"),
+
+            FindAddKara(uri) => writeln!(f, "findadd {uri}"),
+            InsertKara(uri) => writeln!(f, "__insert {uri}"),
+            AddKara(uri) => writeln!(f, "add {uri}"),
+
+            Continuation(cont, query) => write!(f, "{cont} {query}"),
+        }
+    }
+}
diff --git a/src/rust/amadeus-next/amalib/src/response.rs b/src/rust/amadeus-next/amalib/src/response.rs
new file mode 100644
index 0000000000000000000000000000000000000000..75e53f74543e5d171b2cca43f42b0a6de6101d81
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/src/response.rs
@@ -0,0 +1,338 @@
+//! Contains types for typed response.
+
+use crate::*;
+
+/// A formated response is just a list of key/pairs. We get every line that is
+/// not Ok/Ack/Continue (i.e. data lines) and split on the first ':' and trim
+/// spaces from the keys and the values. The keys are always in lowercase.
+#[derive(Debug)]
+pub struct LektorFormatedResponse {
+    content: Vec<(String, String)>,
+    raw_content: Vec<String>,
+}
+
+impl LektorFormatedResponse {
+    /// Pop the first key found in the response, get an error if the key is not
+    /// found. If multiple keys are found, only the first found is returned.
+    pub fn pop(&mut self, key: &str) -> Result<String, String> {
+        match self
+            .content
+            .iter()
+            .enumerate()
+            .find_map(|(index, (what, _))| (what == key).then_some(index))
+        {
+            Some(index) => Ok(self.content.remove(index).1),
+            None => Err(format!("no key {key} was found in formated response")),
+        }
+    }
+
+    /// Pop all the entries with the said key. If no key is found then the empty
+    /// vector is returned. This function can't fail.
+    pub fn pop_all(&mut self, key: &str) -> Vec<String> {
+        let mut ret: Vec<String> = Vec::new();
+        self.content.retain(|(what, field)| {
+            if *what == key {
+                ret.push(field.clone());
+                false
+            } else {
+                true
+            }
+        });
+        ret
+    }
+
+    /// Get the raw content of the response.
+    pub fn pop_raw(self) -> Vec<String> {
+        self.raw_content
+    }
+}
+
+impl IntoIterator for LektorFormatedResponse {
+    type Item = (String, String);
+    type IntoIter =
+        <std::vec::Vec<(std::string::String, std::string::String)> as IntoIterator>::IntoIter;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.content.into_iter()
+    }
+}
+
+impl TryFrom<Vec<String>> for LektorFormatedResponse {
+    type Error = String;
+
+    fn try_from(vec: Vec<String>) -> Result<Self, Self::Error> {
+        let (mut content, mut raw_content) = (Vec::new(), Vec::new());
+        for line in vec {
+            match line.splitn(2, ':').collect::<Vec<&str>>()[..] {
+                [key, value] => content.push((key.trim().to_lowercase(), value.trim().to_string())),
+                _ => raw_content.push(line),
+            }
+        }
+        Ok(Self {
+            content,
+            raw_content,
+        })
+    }
+}
+
+#[derive(Debug)]
+pub enum LektorResponse {
+    PlaybackStatus(LektorPlaybackStatusResponse),
+    CurrentKara(LektorCurrentKaraResponse),
+    PlaylistSet(LektorPlaylistSetResponse),
+    EmptyResponse(LektorEmptyResponse),
+    KaraSet(LektorKaraSetResponse),
+}
+
+/// A trait for typed lektor responses. Such responses must be built by
+/// consuming a formated response. We also protect from implemeting this trait
+/// outside of this crate.
+pub trait FromLektorResponse: std::fmt::Debug + private::Sealed {
+    /// Consume a formated response to produce the correctly typed response. May
+    /// got an error as a string that describes the problem.
+    fn from_formated(response: LektorFormatedResponse) -> Result<LektorResponse, String>
+    where
+        Self: Sized;
+}
+
+mod private {
+    use super::*;
+    pub trait Sealed {}
+    impl Sealed for LektorPlaybackStatusResponse {}
+    impl Sealed for LektorCurrentKaraResponse {}
+    impl Sealed for LektorPlaylistSetResponse {}
+    impl Sealed for LektorKaraSetResponse {}
+    impl Sealed for LektorEmptyResponse {}
+}
+
+macro_rules! getter {
+    ($name: ident: ref $type: ty) => {
+        pub fn $name(&self) -> &$type {
+            &self.$name
+        }
+    };
+
+    ($name: ident: $type: ty) => {
+        pub fn $name(&self) -> $type {
+            self.$name
+        }
+    };
+}
+
+#[derive(Debug)]
+pub struct LektorPlaybackStatusResponse {
+    elapsed: usize,
+    songid: Option<usize>,
+    song: Option<usize>,
+    volume: usize,
+    state: LektorState,
+    duration: usize,
+    updating_db: usize,
+    playlistlength: usize,
+    random: bool,
+    consume: bool,
+    single: bool,
+    repeat: bool,
+}
+
+#[derive(Debug)]
+pub struct LektorPlaylistSetResponse {
+    playlists: Vec<String>,
+}
+
+#[derive(Debug)]
+pub struct LektorCurrentKaraInnerResponse {
+    title: String,
+    author: String,
+    source: String,
+    song_type: String,
+    song_number: Option<usize>,
+    category: String,
+    language: String,
+}
+
+#[derive(Debug)]
+pub struct LektorKaraSetResponse {
+    karas: Vec<String>,
+}
+
+#[derive(Debug)]
+pub struct LektorCurrentKaraResponse {
+    content: Option<LektorCurrentKaraInnerResponse>,
+}
+
+#[derive(Debug)]
+pub struct LektorEmptyResponse;
+
+impl LektorCurrentKaraResponse {
+    getter!(content: ref Option<LektorCurrentKaraInnerResponse>);
+
+    pub fn maybe_into_inner(self) -> Option<LektorCurrentKaraInnerResponse> {
+        self.content
+    }
+
+    pub fn into_inner(self) -> LektorCurrentKaraInnerResponse {
+        self.content.unwrap()
+    }
+}
+
+impl LektorCurrentKaraInnerResponse {
+    getter!(title: ref String);
+    getter!(author: ref String);
+    getter!(source: ref String);
+    getter!(song_type: ref String);
+    getter!(song_number: Option<usize>);
+    getter!(category: ref String);
+    getter!(language: ref String);
+}
+
+impl LektorPlaybackStatusResponse {
+    getter!(elapsed: usize);
+    getter!(songid: Option<usize>);
+    getter!(song: Option<usize>);
+    getter!(volume: usize);
+    getter!(state: LektorState);
+    getter!(duration: usize);
+    getter!(updating_db: usize);
+    getter!(playlistlength: usize);
+    getter!(random: bool);
+    getter!(consume: bool);
+    getter!(single: bool);
+    getter!(repeat: bool);
+
+    pub fn verify(&self) -> bool {
+        self.elapsed() <= self.duration()
+    }
+}
+
+impl LektorKaraSetResponse {
+    pub fn iter(&self) -> &[String] {
+        &self.karas[..]
+    }
+}
+
+impl LektorPlaylistSetResponse {
+    pub fn iter(&self) -> &[String] {
+        &self.playlists[..]
+    }
+}
+
+impl IntoIterator for LektorKaraSetResponse {
+    type Item = String;
+    type IntoIter = <std::vec::Vec<std::string::String> as IntoIterator>::IntoIter;
+    fn into_iter(self) -> Self::IntoIter {
+        self.karas.into_iter()
+    }
+}
+
+impl IntoIterator for LektorPlaylistSetResponse {
+    type Item = String;
+    type IntoIter = <std::vec::Vec<std::string::String> as IntoIterator>::IntoIter;
+    fn into_iter(self) -> Self::IntoIter {
+        self.playlists.into_iter()
+    }
+}
+
+impl LektorCurrentKaraInnerResponse {
+    /// If the response is partial we might want to return none (if we are not
+    /// playing anything for example...)
+    pub(self) fn is_partial(&self) -> bool {
+        self.title.is_empty()
+            || self.author.is_empty()
+            || self.source.is_empty()
+            || self.song_type.is_empty()
+            || self.category.is_empty()
+            || self.language.is_empty()
+    }
+}
+
+impl FromLektorResponse for LektorEmptyResponse {
+    fn from_formated(_: LektorFormatedResponse) -> Result<LektorResponse, String> {
+        Ok(LektorResponse::EmptyResponse(Self {}))
+    }
+}
+
+impl FromLektorResponse for LektorPlaylistSetResponse {
+    fn from_formated(mut response: LektorFormatedResponse) -> Result<LektorResponse, String> {
+        Ok(LektorResponse::PlaylistSet(Self {
+            playlists: response.pop_all("name"),
+        }))
+    }
+}
+
+impl FromLektorResponse for LektorKaraSetResponse {
+    fn from_formated(response: LektorFormatedResponse) -> Result<LektorResponse, String>
+    where
+        Self: Sized,
+    {
+        Ok(LektorResponse::KaraSet(Self {
+            karas: response.pop_raw(),
+        }))
+    }
+}
+
+impl FromLektorResponse for LektorPlaybackStatusResponse {
+    fn from_formated(mut response: LektorFormatedResponse) -> Result<LektorResponse, String> {
+        let mut ret = Self {
+            elapsed: response.pop("elapsed")?.parse::<usize>().unwrap_or(0),
+            songid: match response.pop("songid")?.parse::<isize>() {
+                Ok(x) if x <= 0 => None,
+                Ok(x) => Some(x as usize),
+                Err(_) => None,
+            },
+            song: match response.pop("song")?.parse::<usize>() {
+                Ok(x) => Some(x),
+                Err(_) => None,
+            },
+            volume: response
+                .pop("volume")?
+                .parse::<usize>()
+                .unwrap()
+                .clamp(0, 100),
+            duration: response.pop("duration")?.parse::<usize>().unwrap(),
+            updating_db: response.pop("updating_db")?.parse::<usize>().unwrap(),
+            playlistlength: response.pop("playlistlength")?.parse::<usize>().unwrap(),
+            random: response.pop("random")?.parse::<usize>().unwrap() != 0,
+            consume: response.pop("consume")?.parse::<usize>().unwrap() != 0,
+            single: response.pop("single")?.parse::<usize>().unwrap() != 0,
+            repeat: response.pop("repeat")?.parse::<usize>().unwrap() != 0,
+            state: LektorState::Stopped,
+        };
+        ret.state = match &response.pop("state")?[..] {
+            "play" => LektorState::Play(ret.songid.unwrap()),
+            "pause" => LektorState::Pause(ret.songid.unwrap()),
+            _ => LektorState::Stopped,
+        };
+        Ok(LektorResponse::PlaybackStatus(ret))
+    }
+}
+
+impl FromLektorResponse for LektorCurrentKaraResponse {
+    fn from_formated(mut response: LektorFormatedResponse) -> Result<LektorResponse, String> {
+        let song_type_number = response.pop("type")?;
+        let (song_type, song_number) = match song_type_number.find(char::is_numeric) {
+            Some(index) => (
+                song_type_number[..index].to_owned(),
+                match song_type_number[index..].parse::<usize>() {
+                    Ok(x) => Some(x),
+                    Err(_) => None,
+                },
+            ),
+            None => panic!("Oupsy"),
+        };
+
+        let inner = LektorCurrentKaraInnerResponse {
+            title: response.pop("title")?,
+            author: response.pop("author")?,
+            source: response.pop("source")?,
+            category: response.pop("category")?,
+            language: response.pop("language")?,
+            song_type,
+            song_number,
+        };
+
+        Ok(LektorResponse::CurrentKara(Self {
+            content: (!inner.is_partial()).then_some(inner),
+        }))
+    }
+}
diff --git a/src/rust/amadeus-next/amalib/src/uri.rs b/src/rust/amadeus-next/amalib/src/uri.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4d4bf040ce37ddad476d4520c227ff42c5d16859
--- /dev/null
+++ b/src/rust/amadeus-next/amalib/src/uri.rs
@@ -0,0 +1,36 @@
+//! Build lektord queries.
+
+use crate::*;
+
+#[derive(Debug, Clone)]
+pub enum LektorUri {
+    Id(i32),
+    Author(String),
+    Playlist(String),
+    Query(Vec<String>),
+}
+
+impl std::fmt::Display for LektorUri {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        let mut ret_str = match self {
+            Self::Id(id) => format!("id://{id}"),
+            Self::Author(author) => format!("author://{author}"),
+            Self::Playlist(plt_name) => format!("playlist://{plt_name}"),
+            Self::Query(sql_query) => {
+                const MAX_ARGS: usize = crate::constants::LKT_MESSAGE_ARGS_MAX;
+                if sql_query.len() > MAX_ARGS {
+                    warn!("the query will be truncated to {MAX_ARGS} arguments")
+                }
+                format!("query://%{}%", sql_query[..MAX_ARGS].join(" "))
+            }
+        };
+
+        const MAX_CHARS: usize = crate::constants::LKT_MESSAGE_MAX - 1;
+        if ret_str.len() > MAX_CHARS {
+            warn!("the uri will be truncated to {MAX_CHARS} characters, note that it can be further truncated");
+            ret_str = ret_str[..=MAX_CHARS].to_string();
+        }
+
+        f.write_str(&ret_str)
+    }
+}
diff --git a/src/rust/amadeus-next/commons/Cargo.toml b/src/rust/amadeus-next/commons/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..a76970b4340f88edf0b23d3b1a0abf509af3bb64
--- /dev/null
+++ b/src/rust/amadeus-next/commons/Cargo.toml
@@ -0,0 +1,12 @@
+[package]
+name = "commons"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+log.workspace = true
+
+thiserror = { version = "^1", default-features = false }
+error-stack = { version = "^0.2", default-features = false, features = ["std"] }
diff --git a/src/rust/amadeus-next/commons/src/error.rs b/src/rust/amadeus-next/commons/src/error.rs
new file mode 100644
index 0000000000000000000000000000000000000000..4802dfedbf10a7c69a703ec1e04623b004e6937a
--- /dev/null
+++ b/src/rust/amadeus-next/commons/src/error.rs
@@ -0,0 +1,119 @@
+pub use error_stack::{
+    report, Context as StackedContext, Report, Result as StackedResult,
+    ResultExt as StackedResultExt,
+};
+pub use thiserror;
+pub use thiserror::{Error as ThisError, __private::DisplayAsDisplay};
+
+/// Create an error report. Be sure to have the correct elements from
+/// `error_stack` are imported into the current scope. If you use the
+/// 'attachement' variant, they will be printed in the order you specified (i.e.
+/// they will be pushed in the reverse order)
+///
+/// For example:
+/// ```no_run
+/// err_report!(ConnexionError::IoError => [ "Some precisions" ])
+/// ```
+/// ```no_run
+/// return err_report!(PassError::NotImplemented);
+/// ```
+#[macro_export]
+macro_rules! err_report {
+    (err $expr: expr) => {
+        Err($crate::report!($expr))
+    };
+
+    (err $expr: expr => $attachements: expr) => {
+        $attachements
+            .into_iter()
+            .fold(Err(report!($expr)), |err, attachement| {
+                err.attach_printable(attachement)
+            })
+    };
+
+    ($expr: expr) => {
+        $crate::report!($expr)
+    };
+
+    ($expr: expr => $attachements: expr) => {
+        $attachements
+            .into_iter()
+            .fold(report!($expr), |err, attachement| {
+                err.attach_printable(attachement)
+            })
+    };
+}
+
+/// Attach more things to the error
+#[macro_export]
+macro_rules! err_attach {
+    ($error: expr => $attachements: expr) => {
+        $attachements
+            .into_iter()
+            .fold($error, |err, attachement| err.attach_printable(attachement))
+    };
+}
+
+/// Create a new error report, initialized with no error in it.
+#[macro_export]
+macro_rules! err_init {
+    ($ty: ident) => {
+        Option::<Report<$ty>>::None
+    };
+}
+
+/// Return the stacked errors if some are found.
+#[macro_export]
+macro_rules! err_return {
+    ($error: expr) => {
+        if let Some(error) = $error {
+            return Err(error);
+        }
+    };
+}
+
+/// Append an error to the stack.
+#[macro_export]
+macro_rules! err_append {
+    ($error: expr => $err: expr) => {{
+        err_append!($error, $err)
+    }};
+
+    ($error: expr, $err: expr) => {{
+        if let Some(error) = $error.as_mut() {
+            error.extend_one($err);
+        } else {
+            $error = Some($err);
+        }
+    }};
+}
+
+#[macro_export]
+#[rustfmt::skip]
+macro_rules! err_ctx {
+    ($error: expr => $ctx: expr) => { $error.change_context($ctx) };
+    ($error: expr,   $ctx: expr) => { $error.change_context($ctx) };
+
+    (err $error: expr => $ctx: expr) => { Err($error.change_context($ctx)) };
+    (err $error: expr,   $ctx: expr) => { Err($error.change_context($ctx)) };
+}
+
+/// Declare a new error variant type to be used with reports and the
+/// [`crate::err_report`] macro. You must declare a closure to implement the fmt function
+/// from [`std::fmt::Display`]. You may use this macro like:
+/// ```no_run
+/// declare_err_type!(pub(crate) CliArgumentParsingError {
+///     #[error("unknown command")]
+///     UnknownCommand(#[passed] String),
+///     UnknownFileType,
+///     UnknownOption,
+/// });
+/// ```
+#[macro_export]
+macro_rules! declare_err_type {
+    ($vis: vis $enum: ident $variants: tt) => {
+        #[derive(std::fmt::Debug, $crate::ThisError)]
+        #[allow(clippy::enum_variant_names, missing_docs)]
+        $vis enum $enum $variants
+    };
+}
diff --git a/src/rust/amadeus-next/commons/src/lib.rs b/src/rust/amadeus-next/commons/src/lib.rs
new file mode 100644
index 0000000000000000000000000000000000000000..3c4eeed704578339984eb952b5a03b4a678359b1
--- /dev/null
+++ b/src/rust/amadeus-next/commons/src/lib.rs
@@ -0,0 +1,4 @@
+mod error;
+mod macros;
+
+pub use error::*;
diff --git a/src/rust/amadeus-next/commons/src/macros.rs b/src/rust/amadeus-next/commons/src/macros.rs
new file mode 100644
index 0000000000000000000000000000000000000000..e8b692585346eeb3ef456b02817a4582e9735403
--- /dev/null
+++ b/src/rust/amadeus-next/commons/src/macros.rs
@@ -0,0 +1,10 @@
+#[macro_export]
+macro_rules! either {
+    ($test:expr => $true_expr:expr; $false_expr:expr) => {
+        if $test {
+            $true_expr
+        } else {
+            $false_expr
+        }
+    };
+}
diff --git a/src/rust/amadeus-next/lkt-rs/Cargo.toml b/src/rust/amadeus-next/lkt-rs/Cargo.toml
new file mode 100644
index 0000000000000000000000000000000000000000..e2902802689ce6f8e27cf6a45d4291f3fe88cc9d
--- /dev/null
+++ b/src/rust/amadeus-next/lkt-rs/Cargo.toml
@@ -0,0 +1,10 @@
+[package]
+name = "lkt-rs"
+version.workspace = true
+edition.workspace = true
+authors.workspace = true
+license.workspace = true
+
+[dependencies]
+log.workspace = true
+serde.workspace = true
diff --git a/src/rust/amadeus-next/lkt-rs/src/main.rs b/src/rust/amadeus-next/lkt-rs/src/main.rs
new file mode 100644
index 0000000000000000000000000000000000000000..f328e4d9d04c31d0d70d16d21a07d1613be9d577
--- /dev/null
+++ b/src/rust/amadeus-next/lkt-rs/src/main.rs
@@ -0,0 +1 @@
+fn main() {}