From 58165a43497b4f120d483bd8a1aa12c1142c043a Mon Sep 17 00:00:00 2001
From: Kubat <maelle.martin@proton.me>
Date: Sat, 12 Oct 2024 12:30:07 +0200
Subject: [PATCH] LEKTOR*: Continue implementations

Also, turn the binaries into lib+bin. We do this to try to begin their
compilation before the codegen finished for dependencies, specially for
zbus which takes a really long time.

We have a big refactor for the NKDB crate. We remove the queue from the
database. The queue should not be in the database. We place it in the
lektord crate. We also will place the search functionnalities out of the
nkdb or the lektord crate, and place it into a new one.

Also, update dependencies and run clippy.

Other developments:
- Update to amadeus
- Make the KId just a u64. This is just a simpler and better
  representation! The thing is Copy, we can use atomics and not bizarre
  mechanisms to factorize some Arc<str>, etc. We still can know when a
  kara was updated or the file changed, and we no longer need to
  factorize playlists. Note that the relation `RemoteKId <-> KId` is now
  a one-to-one relation.
- We also eepercutate the fact that KId is now copy in Amadeus. More
  things don't need clone anymore and the iterators now will return the
  KId and not a ref to it.
- Rationalize routes for lektord
- Update the returned data from kurisu API
- Change how to get, read or write the playlists and the queue.
- Add a separated epoch to playlists and the queue that is incremented
  each time we try to write it (controled by the methods.)
- Change the payload crates to reflect that
- Remove async_trait from where we can
---
 Cargo.lock                                    | 247 ++++-----
 Cargo.toml                                    |  40 +-
 README.md                                     |   2 +-
 amadeus/Cargo.toml                            |   3 +-
 amadeus/i18n/en/amadeus.ftl                   |  10 +-
 amadeus/i18n/es-ES/amadeus.ftl                |  10 +-
 amadeus/i18n/fr-FR/amadeus.ftl                |  57 ++
 amadeus/rsc/icons/fontawesome/crop.svg        |   1 +
 amadeus/rsc/icons/fontawesome/retry.svg       |   1 +
 amadeus/src/app.rs                            | 220 ++++++--
 amadeus/src/app/bottom_bar.rs                 |  16 +-
 amadeus/src/app/context_pages.rs              |  20 +-
 amadeus/src/app/context_pages/about.rs        |   1 +
 amadeus/src/app/context_pages/config.rs       |   1 +
 amadeus/src/app/context_pages/kara_info.rs    |  24 +
 amadeus/src/app/kard.rs                       |  23 +-
 amadeus/src/app/menu.rs                       |   2 +
 amadeus/src/app/pages.rs                      | 285 ++++++++--
 amadeus/src/app/pages/history.rs              |  36 +-
 amadeus/src/app/pages/home.rs                 |   9 +-
 amadeus/src/app/pages/playlist.rs             |  95 +++-
 amadeus/src/app/pages/playlists.rs            |  31 +-
 amadeus/src/app/pages/queue.rs                |  82 ++-
 amadeus/src/app/pages/search.rs               | 124 ++---
 amadeus/src/app/progress_bar.rs               |  11 +-
 amadeus/src/{ => app}/subscriptions.rs        |   0
 .../src/{ => app}/subscriptions/playback.rs   |   4 +-
 .../src/{ => app}/subscriptions/updates.rs    |   4 +-
 amadeus/src/config.rs                         |   4 +
 amadeus/src/i18n.rs                           |   4 +-
 amadeus/src/icons.rs                          |   3 +
 amadeus/src/lib.rs                            |   8 +
 amadeus/src/main.rs                           |  14 +-
 amadeus/src/playlist.rs                       |  27 +
 amadeus/src/store.rs                          |  61 ++-
 amadeus/src/store/history.rs                  |  12 +-
 amadeus/src/store/playlist_content.rs         |  30 +-
 amadeus/src/store/playlists.rs                |  16 +-
 amadeus/src/store/queue.rs                    |  37 --
 amadeus/src/store/queue_level.rs              |  11 +-
 kurisu_api/Cargo.toml                         |   7 +-
 kurisu_api/src/error.rs                       |  20 +
 kurisu_api/src/lib.rs                         |   1 +
 kurisu_api/src/v2.rs                          | 261 ++++++++-
 kurisu_api/tests/dbinfo.json                  | 216 +++++++-
 kurisu_api/tests/favorites.json               |   5 +
 kurisu_api/tests/karas.json                   | 150 ++++++
 kurisu_api/tests/playlists.json               |  37 ++
 kurisu_api/tests/sample.json                  | 323 ------------
 kurisu_api/tests/v2.rs                        |  20 +-
 lektor_lib/Cargo.toml                         |  13 +-
 lektor_lib/src/requests.rs                    | 130 +++--
 lektor_nkdb/Cargo.toml                        |  40 +-
 lektor_nkdb/src/database/epoch.rs             |  20 +-
 lektor_nkdb/src/database/mod.rs               |  16 +-
 lektor_nkdb/src/database/pool.rs              | 302 ++---------
 lektor_nkdb/src/database/update.rs            | 100 ++--
 lektor_nkdb/src/id.rs                         |  97 ++++
 lektor_nkdb/src/lib.rs                        | 495 ++----------------
 lektor_nkdb/src/playlist/infos.rs             |  64 ---
 lektor_nkdb/src/playlist/mod.rs               |  10 -
 lektor_nkdb/src/playlist/name.rs              | 140 -----
 lektor_nkdb/src/playlist/register.rs          | 292 -----------
 lektor_nkdb/src/playlist/values.rs            |  54 --
 lektor_nkdb/src/playlists/mod.rs              | 112 ++++
 lektor_nkdb/src/playlists/playlist.rs         | 221 ++++++++
 lektor_nkdb/src/queue/mod.rs                  | 304 -----------
 lektor_nkdb/src/search/mod.rs                 |   6 +-
 lektor_nkdb/src/storage/disk_storage.rs       | 149 +++---
 lektor_nkdb/src/storage/mod.rs                |   9 +-
 lektor_nkdb/src/storage/test_storage.rs       |  23 +-
 lektor_nkdb/src/strings.rs                    |  34 ++
 lektor_payloads/Cargo.toml                    |  17 +-
 lektor_payloads/src/lib.rs                    |  17 +-
 lektor_payloads/src/play_state.rs             |  23 +
 lektor_payloads/src/playlist_name.rs          |  78 ---
 .../queue => lektor_payloads/src}/priority.rs |   2 +-
 lektor_payloads/src/search.rs                 |   2 +-
 lektor_payloads/src/userid.rs                 |  76 ++-
 lektor_repo/src/downloader.rs                 |  28 +-
 lektor_repo/src/lib.rs                        |  10 +-
 lektor_utils/src/base64.rs                    |  36 +-
 lektord/Cargo.toml                            |   1 -
 lektord/src/app/mod.rs                        | 100 ++--
 lektord/src/app/mpris.rs                      |  18 +-
 lektord/src/app/play_state.rs                 |  26 +
 lektord/src/app/queue.rs                      | 307 +++++++++++
 lektord/src/app/routes.rs                     | 411 +++++++++------
 lektord/src/c_wrapper/commands.rs             |   6 +-
 lektord/src/c_wrapper/mod.rs                  |  66 ++-
 lektord/src/c_wrapper/playstate.rs            |   2 +-
 lektord/src/lib.rs                            | 106 ++++
 lektord/src/main.rs                           | 113 +---
 lkt/Cargo.toml                                |   1 -
 lkt/src/args/mod.rs                           |   3 +-
 lkt/src/lib.rs                                | 346 ++++++++++++
 lkt/src/main.rs                               | 341 +-----------
 97 files changed, 3831 insertions(+), 3562 deletions(-)
 create mode 100644 amadeus/i18n/fr-FR/amadeus.ftl
 create mode 100644 amadeus/rsc/icons/fontawesome/crop.svg
 create mode 100644 amadeus/rsc/icons/fontawesome/retry.svg
 create mode 100644 amadeus/src/app/context_pages/kara_info.rs
 rename amadeus/src/{ => app}/subscriptions.rs (100%)
 rename amadeus/src/{ => app}/subscriptions/playback.rs (87%)
 rename amadeus/src/{ => app}/subscriptions/updates.rs (90%)
 create mode 100644 amadeus/src/lib.rs
 create mode 100644 amadeus/src/playlist.rs
 delete mode 100644 amadeus/src/store/queue.rs
 create mode 100644 kurisu_api/src/error.rs
 create mode 100644 kurisu_api/tests/favorites.json
 create mode 100644 kurisu_api/tests/karas.json
 create mode 100644 kurisu_api/tests/playlists.json
 delete mode 100644 kurisu_api/tests/sample.json
 create mode 100644 lektor_nkdb/src/id.rs
 delete mode 100644 lektor_nkdb/src/playlist/infos.rs
 delete mode 100644 lektor_nkdb/src/playlist/mod.rs
 delete mode 100644 lektor_nkdb/src/playlist/name.rs
 delete mode 100644 lektor_nkdb/src/playlist/register.rs
 delete mode 100644 lektor_nkdb/src/playlist/values.rs
 create mode 100644 lektor_nkdb/src/playlists/mod.rs
 create mode 100644 lektor_nkdb/src/playlists/playlist.rs
 delete mode 100644 lektor_nkdb/src/queue/mod.rs
 create mode 100644 lektor_nkdb/src/strings.rs
 create mode 100644 lektor_payloads/src/play_state.rs
 delete mode 100644 lektor_payloads/src/playlist_name.rs
 rename {lektor_nkdb/src/queue => lektor_payloads/src}/priority.rs (98%)
 create mode 100644 lektord/src/app/play_state.rs
 create mode 100644 lektord/src/app/queue.rs
 create mode 100644 lektord/src/lib.rs
 create mode 100644 lkt/src/lib.rs

diff --git a/Cargo.lock b/Cargo.lock
index 998566d4..2055200c 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -4,9 +4,9 @@ version = 3
 
 [[package]]
 name = "ab_glyph"
-version = "0.2.28"
+version = "0.2.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79faae4620f45232f599d9bc7b290f88247a0834162c4495ab2f02d60004adfb"
+checksum = "ec3672c180e71eeaaac3a541fbbc5f5ad4def8b747c595ad30d674e43049f7b0"
 dependencies = [
  "ab_glyph_rasterizer",
  "owned_ttf_parser",
@@ -89,9 +89,9 @@ dependencies = [
 
 [[package]]
 name = "addr2line"
-version = "0.24.1"
+version = "0.24.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f5fb1d8e4442bd405fdfd1dacb42792696b0cf9cb15882e5d097b742a676d375"
+checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
 dependencies = [
  "gimli",
 ]
@@ -150,11 +150,10 @@ checksum = "3aa2999eb46af81abb65c2d30d446778d7e613b60bbf4e174a027e80f90a3c14"
 
 [[package]]
 name = "amadeus"
-version = "0.0.1"
+version = "3.0.1"
 dependencies = [
  "anyhow",
  "async-channel",
- "async-trait",
  "chrono",
  "derive_more",
  "futures",
@@ -299,9 +298,9 @@ dependencies = [
 
 [[package]]
 name = "ashpd"
-version = "0.9.1"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bfe7e0dd0ac5a401dc116ed9f9119cf9decc625600474cb41f0fc0a0050abc9a"
+checksum = "4d43c03d9e36dd40cab48435be0b09646da362c278223ca535493877b2c1dee9"
 dependencies = [
  "enumflags2",
  "futures-channel",
@@ -822,9 +821,9 @@ dependencies = [
 
 [[package]]
 name = "bytemuck_derive"
-version = "1.7.1"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0cc8b54b395f2fcfbb3d90c47b01c7f444d94d05bdeb775811dec868ac3bbc26"
+checksum = "bcfcc3cd946cb52f0bbfdbbcfa2f4e24f75ebb6c0e1002f7c25904fada18b9ec"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -897,9 +896,9 @@ dependencies = [
 
 [[package]]
 name = "cc"
-version = "1.1.24"
+version = "1.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "812acba72f0a070b003d3697490d2b55b837230ae7c6c6497f05cc2ddbb8d938"
+checksum = "b16803a61b81d9eabb7eae2588776c4c1e584b738ede45fdbb4c972cec1e9945"
 dependencies = [
  "jobserver",
  "libc",
@@ -946,9 +945,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.18"
+version = "4.5.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0956a43b323ac1afaffc053ed5c4b7c1f1800bacd1683c353aabbb752515dd3"
+checksum = "b97f376d85a664d5837dbae44bf546e6477a679ff6610010f17276f686d867e8"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -956,9 +955,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.18"
+version = "4.5.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4d72166dd41634086d5803a47eb71ae740e61d84709c36f3c34110173db3961b"
+checksum = "19bc80abd44e4bed93ca373a0704ccbd1b710dc5749406201bb018272808dc54"
 dependencies = [
  "anstyle",
  "clap_lex",
@@ -967,9 +966,9 @@ dependencies = [
 
 [[package]]
 name = "clap_complete"
-version = "4.5.29"
+version = "4.5.33"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8937760c3f4c60871870b8c3ee5f9b30771f792a7045c48bcbba999d7d6b3b8e"
+checksum = "9646e2e245bf62f45d39a0f3f36f1171ad1ea0d6967fd114bca72cb02a8fcdfb"
 dependencies = [
  "clap",
 ]
@@ -1198,7 +1197,7 @@ dependencies = [
 [[package]]
 name = "cosmic-config"
 version = "0.1.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "atomicwrites",
  "cosmic-config-derive",
@@ -1220,7 +1219,7 @@ dependencies = [
 [[package]]
 name = "cosmic-config-derive"
 version = "0.1.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "quote",
  "syn 1.0.109",
@@ -1229,7 +1228,7 @@ dependencies = [
 [[package]]
 name = "cosmic-settings-daemon"
 version = "0.1.0"
-source = "git+https://github.com/pop-os/dbus-settings-bindings#01ee80cd975ad3f41a47738ed21d778a7cd07552"
+source = "git+https://github.com/pop-os/dbus-settings-bindings#931f5db558bf3fcb572ff4e18f7f1618a7430046"
 dependencies = [
  "zbus 4.4.0",
 ]
@@ -1260,7 +1259,7 @@ dependencies = [
 [[package]]
 name = "cosmic-theme"
 version = "0.1.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "almost",
  "cosmic-config",
@@ -1865,15 +1864,15 @@ checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
 name = "foldhash"
-version = "0.1.2"
+version = "0.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33d5ac82bdbbd872ce0dfea4e848a678cfcfdc0d210a5b20549b8c659fec0392"
+checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2"
 
 [[package]]
 name = "font-types"
-version = "0.6.0"
+version = "0.7.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f0189ccb084f77c5523e08288d418cbaa09c451a08515678a0aa265df9a8b60"
+checksum = "dda6e36206148f69fc6ecb1bb6c0dedd7ee469f3db1d0dc2045beea28430ca43"
 dependencies = [
  "bytemuck",
 ]
@@ -1971,9 +1970,9 @@ dependencies = [
 
 [[package]]
 name = "futures"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0"
+checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -1986,9 +1985,9 @@ dependencies = [
 
 [[package]]
 name = "futures-channel"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78"
+checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
 dependencies = [
  "futures-core",
  "futures-sink",
@@ -1996,15 +1995,15 @@ dependencies = [
 
 [[package]]
 name = "futures-core"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d"
+checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 
 [[package]]
 name = "futures-executor"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d"
+checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
 dependencies = [
  "futures-core",
  "futures-task",
@@ -2014,9 +2013,9 @@ dependencies = [
 
 [[package]]
 name = "futures-io"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
+checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
 
 [[package]]
 name = "futures-lite"
@@ -2048,9 +2047,9 @@ dependencies = [
 
 [[package]]
 name = "futures-macro"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
+checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2059,21 +2058,21 @@ dependencies = [
 
 [[package]]
 name = "futures-sink"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5"
+checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
 
 [[package]]
 name = "futures-task"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004"
+checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
 
 [[package]]
 name = "futures-util"
-version = "0.3.30"
+version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
+checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 dependencies = [
  "futures-channel",
  "futures-core",
@@ -2140,9 +2139,9 @@ dependencies = [
 
 [[package]]
 name = "gimli"
-version = "0.31.0"
+version = "0.31.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "32085ea23f3234fc7846555e85283ba4de91e21016dc0455a16286d87a292d64"
+checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 
 [[package]]
 name = "gl_generator"
@@ -2563,7 +2562,7 @@ dependencies = [
 [[package]]
 name = "iced"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "dnd",
  "iced_accessibility",
@@ -2581,7 +2580,7 @@ dependencies = [
 [[package]]
 name = "iced_accessibility"
 version = "0.1.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "accesskit",
  "accesskit_winit",
@@ -2590,7 +2589,7 @@ dependencies = [
 [[package]]
 name = "iced_core"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "bitflags 2.6.0",
  "dnd",
@@ -2610,7 +2609,7 @@ dependencies = [
 [[package]]
 name = "iced_futures"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "futures",
  "iced_core",
@@ -2623,7 +2622,7 @@ dependencies = [
 [[package]]
 name = "iced_graphics"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "bitflags 2.6.0",
  "bytemuck",
@@ -2647,7 +2646,7 @@ dependencies = [
 [[package]]
 name = "iced_renderer"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "iced_graphics",
  "iced_tiny_skia",
@@ -2659,7 +2658,7 @@ dependencies = [
 [[package]]
 name = "iced_runtime"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "dnd",
  "iced_core",
@@ -2671,7 +2670,7 @@ dependencies = [
 [[package]]
 name = "iced_style"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "iced_core",
  "once_cell",
@@ -2681,7 +2680,7 @@ dependencies = [
 [[package]]
 name = "iced_tiny_skia"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "bytemuck",
  "cosmic-text",
@@ -2698,7 +2697,7 @@ dependencies = [
 [[package]]
 name = "iced_wgpu"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "as-raw-xcb-connection",
  "bitflags 2.6.0",
@@ -2727,7 +2726,7 @@ dependencies = [
 [[package]]
 name = "iced_widget"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "dnd",
  "iced_renderer",
@@ -2743,7 +2742,7 @@ dependencies = [
 [[package]]
 name = "iced_winit"
 version = "0.12.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "dnd",
  "iced_graphics",
@@ -2821,12 +2820,12 @@ checksum = "029d73f573d8e8d63e6d5020011d3255b28c3ba85d6cf870a07184ed23de9284"
 
 [[package]]
 name = "indexmap"
-version = "2.5.0"
+version = "2.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68b900aa2f7301e21c36462b170ee99994de34dff39a4a6a528e80e7376d07e5"
+checksum = "707907fe3c25f5424cce2cb7e1cbcafee6bdbe735ca90ef77c29e84591e5b9da"
 dependencies = [
  "equivalent",
- "hashbrown 0.14.5",
+ "hashbrown 0.15.0",
 ]
 
 [[package]]
@@ -2890,9 +2889,9 @@ dependencies = [
 
 [[package]]
 name = "ipnet"
-version = "2.10.0"
+version = "2.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "187674a687eed5fe42285b40c6291f9a01517d415fad1c3cbc6a9f778af7fcd4"
+checksum = "ddc24109865250148c2e0f3d25d4f0f479571723792d3802153c60922a4fb708"
 
 [[package]]
 name = "itoa"
@@ -2942,9 +2941,9 @@ dependencies = [
 
 [[package]]
 name = "js-sys"
-version = "0.3.70"
+version = "0.3.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1868808506b929d7b0cfa8f75951347aa71bb21144b7791bae35d9bccfcfe37a"
+checksum = "6a88f1bda2bd75b0452a14784937d796722fdebfe50df998aeb3f0b7603019a9"
 dependencies = [
  "wasm-bindgen",
 ]
@@ -3017,8 +3016,10 @@ dependencies = [
 name = "kurisu_api"
 version = "8.0.1"
 dependencies = [
+ "derive_more",
  "hashbrown 0.15.0",
  "lektor_utils",
+ "log",
  "serde",
  "serde_json",
  "sha256",
@@ -3041,7 +3042,6 @@ name = "lektor_lib"
 version = "8.0.1"
 dependencies = [
  "anyhow",
- "async-trait",
  "futures",
  "lektor_payloads",
  "lektor_utils",
@@ -3071,8 +3071,8 @@ name = "lektor_nkdb"
 version = "8.0.1"
 dependencies = [
  "anyhow",
- "async-trait",
  "chrono",
+ "derive_more",
  "futures",
  "hashbrown 0.15.0",
  "kurisu_api",
@@ -3098,6 +3098,7 @@ dependencies = [
  "axum",
  "futures",
  "lektor_nkdb",
+ "lektor_procmacros",
  "lektor_utils",
  "serde",
  "serde_json",
@@ -3151,7 +3152,6 @@ name = "lektord"
 version = "8.0.1"
 dependencies = [
  "anyhow",
- "async-trait",
  "axum",
  "clap",
  "futures",
@@ -3181,10 +3181,10 @@ checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5"
 [[package]]
 name = "libcosmic"
 version = "0.1.0"
-source = "git+https://github.com/pop-os/libcosmic.git#228eb4d70d581be88bacb1e261106a58603d847b"
+source = "git+https://github.com/pop-os/libcosmic.git#8da25f94e9c363c8c6b93284adf4cf6e07bc3a45"
 dependencies = [
  "apply",
- "ashpd 0.9.1",
+ "ashpd 0.9.2",
  "chrono",
  "cosmic-config",
  "cosmic-settings-daemon",
@@ -3215,6 +3215,7 @@ dependencies = [
  "tracing",
  "unicode-segmentation",
  "url",
+ "ustr",
  "zbus 4.4.0",
 ]
 
@@ -3289,7 +3290,6 @@ name = "lkt"
 version = "8.0.1"
 dependencies = [
  "anyhow",
- "async-trait",
  "chrono",
  "clap",
  "clap_complete",
@@ -3336,11 +3336,11 @@ checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
 
 [[package]]
 name = "lru"
-version = "0.12.4"
+version = "0.12.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "37ee39891760e7d94734f6f63fedc29a2e4a152f836120753a72503f09fcf904"
+checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
 dependencies = [
- "hashbrown 0.14.5",
+ "hashbrown 0.15.0",
 ]
 
 [[package]]
@@ -3808,21 +3808,18 @@ dependencies = [
 
 [[package]]
 name = "object"
-version = "0.36.4"
+version = "0.36.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "084f1a5821ac4c651660a94a7153d27ac9d8a53736203f58b31945ded098070a"
+checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e"
 dependencies = [
  "memchr",
 ]
 
 [[package]]
 name = "once_cell"
-version = "1.20.1"
+version = "1.20.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82881c4be219ab5faaf2ad5e5e5ecdff8c66bd7402ca3160975c93b24961afd1"
-dependencies = [
- "portable-atomic",
-]
+checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
 
 [[package]]
 name = "option-ext"
@@ -3885,11 +3882,11 @@ dependencies = [
 
 [[package]]
 name = "owned_ttf_parser"
-version = "0.24.0"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "490d3a563d3122bf7c911a59b0add9389e5ec0f5f0c3ac6b91ff235a0e6a7f90"
+checksum = "22ec719bbf3b2a81c109a4e20b1f129b5566b7dce654bc3872f6a05abf82b2c4"
 dependencies = [
- "ttf-parser 0.24.1",
+ "ttf-parser 0.25.0",
 ]
 
 [[package]]
@@ -4110,12 +4107,6 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "22686f4785f02a4fcc856d3b3bb19bf6c8160d103f7a99cc258bddd0251dc7f2"
 
-[[package]]
-name = "portable-atomic"
-version = "1.9.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cc9c68a3f6da06753e9335d63e27f6b9754dd1920d941135b7ea8224f141adb2"
-
 [[package]]
 name = "ppv-lite86"
 version = "0.2.20"
@@ -4176,9 +4167,9 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.86"
+version = "1.0.87"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
+checksum = "b3e4daa0dcf6feba26f985457cdf104d4b4256fc5a09547140f3631bb076b19a"
 dependencies = [
  "unicode-ident",
 ]
@@ -4340,9 +4331,9 @@ checksum = "3b42e27ef78c35d3998403c1d26f3efd9e135d3e5121b0a4845cc5cc27547f4f"
 
 [[package]]
 name = "read-fonts"
-version = "0.20.0"
+version = "0.22.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c141b9980e1150201b2a3a32879001c8f975fe313ec3df5471a9b5c79a880cd"
+checksum = "fb94d9ac780fdcf9b6b252253f7d8f221379b84bd3573131139b383df69f85e1"
 dependencies = [
  "bytemuck",
  "font-types",
@@ -4657,9 +4648,9 @@ dependencies = [
 
 [[package]]
 name = "rustls"
-version = "0.23.13"
+version = "0.23.14"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2dabaac7466917e566adb06783a81ca48944c6898a1b08b9374106dd671f4c8"
+checksum = "415d9944693cb90382053259f89fbb077ea730ad7273047ec63b19bc9b160ba8"
 dependencies = [
  "once_cell",
  "ring",
@@ -4937,9 +4928,9 @@ checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d"
 
 [[package]]
 name = "skrifa"
-version = "0.20.0"
+version = "0.22.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "abea4738067b1e628c6ce28b2c216c19e9ea95715cdb332680e821c3bec2ef23"
+checksum = "8e1c44ad1f6c5bdd4eefed8326711b7dbda9ea45dfd36068c427d332aa382cbe"
 dependencies = [
  "bytemuck",
  "read-fonts",
@@ -5155,9 +5146,9 @@ dependencies = [
 
 [[package]]
 name = "swash"
-version = "0.1.18"
+version = "0.1.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93cdc334a50fcc2aa3f04761af3b28196280a6aaadb1ef11215c478ae32615ac"
+checksum = "cbd59f3f359ddd2c95af4758c18270eddd9c730dde98598023cdabff472c2ca2"
 dependencies = [
  "skrifa",
  "yazi",
@@ -5245,12 +5236,12 @@ dependencies = [
 
 [[package]]
 name = "terminal_size"
-version = "0.3.0"
+version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "21bebf2b7c9e0a515f6e0f8c51dc0f8e4696391e6f1ff30379559f8365fb0df7"
+checksum = "4f599bd7ca042cfdf8f4512b277c02ba102247820f9d9d4a9f521f496751a6ef"
 dependencies = [
  "rustix 0.38.37",
- "windows-sys 0.48.0",
+ "windows-sys 0.59.0",
 ]
 
 [[package]]
@@ -5366,7 +5357,6 @@ dependencies = [
  "bytes",
  "libc",
  "mio 1.0.2",
- "parking_lot 0.12.3",
  "pin-project-lite",
  "signal-hook-registry",
  "socket2 0.5.7",
@@ -5553,9 +5543,9 @@ checksum = "2c591d83f69777866b9126b24c6dd9a18351f177e49d625920d19f989fd31cf8"
 
 [[package]]
 name = "ttf-parser"
-version = "0.24.1"
+version = "0.25.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5be21190ff5d38e8b4a2d3b6a3ae57f612cc39c96e83cedeaf7abc338a8bac4a"
+checksum = "5902c5d130972a0000f60860bfbf46f7ca3db5391eddfedd1b8728bd9dc96c0e"
 
 [[package]]
 name = "type-map"
@@ -5604,9 +5594,9 @@ dependencies = [
 
 [[package]]
 name = "unicode-bidi"
-version = "0.3.15"
+version = "0.3.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
+checksum = "5ab17db44d7388991a428b2ee655ce0c212e862eff1768a455c58f9aad6e7893"
 
 [[package]]
 name = "unicode-bidi-mirroring"
@@ -5713,6 +5703,19 @@ version = "2.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
 
+[[package]]
+name = "ustr"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e904a2279a4a36d2356425bb20be271029cc650c335bc82af8bfae30085a3d0"
+dependencies = [
+ "ahash",
+ "byteorder",
+ "lazy_static",
+ "parking_lot 0.12.3",
+ "serde",
+]
+
 [[package]]
 name = "usvg"
 version = "0.37.0"
@@ -5813,9 +5816,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
 
 [[package]]
 name = "wasm-bindgen"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a82edfc16a6c469f5f44dc7b571814045d60404b55a0ee849f9bcfa2e63dd9b5"
+checksum = "128d1e363af62632b8eb57219c8fd7877144af57558fb2ef0368d0087bddeb2e"
 dependencies = [
  "cfg-if",
  "once_cell",
@@ -5824,9 +5827,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-backend"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9de396da306523044d3302746f1208fa71d7532227f15e347e2d93e4145dd77b"
+checksum = "cb6dd4d3ca0ddffd1dd1c9c04f94b868c37ff5fac97c30b97cff2d74fce3a358"
 dependencies = [
  "bumpalo",
  "log",
@@ -5839,9 +5842,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-futures"
-version = "0.4.43"
+version = "0.4.45"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61e9300f63a621e96ed275155c108eb6f843b6a26d053f122ab69724559dc8ed"
+checksum = "cc7ec4f8827a71586374db3e87abdb5a2bb3a15afed140221307c3ec06b1f63b"
 dependencies = [
  "cfg-if",
  "js-sys",
@@ -5851,9 +5854,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "585c4c91a46b072c92e908d99cb1dcdf95c5218eeb6f3bf1efa991ee7a68cccf"
+checksum = "e79384be7f8f5a9dd5d7167216f022090cf1f9ec128e6e6a482a2cb5c5422c56"
 dependencies = [
  "quote",
  "wasm-bindgen-macro-support",
@@ -5861,9 +5864,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-macro-support"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afc340c74d9005395cf9dd098506f7f44e38f2b4a21c6aaacf9a105ea5e1e836"
+checksum = "26c6ab57572f7a24a4985830b120de1594465e5d500f24afe89e16b4e833ef68"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -5874,9 +5877,9 @@ dependencies = [
 
 [[package]]
 name = "wasm-bindgen-shared"
-version = "0.2.93"
+version = "0.2.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c62a0a307cb4a311d3a07867860911ca130c3494e8c2719593806c08bc5d0484"
+checksum = "65fc09f10666a9f147042251e0dda9c18f166ff7de300607007e96bdebc1068d"
 
 [[package]]
 name = "wasm-timer"
@@ -6029,9 +6032,9 @@ dependencies = [
 
 [[package]]
 name = "web-sys"
-version = "0.3.70"
+version = "0.3.72"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26fdeaafd9bd129f65e7c031593c24d62186301e0c72c8978fa1678be7d532c0"
+checksum = "f6488b90108c040df0fe62fa815cbdee25124641df01814dd7282749234c6112"
 dependencies = [
  "js-sys",
  "wasm-bindgen",
diff --git a/Cargo.toml b/Cargo.toml
index 693ef9df..638ca35a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,7 +11,7 @@ members = [
 [workspace.package]
 edition = "2021"
 authors = [
-    "Maël MARTIN <mael.martin@protonmail.com>",
+    "Maëlle MARTIN <maelle.martin@proton.me>",
     "Louis GOYARD <elliu@hashi.re>",
     "Loïc ALLEGRE <lallegre26@gmail.com>",
     "Kevin COCCHI <salixor@pm.me>",
@@ -44,7 +44,7 @@ zbus   = { version = "*", default-features = false, features = ["tokio"] }
 chrono = { version = "*", default-features = false, features = ["clock"] }
 sha256 = { version = "*", default-features = false, features = ["async"] }
 anyhow = { version = "*", default-features = false, features = ["std"] }
-regex  = { version = "*", default-features = false, features = ["std", "perf"] }
+regex  = { version = "*", default-features = false, features = ["std"] }
 log    = "*"
 rand   = "*"
 base64 = "*"
@@ -63,6 +63,7 @@ lektor_utils      = { path = "lektor_utils" }
 lektor_mpris      = { path = "lektor_mpris" }
 lektor_payloads   = { path = "lektor_payloads" }
 lektor_procmacros = { path = "lektor_procmacros" }
+lektor_nkdb       = { path = "lektor_nkdb" }
 
 # Data Structures
 hashbrown     = { version = "*", features = ["serde"] }
@@ -71,24 +72,20 @@ async-channel = { version = "*", default-features = false }
 # Serialization & Deserialization
 toml = "*"
 serde_json = { version = "*", default-features = false, features = [
-    "std",
-    "preserve_order",
+    "std", "preserve_order"
 ] }
 serde = { version = "*", default-features = false, features = [
-    "rc",
-    "std",
-    "derive",
+    "rc", "std", "derive"
 ] }
 
 # Async stuff
 async-trait  = "*"
 futures-util = "*"
-futures = { version = "*", default-features = false, features = [
-    "std",
-    "async-await",
+futures      = { version = "*", default-features = false, features = ["std", "async-await"] }
+tokio-stream = { version = "*", default-features = false, features = ["net"]}
+tokio        = { version = "*", default-features = false, features = [
+    "rt-multi-thread", "net", "time", "sync", "fs", "signal"
 ] }
-tokio        = { version = "*", features = [ "full" ] }
-tokio-stream = { version = "*", features = [ "net" ], default-features = false }
 
 # Web stuff
 reqwest = { version = "*", default-features = false, features = [
@@ -101,22 +98,15 @@ axum = { version = "*", default-features = false, features = [
     "macros",
     "tokio",
 ] }
-tower = { version = "*", features = ["util"] }
-hyper-util = { version = "*", features = ["tokio", "server-auto", "http1"] }
-hyper = { version = "*", default-features = false, features = [
-    "http1",
-    "server",
-] }
+tower      = { version = "*", default-features = false, features = ["util"] }
+hyper-util = { version = "*", default-features = false, features = ["http1", "tokio", "server-auto"] }
+hyper      = { version = "*", default-features = false, features = ["http1", "server"] }
 
 # Arguments
-roff = "*"
+roff          = "*"
 clap_complete = { version = "*", default-features = false }
-clap = { version = "*", default-features = false, features = [
-    "usage",
-    "help",
-    "std",
-    "wrap_help",
-    "derive",
+clap          = { version = "*", default-features = false, features = [
+    "usage", "help", "std", "wrap_help", "derive"
 ] }
 
 # Proc macro things
diff --git a/README.md b/README.md
index d7020834..f8ce1125 100644
--- a/README.md
+++ b/README.md
@@ -4,7 +4,7 @@
 [![matrix chat](https://img.shields.io/badge/matrix-%23baka--dev--lektor%3Aiiens.net-brightgreen)](https://matrix.to/#/#baka-dev-lektor:iiens.net)
 [![platform support](https://img.shields.io/badge/platform-Linux|FreeBSD|Windows-lightgrey)](https://git.iiens.net/martin2018/lektor/-/commits/master)
 [![lektord version](https://img.shields.io/badge/lektord-v8.0.1-blueviolet)](https://git.iiens.net/martin2018/lektor/-/tags)
-[![amadeus version](https://img.shields.io/badge/amadeus-v0.0.1-blueviolet)](https://git.iiens.net/martin2018/lektor/-/tags)
+[![amadeus version](https://img.shields.io/badge/amadeus-v3.0.1-blueviolet)](https://git.iiens.net/martin2018/lektor/-/tags)
 <a href="https://github.com/iced-rs/iced">
   <img src="https://gist.githubusercontent.com/hecrj/ad7ecd38f6e47ff3688a38c79fd108f0/raw/74384875ecbad02ae2a926425e9bcafd0695bade/color.svg" width="100px">
 </a>
diff --git a/amadeus/Cargo.toml b/amadeus/Cargo.toml
index dc72b513..df2c1612 100644
--- a/amadeus/Cargo.toml
+++ b/amadeus/Cargo.toml
@@ -1,7 +1,7 @@
 [package]
 name        = "amadeus"
 description = "Amadeus-RS, graphical interface for lektord"
-version     = "0.0.1"
+version     = "3.0.1"
 
 rust-version.workspace = true
 edition.workspace      = true
@@ -31,7 +31,6 @@ futures-util.workspace = true
 tokio.workspace        = true
 reqwest.workspace      = true
 futures.workspace      = true
-async-trait.workspace  = true
 
 i18n-embed-fl.workspace = true
 rust-embed.workspace    = true
diff --git a/amadeus/i18n/en/amadeus.ftl b/amadeus/i18n/en/amadeus.ftl
index 570f09ac..64b16d88 100644
--- a/amadeus/i18n/en/amadeus.ftl
+++ b/amadeus/i18n/en/amadeus.ftl
@@ -12,6 +12,10 @@ settings  = Settings
 playback  = Playback
 page-id   = Page { $num }
 
+empty-queue     = Empty Queue
+empty-history   = Empty History
+empty-playlists = Empty playlists
+
 next-kara        = Next kara
 prev-kara        = Previous kara
 toggle-playback  = Play/Pause
@@ -27,7 +31,7 @@ home      = Home
 queue     = Queue
 search    = Search
 playlists = Playlists
-playlist  = Playlist { $name }
+playlist  = Playlist “{ $name }”
 
 log-level        = Log level
 icon-theme       = Icon theme
@@ -47,3 +51,7 @@ epoch            = { $what } epoch
 user             = User
 config           = { $what } configuration
 url-of           = { $what } URL
+
+kara        = Kara
+click-to-dl = Click to download
+
diff --git a/amadeus/i18n/es-ES/amadeus.ftl b/amadeus/i18n/es-ES/amadeus.ftl
index 4f30527f..53619d63 100644
--- a/amadeus/i18n/es-ES/amadeus.ftl
+++ b/amadeus/i18n/es-ES/amadeus.ftl
@@ -12,6 +12,10 @@ settings  = Ajustes
 playback  = Reproducción
 page-id   = Pagina { $num }
 
+empty-queue     = No hay nada en la cola de reproducción
+empty-history   = No has escuchado nada recientemente
+empty-playlists = No tienes listas de reproducción
+
 next-kara        = Póxima kara
 prev-kara        = Previo kara
 toggle-playback  = Alternar reprod.
@@ -27,7 +31,7 @@ home      = Inicio
 queue     = Cola de reproducción
 search    = Buscar
 playlists = Listas de reproducción
-playlist  = Lista { $name }
+playlist  = Lista “{ $name }”
 
 log-level        = Nivel de registro
 icon-theme       = Tema de icono
@@ -47,3 +51,7 @@ epoch            = Época de { $what }
 user             = Usuario·a
 config           = Ajustes de { $what }
 url-of           = Dirección de { $what }
+
+kara        = Kara
+click-to-dl = Haga clic para descargar
+
diff --git a/amadeus/i18n/fr-FR/amadeus.ftl b/amadeus/i18n/fr-FR/amadeus.ftl
new file mode 100644
index 00000000..8d581be6
--- /dev/null
+++ b/amadeus/i18n/fr-FR/amadeus.ftl
@@ -0,0 +1,57 @@
+app-title = Amadeus
+
+unimplemented = Il faut implémenter { $what }
+error-found   = Une erreur est survenue
+
+about     = À propos de
+edit      = Éditer
+view      = Vue
+history   = Historique
+welcome   = Bonjour!
+settings  = Paramettres
+playback  = Letcure
+page-id   = Page { $num }
+
+empty-queue     = La queue est vide
+empty-history   = L'historique est vide
+empty-playlists = Il n'y a pas de listes de lecture de disponibles
+
+next-kara        = Kara suivant
+prev-kara        = Kara précédent
+toggle-playback  = Play/Pause
+play-playback    = Play
+pause-playback   = Pause
+stop-playback    = Stop
+playback-shuffle = Mélanger
+playback-crop    = Recadrer
+playback-clear   = Effacer
+menu-queue       = Queue
+
+home      = Accueil
+queue     = Queue
+search    = Chercher
+playlists = Listes de lecture
+playlist  = Liste « { $name } »
+
+log-level        = Log level
+icon-theme       = Theme d'icones
+dark-theme       = Theme sombre
+address-ip       = Address
+scheme           = Scheme
+kurisu-token     = Token pour Kurisu
+open-url         = Ouvrir l'URL { $url }
+token            = Token
+port             = Port
+status           = Status
+connected        = Connecté
+disconnected     = Déconnecté
+version          = Version
+info-about       = Information sur { $what }
+epoch            = Époch { $what }
+user             = Utilisateur
+config           = Configuration de { $what }
+url-of           = URL pour { $what }
+
+kara        = Kara
+click-to-dl = Clicker pour télécharger
+
diff --git a/amadeus/rsc/icons/fontawesome/crop.svg b/amadeus/rsc/icons/fontawesome/crop.svg
new file mode 100644
index 00000000..eae76724
--- /dev/null
+++ b/amadeus/rsc/icons/fontawesome/crop.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M128 32c0-17.7-14.3-32-32-32S64 14.3 64 32l0 32L32 64C14.3 64 0 78.3 0 96s14.3 32 32 32l32 0 0 256c0 35.3 28.7 64 64 64l224 0 0-64-224 0 0-352zM384 480c0 17.7 14.3 32 32 32s32-14.3 32-32l0-32 32 0c17.7 0 32-14.3 32-32s-14.3-32-32-32l-32 0 0-256c0-35.3-28.7-64-64-64L160 64l0 64 224 0 0 352z"/></svg>
diff --git a/amadeus/rsc/icons/fontawesome/retry.svg b/amadeus/rsc/icons/fontawesome/retry.svg
new file mode 100644
index 00000000..50c27ae9
--- /dev/null
+++ b/amadeus/rsc/icons/fontawesome/retry.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><!--!Font Awesome Free 6.6.0 by @fontawesome - https://fontawesome.com License - https://fontawesome.com/license/free Copyright 2024 Fonticons, Inc.--><path d="M463.5 224l8.5 0c13.3 0 24-10.7 24-24l0-128c0-9.7-5.8-18.5-14.8-22.2s-19.3-1.7-26.2 5.2L413.4 96.6c-87.6-86.5-228.7-86.2-315.8 1c-87.5 87.5-87.5 229.3 0 316.8s229.3 87.5 316.8 0c12.5-12.5 12.5-32.8 0-45.3s-32.8-12.5-45.3 0c-62.5 62.5-163.8 62.5-226.3 0s-62.5-163.8 0-226.3c62.2-62.2 162.7-62.5 225.3-1L327 183c-6.9 6.9-8.9 17.2-5.2 26.2s12.5 14.8 22.2 14.8l119.5 0z"/></svg>
diff --git a/amadeus/src/app.rs b/amadeus/src/app.rs
index df1846dc..359180a2 100644
--- a/amadeus/src/app.rs
+++ b/amadeus/src/app.rs
@@ -1,9 +1,12 @@
+//! Implements the whole application model.
+
 mod bottom_bar;
 mod context_pages;
 mod kard;
 mod menu;
 mod pages;
 mod progress_bar;
+mod subscriptions;
 
 use crate::{
     app::{
@@ -14,23 +17,23 @@ use crate::{
     config::{Config, LogLevel},
     fl,
     store::Store,
-    subscriptions,
 };
 use cosmic::{
     app::{Command, Core},
-    iced::{
-        alignment::{Horizontal, Vertical},
-        Length, Subscription,
-    },
+    iced::{Length, Subscription},
+    prelude::*,
+    style, theme, widget, Application,
+};
+use futures::{
     prelude::*,
-    theme, widget, Application,
+    stream::{self, FuturesUnordered},
 };
-use lektor_lib::ConnectConfig;
+use lektor_lib::{requests, ConnectConfig};
 use lektor_payloads::{
-    KId, PlayStateWithCurrent, Playlist, PlaylistName, Priority, PRIORITY_LENGTH, PRIORITY_VALUES,
+    KId, Kara, PlayStateWithCurrent, Priority, SearchFrom, PRIORITY_LENGTH, PRIORITY_VALUES,
 };
 use lektor_utils::{config::SocketScheme, open};
-use pages::search::{FilterAtom, FilterAtomId};
+use pages::search::FilterAtomId;
 use std::{
     borrow::Cow,
     collections::HashMap,
@@ -87,13 +90,22 @@ 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>,
     },
 }
@@ -126,8 +138,9 @@ impl LektordState {
 }
 
 /// A command to send to the lektord instance.
-#[derive(Debug, Clone, Copy)]
+#[derive(Debug, Clone)]
 pub enum LektordCommand {
+    // Playback stuff
     PlaybackToggle,
     PlaybackPlay,
     PlaybackPause,
@@ -135,9 +148,33 @@ pub enum LektordCommand {
     PlaybackNext,
     PlaybackPrevious,
 
+    // Queue stuff
     QueueShuffle,
     QueueClear,
     QueueCrop,
+    QueueGet,
+
+    // QueueLevel stuff
+    QueueLevelShuffle(Priority),
+    QueueLevelClear(Priority),
+    QueueLevelGet(Priority),
+
+    // History stuff
+    HistoryClear,
+    HistoryGet,
+
+    // Playlists stuff
+    PlaylistsGet,
+
+    // Playlist stuff
+    PlaylistDelete(KId),
+    PlaylistRemoveKara(KId, KId),
+    PlaylistGetContent(KId),
+    PlaylistShuffleContent(KId),
+
+    // Misc stuff
+    DownloadKaraInfo(KId),
+    DownloadKarasInfo(Vec<KId>),
 }
 
 /// Something changed with the config.
@@ -162,33 +199,40 @@ pub enum LektordMessage {
     Connected(lektor_payloads::Infos),
     PlaybackUpdate(lektor_payloads::PlayStateWithCurrent),
 
-    DownloadKaraInfo(KId),
-    DownloadKarasInfo(Vec<KId>),
+    DownloadedKaraInfo(Kara),
+    DownloadedKarasInfo(Vec<Kara>),
 
     ChangedQueue([Vec<KId>; PRIORITY_LENGTH]),
     ChangedQueueLevel(Priority, Vec<KId>),
     ChangedHistory(Vec<KId>),
-    ChangedAvailablePlaylists(Vec<PlaylistName>),
-    ChangedPlaylistContent(PlaylistName, Playlist),
+    ChangedAvailablePlaylists(Vec<KId>),
+    ChangedPlaylistContent(KId, Vec<KId>),
+    ChangedPlaylistsContent(Vec<(KId, Vec<KId>)>),
 }
 
 /// Messages emitted by the application and its widgets.
 #[derive(Debug, Clone)]
 pub enum Message {
+    // Misc UI things
     OpenUrl(&'static str),
     OpenKaraInfo(KId),
     ToggleContextPage(ContextPage),
 
+    // Update the configuration
     UpdateConfig(ConfigMessage),
 
+    // Lektord stuff
     SendCommand(LektordCommand),
-    Lektord(LektordMessage),
+    LektordUpdate(LektordMessage),
 
+    // Search stuff
     ChangeFilterStageBuffer(String),
     AddFilterAtomFromStageBuffer,
     RemoveFilterAtom(FilterAtomId),
+    QueryWithFiltersResults(Vec<KId>),
     ClearFilters,
     QueryWithFilters,
+    SearchOnlyAuthor(String),
 }
 
 /// Create a COSMIC application from the app model
@@ -255,15 +299,18 @@ impl Application for AppModel {
     fn header_center(&self) -> Vec<Element<Self::Message>> {
         let is_connected = !matches!(self.lektord_state, LektordState::Disconnected);
         macro_rules! icon {
-            ($icon:ident => $action:ident) => {{
+            ($icon:ident => $action:ident $(| $true:ident)?) => {{
                 let button = widget::icon::from_svg_bytes(crate::icons::$icon)
                     .symbolic(true)
                     .apply(widget::button::icon);
                 Element::from(match is_connected {
-                    true => button.on_press(Message::SendCommand(LektordCommand::$action)),
-                    false => button,
+                    true  => icon!(@ $($true)?, button).on_press(Message::SendCommand(LektordCommand::$action)),
+                    false => icon!(@ $($true)?, button),
                 })
             }};
+
+            (@ destructive, $expr:expr) => { $expr.style(style::Button::AppletIcon) };
+            (@            , $expr:expr) => { $expr };
         }
 
         let pre = [
@@ -274,7 +321,8 @@ impl Application for AppModel {
         let post = [
             icon!(NEXT    => PlaybackNext),
             icon!(SHUFFLE => QueueShuffle),
-            icon!(METEOR  => QueueClear),
+            icon!(CROP    => QueueCrop  | destructive),
+            icon!(METEOR  => QueueClear | destructive),
             Element::from(widget::horizontal_space(Length::Fill)),
         ];
 
@@ -322,25 +370,21 @@ impl Application for AppModel {
         (self.core.window.show_context).then(|| match self.context_page {
             ContextPage::About => context_pages::about::view(self),
             ContextPage::Settings => context_pages::config::view(self),
+            ContextPage::KaraInfo(kid) => context_pages::kara_info::view(self.store.get(kid)),
         })
     }
 
     /// Describes the interface based on the current state of the application model.
     fn view(&self) -> Element<Self::Message> {
-        let page = match self.nav.active_data::<Page>() {
-            Some(Page::Home) => pages::home::view(),
-            Some(Page::Queue) => pages::queue::view(&self.store),
-            Some(Page::History) => pages::history::view(&self.store),
-            Some(Page::Search) => pages::search::view(self),
-            Some(Page::Playlists) => pages::playlists::view(&self.store),
-            Some(Page::Playlist(name)) => pages::playlist::view(&self.store, name),
-            page => pages::not_found(page),
-        }
-        .apply(widget::container)
-        .width(Length::Fill)
-        .height(Length::Fill)
-        .align_x(Horizontal::Center)
-        .align_y(Vertical::Top);
+        let page = match self.nav.active_data::<Page>().copied() {
+            Some(Page::Home) => pages::home::view().into(),
+            Some(Page::Queue) => pages::queue::view(&self.store).into(),
+            Some(Page::History) => pages::history::view(&self.store).into(),
+            Some(Page::Search) => pages::search::view(self).into(),
+            Some(Page::Playlists) => pages::playlists::view(&self.store).into(),
+            Some(Page::Playlist(id)) => pages::playlist::view(&self.store, id).into(),
+            page => pages::not_found(page).into(),
+        };
 
         widget::column()
             .push(page)
@@ -372,7 +416,7 @@ impl Application for AppModel {
         match message {
             Message::UpdateConfig(message) => self.update_config(message),
 
-            Message::Lektord(message) => self.handle_lektord_message(message),
+            Message::LektordUpdate(message) => self.handle_lektord_message(message),
             Message::OpenUrl(url) => self.open_url(url),
             Message::ToggleContextPage(context_page) => self.toggle_context_page(context_page),
 
@@ -396,13 +440,32 @@ impl Application for AppModel {
             }
             Message::ClearFilters => {
                 self.search_filter.clear();
-                self.search_results.clear();;
+                self.search_results.clear();
                 Command::none()
             }
-            Message::QueryWithFilters => {
-                log::error!("query with filters");
+            Message::QueryWithFiltersResults(results) => {
+                self.search_results = results;
+                Command::none()
+            }
+            Message::SearchOnlyAuthor(author) => {
+                log::error!("implement search of only author {author}, need to clear everything and do the query");
                 Command::none()
             }
+            Message::QueryWithFilters => {
+                let config = self.connect_config.clone();
+                let filters: Vec<_> = self.search_filter.iter_cloned().collect();
+                cosmic::command::future(async move {
+                    requests::search_karas(&*config.read().await, SearchFrom::Database, filters)
+                        .await
+                        .map(|matches| {
+                            cosmic::app::message::app(Message::QueryWithFiltersResults(matches))
+                        })
+                        .unwrap_or_else(|err| {
+                            log::error!("failed to query with filters: {err}");
+                            cosmic::app::message::none()
+                        })
+                })
+            }
         }
     }
 
@@ -429,7 +492,7 @@ impl AppModel {
     fn update_connect_config(&self) -> Command<Message> {
         let config = self.config.get_connect_config();
         let connect_config = self.connect_config.clone();
-        cosmic::command::future(async {
+        cosmic::command::future(async move {
             *connect_config.write_owned().await = config;
             cosmic::app::message::none()
         })
@@ -443,7 +506,7 @@ impl AppModel {
             self.context_page = context_page;
             self.core.window.show_context = true;
         }
-        self.set_context_title(context_page.title());
+        self.set_context_title(self.context_page.title());
         Command::none()
     }
 
@@ -542,9 +605,9 @@ impl AppModel {
     fn handle_lektord_message(&mut self, message: LektordMessage) -> Command<Message> {
         use LektordMessage::*;
         match message {
-            // Asked to download metadata informations.
-            DownloadKaraInfo(kid) => log::error!("download kara info {kid}"),
-            DownloadKarasInfo(kids) => log::error!("download kara info {kids:?}"),
+            // Downloaded metadata informations.
+            DownloadedKaraInfo(kara) => self.store.set(kara),
+            DownloadedKarasInfo(karas) => karas.into_iter().for_each(|kara| self.store.set(kara)),
 
             // Disconnected, if any query failed we set the disconnected status
             Disconnected => {
@@ -576,14 +639,38 @@ impl AppModel {
             }
 
             // Down here, got updates from lektord.
-            ChangedAvailablePlaylists(names) => self.store.keep_playlists(names),
-            ChangedPlaylistContent(name, plt) => self.store.set_playlist(name, plt),
+            ChangedAvailablePlaylists(names) => {
+                let config = self.connect_config.clone();
+                let playlists = self.store.keep_playlists(names);
+                return cosmic::command::future(async move {
+                    let updated_playlists = stream::iter(playlists)
+                        .zip(stream::repeat_with(move || config.clone()))
+                        .then(|(id, config)| async move {
+                            let config = config.read().await;
+                            requests::get_playlist_content(config.as_ref(), id)
+                                .await
+                                .map(|content| (id, content))
+                        })
+                        .collect::<FuturesUnordered<_>>()
+                        .await;
+                    Message::LektordUpdate(ChangedPlaylistsContent(
+                        updated_playlists.into_iter().flatten().collect::<Vec<_>>(),
+                    ))
+                });
+            }
+
             ChangedHistory(kids) => self.store.set_history(kids),
+
             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);
             }),
+
+            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)),
         }
         Command::none()
     }
@@ -592,11 +679,16 @@ impl AppModel {
     fn send_command(&self, cmd: LektordCommand) -> Command<Message> {
         let config = self.connect_config.clone();
         use lektor_payloads::*;
+        macro_rules! msg {
+            ($msg:ident $(($($args:expr),+))?) => {
+                cosmic::app::message::app(Message::LektordUpdate(LektordMessage::$msg $(($($args),+))?))
+            };
+        }
         macro_rules! cmd {
             ($req:ident ($($arg:expr),*) $(, $res:pat => $handle:expr)?) => {
                 cosmic::command::future(async move {
                     cmd!(@handle $req:
-                        lektor_lib::requests::$req(config.read().await.as_ref() $(, $arg)*).await,
+                        requests::$req(config.read().await.as_ref() $(, $arg)*).await,
                         $($res => $handle)?
                     )
                 })
@@ -620,12 +712,40 @@ impl AppModel {
             LektordCommand::PlaybackNext => cmd!(play_next()),
             LektordCommand::PlaybackPrevious => cmd!(play_previous()),
 
-            LektordCommand::QueueShuffle => cmd!(shuffle_queue()),
+            LektordCommand::QueueShuffle => cmd!(shuffle_queue_range(..)),
+            LektordCommand::QueueClear => cmd!(remove_range_from_queue(..)),
+            LektordCommand::QueueCrop => cmd!(remove_range_from_queue(1..)),
+            LektordCommand::QueueGet => cmd!(get_queue_range(..), queue => {
+                msg!(ChangedQueue(queue.into_iter().fold(<[Vec<KId>; PRIORITY_LENGTH]>::default(), |mut ret, (level, kid)| {
+                    ret[level.index()].push(kid);
+                    ret
+                })))
+            }),
+            LektordCommand::QueueLevelShuffle(lvl) => cmd!(shuffle_level_queue(lvl)),
+            LektordCommand::QueueLevelClear(lvl) => cmd!(remove_level_from_queue(lvl)),
+            LektordCommand::QueueLevelGet(lvl) => cmd!(get_queue_level(lvl), queue => {
+                msg!(ChangedQueueLevel(lvl, queue))
+            }),
 
-            cmd => {
-                log::error!("need to implement {cmd:?}");
-                Command::none()
-            }
+            LektordCommand::DownloadKaraInfo(kid) => cmd!(get_kara_by_kid(kid), kara => {
+                msg!(DownloadedKaraInfo(kara))
+            }),
+            LektordCommand::DownloadKarasInfo(kids) => cmd!(get_karas_by_kid(kids), karas => {
+                msg!(DownloadedKarasInfo(karas))
+            }),
+
+            LektordCommand::HistoryClear => cmd!(remove_range_from_history(..)),
+            LektordCommand::HistoryGet => cmd!(get_history_range(..), history => {
+                msg!(ChangedHistory(history))
+            }),
+
+            LektordCommand::PlaylistGetContent(id) => cmd!(get_playlist_content(id), content => {
+                msg!(ChangedPlaylistContent(id, content))
+            }),
+            LektordCommand::PlaylistsGet => todo!(),
+            LektordCommand::PlaylistShuffleContent(_) => todo!(),
+            LektordCommand::PlaylistDelete(_) => todo!(),
+            LektordCommand::PlaylistRemoveKara(..) => todo!(),
         }
     }
 }
diff --git a/amadeus/src/app/bottom_bar.rs b/amadeus/src/app/bottom_bar.rs
index e9f55288..f4d15607 100644
--- a/amadeus/src/app/bottom_bar.rs
+++ b/amadeus/src/app/bottom_bar.rs
@@ -1,5 +1,6 @@
 use crate::{
-    app::{AppModel, LektordMessage, Message},
+    app::{AppModel, LektordCommand, Message},
+    fl,
     store::KaraOrId,
 };
 use cosmic::{
@@ -10,7 +11,9 @@ use cosmic::{
         Alignment, Length,
     },
     iced_core::text::Wrap,
-    style, theme, widget, Apply, Element,
+    prelude::*,
+    style, theme,
+    widget::{self, tooltip::Position},
 };
 use lektor_payloads::{KId, Kara};
 
@@ -79,7 +82,7 @@ fn view_left_part<'a>(kara: &Kara) -> Element<'a, Message> {
         .style(theme::Text::Color(
             theme::active().cosmic().accent_text_color().into(),
         ))
-        .font(font::FONT_LIGHT)
+        .font(font::light())
         .wrap(Wrap::None);
 
     widget::column()
@@ -87,7 +90,7 @@ fn view_left_part<'a>(kara: &Kara) -> Element<'a, Message> {
         .push(source)
         .apply(widget::button::custom)
         .style(style::Button::Transparent)
-        .on_press(Message::OpenKaraInfo(kara.id.clone()))
+        .on_press(Message::OpenKaraInfo(kara.id))
         .apply(widget::container)
         .align_x(Horizontal::Left)
         .align_y(Vertical::Center)
@@ -104,7 +107,8 @@ fn view_kara_id<'a>(kid: KId) -> Element<'a, Message> {
         .wrap(Wrap::None)
         .apply(widget::button::custom)
         .style(style::Button::Transparent)
-        .on_press(Message::Lektord(LektordMessage::DownloadKaraInfo(kid)))
+        .on_press(Message::SendCommand(LektordCommand::DownloadKaraInfo(kid)))
+        .apply(|btn| widget::tooltip(btn, fl!("click-to-dl"), Position::Top))
         .apply(widget::container)
         .align_x(Horizontal::Left)
         .align_y(Vertical::Center)
@@ -118,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.rs b/amadeus/src/app/context_pages.rs
index e22a40fa..f74507bc 100644
--- a/amadeus/src/app/context_pages.rs
+++ b/amadeus/src/app/context_pages.rs
@@ -1,23 +1,33 @@
 use crate::fl;
+use derive_more::Display;
+use lektor_payloads::KId;
 
 pub mod about;
 pub mod config;
+pub mod kara_info;
 
 /// The context page to display in the context drawer. This is the pane on the right that can be
 /// hidden or shown.
-#[derive(Copy, Clone, Debug, Default, Eq, PartialEq)]
+#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Display)]
 pub enum ContextPage {
+    /// Show the about page, with informations on Amadeus and the Connected lektord instance.
     #[default]
+    #[display("{}", fl!("about"))]
     About,
 
+    /// Show the settings page for Amadeus.
+    #[display("{}", fl!("settings"))]
     Settings,
+
+    /// Show more informations about a kara than what is shown in tables (more tags, time, size,
+    /// etc...)
+    #[display("{}", fl!("kara"))]
+    KaraInfo(KId),
 }
 
 impl ContextPage {
+    /// The title of the context page.
     pub fn title(&self) -> String {
-        match self {
-            Self::About => fl!("about"),
-            Self::Settings => fl!("settings"),
-        }
+        self.to_string()
     }
 }
diff --git a/amadeus/src/app/context_pages/about.rs b/amadeus/src/app/context_pages/about.rs
index 7b8e6153..da39f353 100644
--- a/amadeus/src/app/context_pages/about.rs
+++ b/amadeus/src/app/context_pages/about.rs
@@ -7,6 +7,7 @@ use cosmic::{
     theme, widget, Apply as _, Element,
 };
 
+/// Show more informations about Amadeus and the connected Lektord instance (if connected.)
 pub fn view(app: &AppModel) -> Element<Message> {
     macro_rules! url {
         ($icon:ident => $url:literal) => {{
diff --git a/amadeus/src/app/context_pages/config.rs b/amadeus/src/app/context_pages/config.rs
index 6452dbe4..d6ba3624 100644
--- a/amadeus/src/app/context_pages/config.rs
+++ b/amadeus/src/app/context_pages/config.rs
@@ -7,6 +7,7 @@ use cosmic::{iced::Length, theme, widget, Element};
 use lektor_utils::config::{SocketScheme, UserConfig};
 use std::sync::LazyLock;
 
+/// View and edit the settings for Amadeus
 pub fn view(app: &AppModel) -> Element<Message> {
     use {std::borrow::Cow::*, ConfigMessage::*, Message::*};
 
diff --git a/amadeus/src/app/context_pages/kara_info.rs b/amadeus/src/app/context_pages/kara_info.rs
new file mode 100644
index 00000000..39096063
--- /dev/null
+++ b/amadeus/src/app/context_pages/kara_info.rs
@@ -0,0 +1,24 @@
+use crate::{
+    app::{LektordCommand, Message},
+    fl,
+    store::KaraOrId,
+};
+use cosmic::{
+    prelude::*,
+    style,
+    widget::{self, tooltip::Position},
+};
+
+/// View more details about a single kara.
+pub fn view<'a>(kara_or_kid: KaraOrId<'_>) -> Element<'a, Message> {
+    match kara_or_kid {
+        KaraOrId::Id(kid) => widget::text::monotext(kid.to_string())
+            .apply(widget::button::custom)
+            .style(style::Button::Transparent)
+            .on_press(Message::SendCommand(LektordCommand::DownloadKaraInfo(kid)))
+            .apply(|btn| widget::tooltip(btn, fl!("click-to-dl"), Position::Left))
+            .apply(Element::from),
+
+        KaraOrId::Kara(kara) => widget::text::monotext(format!("{kara:#?}")).into(),
+    }
+}
diff --git a/amadeus/src/app/kard.rs b/amadeus/src/app/kard.rs
index a2d3e307..3bdb3c05 100644
--- a/amadeus/src/app/kard.rs
+++ b/amadeus/src/app/kard.rs
@@ -1,12 +1,17 @@
+//! Contains everything to display kara in a card (here more precisely a row.)
+
 use crate::{
-    app::{LektordMessage, Message},
+    app::{LektordCommand, Message},
+    fl,
     store::KaraOrId,
 };
 use cosmic::{
     cosmic_theme::Spacing,
     font,
     iced::{Alignment, Length},
-    style, theme, widget, Apply, Element,
+    prelude::*,
+    style, theme,
+    widget::{self, tooltip::Position},
 };
 use lektor_payloads::Kara;
 
@@ -19,12 +24,12 @@ fn kara_title<'a>(kara: &Kara) -> Element<'a, Message> {
             .map(|num| format!("{}{num} - {}", kara.song_type, kara.song_source))
             .unwrap_or_else(|| format!("{} - {}", kara.song_type, kara.song_source))
             .apply(widget::text::text)
-            .font(font::FONT_LIGHT)
+            .font(font::light())
             .into(),
     ])
     .apply(widget::button::custom)
     .style(style::Button::Transparent)
-    .on_press(Message::OpenKaraInfo(kara.id.clone()))
+    .on_press(Message::OpenKaraInfo(kara.id))
     .apply(Element::from)
 }
 
@@ -78,15 +83,15 @@ pub fn view<'a>(kara_or_id: KaraOrId) -> Element<'a, Message> {
             .style(style::Text::Color(
                 theme::active().cosmic().destructive_text_color().into(),
             ))
+            .font(font::mono())
             .apply(widget::button::custom)
             .style(style::Button::Transparent)
-            .on_press(Message::Lektord(LektordMessage::DownloadKaraInfo(
-                kid.clone(),
-            )))
-            .apply(Element::from)],
+            .on_press(Message::SendCommand(LektordCommand::DownloadKaraInfo(kid)))
+            .apply(|btn| widget::tooltip(btn, fl!("click-to-dl"), Position::Top))
+            .into()],
         KaraOrId::Kara(kara) => vec![
             kara_title(kara),
-            widget::horizontal_space(Length::Fill).apply(Element::from),
+            widget::horizontal_space(Length::Fill).into(),
             kara_tags(kara),
             // TODO: Add the controls here...
         ],
diff --git a/amadeus/src/app/menu.rs b/amadeus/src/app/menu.rs
index 4e78b684..50d8f142 100644
--- a/amadeus/src/app/menu.rs
+++ b/amadeus/src/app/menu.rs
@@ -1,3 +1,5 @@
+//! Contains everything about the application menu, from the top-left-hand corner
+
 use crate::{
     app::{context_pages::ContextPage, LektordCommand, Message},
     fl,
diff --git a/amadeus/src/app/pages.rs b/amadeus/src/app/pages.rs
index 0e1e03f7..0d2fc73a 100644
--- a/amadeus/src/app/pages.rs
+++ b/amadeus/src/app/pages.rs
@@ -1,17 +1,19 @@
+//! Contains everything to help display the main pages of the application.
+
 use crate::{app::Message, fl};
 use cosmic::{
-    font,
+    cosmic_theme::Spacing,
     iced::{
         alignment::{Horizontal, Vertical},
         Alignment, Length,
     },
-    prelude::CollectionWidget as _,
+    prelude::*,
     style, theme,
     widget::{self, icon, nav_bar, Icon},
-    Apply as _, Element,
 };
 use derive_more::Display;
-use std::sync::Arc;
+use lektor_payloads::KId;
+use std::marker;
 
 pub mod history;
 pub mod home;
@@ -21,7 +23,7 @@ pub mod queue;
 pub mod search;
 
 /// The page to display in the application.
-#[derive(Default, Debug, Eq, PartialEq, Clone, Display)]
+#[derive(Default, Debug, Eq, PartialEq, Clone, Copy, Display)]
 #[non_exhaustive]
 pub enum Page {
     #[default]
@@ -40,11 +42,12 @@ pub enum Page {
     #[display("{}", fl!("playlists"))]
     Playlists,
 
-    #[display("{}", fl!("playlist", name = _0.as_ref()))]
-    Playlist(Arc<str>),
+    #[display("{}", fl!("playlist"))]
+    Playlist(KId),
 }
 
 impl Page {
+    /// Get the icon associated with the page, to be displayed along with the [nav_bar::Model].
     pub fn icon(&self) -> Icon {
         let icon = match self {
             Page::Home => crate::icons::USER,
@@ -57,6 +60,7 @@ impl Page {
     }
 }
 
+/// Get the navigation bar for the pages.
 pub fn nav_bar_model() -> nav_bar::Model {
     macro_rules! insert {
         ($b:expr, $page:ident) => {
@@ -74,7 +78,8 @@ pub fn nav_bar_model() -> nav_bar::Model {
         .build()
 }
 
-pub fn not_found(page: Option<&Page>) -> Element<Message> {
+/// We got a page that was not implemented, or no page at all.
+pub fn not_found<'a>(page: Option<Page>) -> impl Into<Element<'a, Message>> {
     widget::column()
         .push(widget::text::title1(fl!("error-found")))
         .push_maybe(
@@ -86,35 +91,245 @@ pub fn not_found(page: Option<&Page>) -> Element<Message> {
         .height(Length::Fill)
         .align_x(Horizontal::Center)
         .align_y(Vertical::Center)
-        .into()
 }
 
-pub fn empty_page_label<'a>(title: impl AsRef<str>) -> widget::Row<'a, Message> {
-    vec![
-        (title.as_ref().to_string())
-            .apply(widget::text::title2)
-            .style(style::Text::Color(
-                theme::active().cosmic().warning_text_color().into(),
-            ))
-            .font(font::FONT_LIGHT)
-            .into(),
-        widget::horizontal_space(Length::Fill).into(),
-    ]
-    .apply(widget::row::with_children)
-    .width(Length::Fill)
-    .padding(theme::active().cosmic().space_m())
+/// Helper for the icons at the right of the title in pages.
+#[must_use]
+struct PageViewControl<'a> {
+    icon: &'static [u8],
+    message: Message,
+    on_non_empty_content: bool,
+    is_destructive_icon: bool,
+    marker: marker::PhantomData<Element<'a, Message>>,
+}
+
+impl<'a> PageViewControl<'a> {
+    /// Create a new control with an icon and an associated message.
+    pub fn new(icon: &'static [u8], message: Message) -> Self {
+        Self {
+            icon,
+            message,
+            on_non_empty_content: false,
+            is_destructive_icon: false,
+            marker: marker::PhantomData,
+        }
+    }
+
+    /// Make the button a destructive one.
+    pub fn destructive(self) -> Self {
+        Self {
+            is_destructive_icon: true,
+            ..self
+        }
+    }
+
+    /// Make the button active only when there is some content in the page.
+    pub fn need_content(self) -> Self {
+        Self {
+            on_non_empty_content: true,
+            ..self
+        }
+    }
+
+    /// Turn the helper into a proper [Element]. Needs to pass a flag to know if the page is empty
+    /// or not.
+    fn into_maybe_element(self, page_is_empty: bool) -> Element<'a, Message> {
+        let Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
+        let Self {
+            icon,
+            message,
+            on_non_empty_content,
+            is_destructive_icon,
+            marker: _,
+        } = self;
+
+        let button = widget::icon::from_svg_bytes(icon)
+            .symbolic(true)
+            .apply(widget::button::icon)
+            .on_press_maybe((!page_is_empty || !on_non_empty_content).then_some(message))
+            .padding(space_xxs)
+            .width(32)
+            .height(32);
+
+        match is_destructive_icon {
+            true => button.style(style::Button::Destructive).into(),
+            false => button.into(),
+        }
+    }
+}
+
+/// Helper to render main pages of the application.
+#[derive(Default)]
+#[must_use]
+struct PageView<'a> {
+    /// The title of the page. If none then we have an error page.
+    title: Option<Element<'a, Message>>,
+
+    /// Tells wether the title is a custom one. If the title is custom, then it's up to the caller
+    /// to pad the controls correctly, we won't insert the space in the [Self::view] function.
+    title_is_custom: bool,
+
+    /// If the page is empty (no content), we can display an alterntive title.
+    empty_title: Option<Element<'a, Message>>,
+
+    /// If we force the fact that there is content on the page (because the state of an ignore
+    /// element changed… like with the search thing…).
+    trust_me_i_have_content: bool,
+
+    /// The controls, at the right of the title or alternative title, from left icon to the right
+    /// one. The first element of the tuple is the data for the SVG icon, the second is wether the
+    /// button is enabled only when there is content in the page, the last one is the message to
+    /// send when the button is pressed.
+    controls: Vec<PageViewControl<'a>>,
+
+    /// The content of the page. From the top one to the bottom one.
+    content: Vec<Element<'a, Message>>,
+
+    /// The number of elements in [Self::content] that are ignore to decide if the page is empty.
+    ignore_content_count: usize,
+}
+
+impl<'a> PageView<'a> {
+    /// Set the title of the page. If the title is not set, a default page that shows that an error
+    /// was encountred will be shown.
+    pub fn title(self, title: impl ToString) -> Self {
+        Self {
+            title: Some(widget::text::title2(title.to_string()).into()),
+            ..self
+        }
+    }
+
+    /// Like [Self::title], but can use any [cosmic::Element].
+    pub fn custom_title(self, title: impl Into<Element<'a, Message>>) -> Self {
+        Self {
+            title: Some(title.into()),
+            title_is_custom: true,
+            ..self
+        }
+    }
+
+    /// If set, when the page is empty, this title will be shown instead of the normal one.
+    pub fn empty_title(self, title: impl ToString) -> Self {
+        Self {
+            empty_title: Some(widget::text::title2(title.to_string()).into()),
+            ..self
+        }
+    }
+
+    /// Sets the title like [Self::title] with the first passed string and the empty title like
+    /// [Self::empty_title] with the second passed string.
+    pub fn titles(self, title: impl ToString, empty: impl ToString) -> Self {
+        self.title(title).empty_title(empty)
+    }
+
+    /// Adds an element to the page. Elements are pushed at the bottom of the page.
+    pub fn push(mut self, element: impl Into<Element<'a, Message>>) -> Self {
+        self.content.push(element.into());
+        self
+    }
+
+    /// Adds an element to the page. Elements are pushed at the bottom of the page. The pushed
+    /// element will be ignore to decide if the page is empty or not. This is usefull for when we
+    /// push more controls or informations, but are not relevent about if we have data in the page.
+    pub fn push_and_ignore_for_empty(mut self, element: impl Into<Element<'a, Message>>) -> Self {
+        self.content.push(element.into());
+        self.ignore_content_count += 1;
+        self
+    }
+
+    /// Adds an element to the page. Elements are pushed at the bottom of the page. If the push
+    /// flag is false, the element is not pushed into the content list.
+    pub fn push_when<E>(self, push: bool, cb: impl FnOnce() -> E) -> Self
+    where
+        E: Into<Element<'a, Message>>,
+    {
+        match push {
+            true => self.push(cb()),
+            false => self,
+        }
+    }
+
+    /// Push elements from a thing that can be turned into an iterator.
+    pub fn extend(self, iter: impl IntoIterator<Item = impl Into<Element<'a, Message>>>) -> Self {
+        iter.into_iter().fold(self, |this, e| this.push(e))
+    }
+
+    /// Push a button in the control part of the page's title bar. Buttons are pushed at the right
+    /// of the control pane.
+    pub fn controls(self, controls: impl IntoIterator<Item = PageViewControl<'a>>) -> Self {
+        controls.into_iter().fold(self, |mut this, control| {
+            this.controls.push(control);
+            this
+        })
+    }
+
+    /// Set the flag about having content to true, this forces the check to see if the page is
+    /// empty or not to false, thus activating some buttons if needed.
+    pub fn has_content(self, flag: bool) -> Self {
+        Self {
+            trust_me_i_have_content: flag,
+            ..self
+        }
+    }
+
+    /// Consumte the setting struct and returns the page.
+    pub fn view(self) -> impl Into<Element<'a, Message>> {
+        let Some(title) = self.title else {
+            // Not found, got an error…
+            return widget::column()
+                .push(
+                    widget::text::title1(fl!("error-found")).style(style::Text::Color(
+                        theme::active().cosmic().warning_text_color().into(),
+                    )),
+                )
+                .align_items(Alignment::Center)
+                .apply(widget::container)
+                .width(Length::Fill)
+                .height(Length::Fill)
+                .align_x(Horizontal::Center)
+                .align_y(Vertical::Center);
+        };
+
+        // Handle the page.
+        let is_empty =
+            (self.content.len() <= self.ignore_content_count) && !self.trust_me_i_have_content;
+        let title = match is_empty {
+            true => self.empty_title.unwrap_or(title),
+            false => title,
+        };
+        let Spacing { space_m, .. } = theme::active().cosmic().spacing;
+
+        let title_row = page_title(title, self.title_is_custom, self.controls, is_empty);
+        widget::column::with_capacity(1 + self.content.len())
+            .push(title_row)
+            .extend(self.content.into_iter())
+            .spacing(space_m)
+            .apply(widget::container)
+            .padding(theme::active().cosmic().space_m())
+            .width(Length::Fill)
+            .height(Length::Fill)
+            .align_x(Horizontal::Center)
+            .align_y(Vertical::Top)
+    }
 }
 
-pub fn page_label<'a>(title: impl AsRef<str>) -> widget::Row<'a, Message> {
-    vec![
-        (title.as_ref().to_string())
-            .apply(widget::text::title2)
-            .style(style::Text::Default)
-            // .font(font::FONT_LIGHT)
-            .into(),
-        widget::horizontal_space(Length::Fill).into(),
-    ]
-    .apply(widget::row::with_children)
-    .width(Length::Fill)
-    .padding(theme::active().cosmic().space_m())
+fn page_title<'a>(
+    title: impl Into<Element<'a, Message>>,
+    title_is_custom: bool,
+    controls: Vec<PageViewControl<'a>>,
+    page_is_empty: bool,
+) -> impl Into<Element<'a, Message>> {
+    let Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
+    widget::row::with_capacity(2 + controls.len())
+        .push(title)
+        .push_maybe((!title_is_custom).then(|| widget::horizontal_space(Length::Fill)))
+        .extend(
+            controls
+                .into_iter()
+                .map(|control| control.into_maybe_element(page_is_empty)),
+        )
+        .width(Length::Fill)
+        .height(48)
+        .align_items(Alignment::Center)
+        .spacing(space_xxs)
 }
diff --git a/amadeus/src/app/pages/history.rs b/amadeus/src/app/pages/history.rs
index 459628a7..7f768e05 100644
--- a/amadeus/src/app/pages/history.rs
+++ b/amadeus/src/app/pages/history.rs
@@ -1,16 +1,34 @@
 use crate::{
-    app::{kard, pages, Message},
+    app::{
+        kard,
+        pages::{PageView, PageViewControl},
+        LektordCommand, Message,
+    },
+    fl,
     store::Store,
 };
-use cosmic::{widget, Apply as _, Element};
+use cosmic::{prelude::*, widget};
 
-pub fn view(store: &Store) -> Element<Message> {
-    match store.iter_history().count() == 0 {
-        true => pages::empty_page_label("The history is empty").apply(Element::<Message>::from),
-        false => (store.iter_history())
-            .fold(widget::list_column(), |list, kid| {
+/// Display the history page.
+pub fn view(store: &Store) -> impl Into<Element<Message>> {
+    PageView::default()
+        .titles(fl!("history"), fl!("empty-history"))
+        .controls([
+            PageViewControl::new(
+                crate::icons::RETRY,
+                Message::SendCommand(LektordCommand::HistoryGet),
+            ),
+            PageViewControl::new(
+                crate::icons::METEOR,
+                Message::SendCommand(LektordCommand::HistoryClear),
+            )
+            .destructive()
+            .need_content(),
+        ])
+        .push_when(!store.iter_history().is_empty(), || {
+            (store.iter_history()).fold(widget::list_column(), |list, kid| {
                 list.add(kard::view(store.get(kid)))
             })
-            .apply(Element::<Message>::from),
-    }
+        })
+        .view()
 }
diff --git a/amadeus/src/app/pages/home.rs b/amadeus/src/app/pages/home.rs
index 9bf742d7..71d31c08 100644
--- a/amadeus/src/app/pages/home.rs
+++ b/amadeus/src/app/pages/home.rs
@@ -1,9 +1,10 @@
 use crate::{
-    app::{pages, Message},
+    app::{pages::PageView, Message},
     fl,
 };
-use cosmic::{Apply as _, Element};
+use cosmic::prelude::*;
 
-pub fn view<'a>() -> Element<'a, Message> {
-    pages::page_label(fl!("home")).apply(Element::<Message>::from)
+/// Display the home page.
+pub fn view<'a>() -> impl Into<Element<'a, Message>> {
+    PageView::default().title(fl!("home")).view()
 }
diff --git a/amadeus/src/app/pages/playlist.rs b/amadeus/src/app/pages/playlist.rs
index 4151fff2..a1202662 100644
--- a/amadeus/src/app/pages/playlist.rs
+++ b/amadeus/src/app/pages/playlist.rs
@@ -1,23 +1,84 @@
 use crate::{
-    app::{kard, pages, Message},
+    app::{
+        kard,
+        pages::{PageView, PageViewControl},
+        LektordCommand, Message,
+    },
     fl,
     store::Store,
 };
-use cosmic::{widget, Apply as _, Element};
+use cosmic::{prelude::*, style, widget};
+use lektor_payloads::{KId, PlaylistInfo};
 
-pub fn view<'a>(store: &'a Store, playlist: &str) -> Element<'a, Message> {
-    match store.iter_playlist_content(playlist).count() == 0 {
-        true => pages::empty_page_label(format!("The playlist {playlist} is empty"))
-            .apply(Element::<Message>::from),
-        false => vec![
-            pages::page_label(fl!("playlist", name = playlist)).apply(Element::<Message>::from),
-            (store.iter_playlist_content(playlist))
-                .fold(widget::list_column(), |list, kid| {
-                    list.add(kard::view(store.get(kid)))
-                })
-                .apply(Element::<Message>::from),
-        ]
-        .apply(widget::row::with_children)
-        .apply(Element::<Message>::from),
-    }
+/// Display the page about a specific playlist.
+pub fn view(store: &Store, id: KId) -> impl Into<Element<Message>> {
+    let Some(infos) = store.playlist(id) else {
+        todo!()
+    };
+
+    let name = infos
+        .name()
+        .map(|name| fl!("playlist", name = name))
+        .unwrap_or_else(|| "untitled".to_string());
+
+    let owners = infos
+        .infos()
+        .into_iter()
+        .flat_map(PlaylistInfo::owners)
+        .map(|owner| -> Element<'_, Message> {
+            widget::text::body(owner)
+                .apply(widget::button::custom)
+                .style(style::Button::Transparent)
+                .into()
+        });
+
+    let (created_at, updated_at) = infos
+        .infos()
+        .map(|infos| (infos.created_at(), infos.updated_at()))
+        .unwrap_or_default();
+
+    let infos = widget::settings::section()
+        .add(widget::settings::item(
+            "owners",
+            widget::row::with_children(owners.collect()),
+        ))
+        .add(widget::settings::item(
+            "created at",
+            widget::text::body(created_at.format("%Y-%m-%d %H:%M:%S").to_string()),
+        ))
+        .add(widget::settings::item(
+            "last modified at",
+            widget::text::body(updated_at.format("%Y-%m-%d %H:%M:%S").to_string()),
+        ));
+
+    let display_playlist_content = || {
+        (store.iter_playlist_content(id)).fold(widget::list_column(), |list, kid| {
+            list.add(kard::view(store.get(kid)))
+        })
+    };
+
+    PageView::default()
+        .titles(&name, format!("The playlist {name} is empty"))
+        .controls([
+            PageViewControl::new(
+                crate::icons::SHUFFLE,
+                Message::SendCommand(LektordCommand::PlaylistShuffleContent(id)),
+            )
+            .need_content(),
+            PageViewControl::new(
+                crate::icons::RETRY,
+                Message::SendCommand(LektordCommand::PlaylistGetContent(id)),
+            ),
+            PageViewControl::new(
+                crate::icons::METEOR,
+                Message::SendCommand(LektordCommand::PlaylistDelete(id)),
+            )
+            .destructive(),
+        ])
+        .push_and_ignore_for_empty(infos)
+        .push_when(
+            !store.iter_playlist_content(id).is_empty(),
+            display_playlist_content,
+        )
+        .view()
 }
diff --git a/amadeus/src/app/pages/playlists.rs b/amadeus/src/app/pages/playlists.rs
index 839f0db9..51745b73 100644
--- a/amadeus/src/app/pages/playlists.rs
+++ b/amadeus/src/app/pages/playlists.rs
@@ -1,10 +1,33 @@
 use crate::{
-    app::{pages, Message},
+    app::{
+        pages::{PageView, PageViewControl},
+        LektordCommand, Message,
+    },
     fl,
+    playlist::Playlist,
     store::Store,
 };
-use cosmic::{Apply as _, Element};
+use cosmic::{prelude::*, widget};
 
-pub fn view(_store: &Store) -> Element<Message> {
-    pages::page_label(fl!("playlists")).apply(Element::<Message>::from)
+fn view_playlist_card(playlist: &Playlist) -> impl Into<Element<Message>> {
+    widget::text(playlist.name().unwrap_or("untitled"))
+}
+
+/// Display all playlists in a page.
+pub fn view(store: &Store) -> impl Into<Element<Message>> {
+    PageView::default()
+        .titles(fl!("playlists"), fl!("empty-playlists"))
+        .controls([PageViewControl::new(
+            crate::icons::RETRY,
+            Message::SendCommand(LektordCommand::PlaylistsGet),
+        )])
+        .push_when(store.iter_playlists().count() != 0, || {
+            store
+                .iter_playlists()
+                .map(view_playlist_card)
+                .map(Into::into)
+                .collect::<Vec<_>>()
+                .apply(widget::flex_row)
+        })
+        .view()
 }
diff --git a/amadeus/src/app/pages/queue.rs b/amadeus/src/app/pages/queue.rs
index 86f851c6..53938deb 100644
--- a/amadeus/src/app/pages/queue.rs
+++ b/amadeus/src/app/pages/queue.rs
@@ -1,14 +1,36 @@
 use crate::{
-    app::{kard, pages, Message},
+    app::{kard, pages::PageView, LektordCommand, Message},
     fl,
     store::Store,
 };
-use cosmic::{widget, Apply as _, Element};
+use cosmic::{prelude::*, style, widget};
 use lektor_payloads::{Priority, PRIORITY_VALUES};
 
-fn view_queue_level(store: &Store, level: Priority) -> Element<Message> {
-    // TODO: Add the controls
-    let header = widget::text::title1(format!("{} {level}", fl!("queue")));
+use super::{page_title, PageViewControl};
+
+fn view_queue_level(store: &Store, level: Priority) -> impl IntoIterator<Item = Element<Message>> {
+    let title = page_title(
+        widget::text::title2(format!("{} {level}", fl!("queue"))).style(style::Text::Default),
+        false,
+        vec![
+            PageViewControl::new(
+                crate::icons::SHUFFLE,
+                Message::SendCommand(LektordCommand::QueueLevelShuffle(level)),
+            )
+            .need_content(),
+            PageViewControl::new(
+                crate::icons::RETRY,
+                Message::SendCommand(LektordCommand::QueueLevelGet(level)),
+            ),
+            PageViewControl::new(
+                crate::icons::METEOR,
+                Message::SendCommand(LektordCommand::QueueLevelClear(level)),
+            )
+            .need_content()
+            .destructive(),
+        ],
+        store.iter_queue_level(level).is_empty(),
+    );
 
     let content = store
         .iter_queue_level(level)
@@ -16,20 +38,42 @@ fn view_queue_level(store: &Store, level: Priority) -> Element<Message> {
             list.add(kard::view(store.get(kid)))
         });
 
-    vec![header.into(), content.into()]
-        .apply(widget::column::with_children)
-        .into()
+    [title.into(), content.into()]
 }
 
-pub fn view(store: &Store) -> Element<Message> {
-    match store.iter_queue().count() == 0 {
-        true => pages::empty_page_label("The queue is empty").apply(Element::<Message>::from),
-        false => (PRIORITY_VALUES.iter().rev().copied())
-            .flat_map(|level| {
-                (store.iter_queue_level(level).count() != 0).then(|| view_queue_level(store, level))
-            })
-            .collect::<Vec<Element<Message>>>()
-            .apply(widget::column::with_children)
-            .into(),
-    }
+/// Displays the page with the queue.
+pub fn view(store: &Store) -> impl Into<Element<Message>> {
+    PageView::default()
+        .titles(fl!("queue"), fl!("empty-queue"))
+        .controls([
+            PageViewControl::new(
+                crate::icons::SHUFFLE,
+                Message::SendCommand(LektordCommand::QueueShuffle),
+            )
+            .need_content(),
+            PageViewControl::new(
+                crate::icons::RETRY,
+                Message::SendCommand(LektordCommand::QueueGet),
+            ),
+            PageViewControl::new(
+                crate::icons::CROP,
+                Message::SendCommand(LektordCommand::QueueCrop),
+            )
+            .need_content()
+            .destructive(),
+            PageViewControl::new(
+                crate::icons::METEOR,
+                Message::SendCommand(LektordCommand::QueueClear),
+            )
+            .need_content()
+            .destructive(),
+        ])
+        .extend(
+            (PRIORITY_VALUES.iter().rev())
+                .flat_map(|&lvl| {
+                    (!store.iter_queue_level(lvl).is_empty()).then(|| view_queue_level(store, lvl))
+                })
+                .flatten(),
+        )
+        .view()
 }
diff --git a/amadeus/src/app/pages/search.rs b/amadeus/src/app/pages/search.rs
index df9f9067..b427ed70 100644
--- a/amadeus/src/app/pages/search.rs
+++ b/amadeus/src/app/pages/search.rs
@@ -1,7 +1,6 @@
-use crate::{
-    app::{kard, pages, AppModel, Message},
-    fl,
-};
+//! Contains everything to implement search from Amadeus.
+
+use crate::app::{kard, pages::PageView, AppModel, Message};
 use cosmic::{iced::Alignment, prelude::*, style, theme, widget};
 use lektor_payloads::KaraBy;
 use std::{
@@ -10,6 +9,8 @@ use std::{
     sync::atomic::{AtomicUsize, Ordering},
 };
 
+use super::PageViewControl;
+
 #[derive(Debug, Clone, PartialEq, Eq, Hash)]
 pub struct FilterAtom(FilterAtomId, KaraBy);
 
@@ -19,10 +20,6 @@ pub struct FilterAtomId(usize);
 #[derive(Debug, Default)]
 pub struct Filter(String, Vec<FilterAtom>);
 
-fn vertical_space<'a>() -> Element<'a, Message> {
-    widget::vertical_space(theme::active().cosmic().space_m()).into()
-}
-
 macro_rules! icon {
     ($icon:ident) => {
         widget::icon::from_svg_bytes(crate::icons::$icon)
@@ -48,19 +45,19 @@ impl FilterAtomId {
 
 impl FilterAtom {
     fn view(&self) -> Element<Message> {
-        let (maybe_icon, text) = match &self.1 {
-            KaraBy::Id(id) => (Some(icon!(HASHTAG)), id.to_string()),
-            KaraBy::Query(query) => (None, query.clone()),
-            KaraBy::Tag((name, None)) => (Some(icon!(TAG)), name.clone()),
-            KaraBy::Tag((name, Some(value))) => (Some(icon!(TAGS)), format!("{name}:{value}")),
-            KaraBy::SongType(song_type) => (None, song_type.to_string()),
-            KaraBy::SongOrigin(song_origin) => (None, song_origin.to_string()),
-            KaraBy::Author(author) => (Some(icon!(USER)), author.clone()),
-            KaraBy::Playlist(playlist) => (Some(icon!(FOLDER)), playlist.clone()),
+        let (icon, text) = match &self.1 {
+            KaraBy::Id(id) => (icon!(HASHTAG), id.to_string()),
+            KaraBy::Query(query) => (icon!(FILTER), query.clone()),
+            KaraBy::Tag((name, None)) => (icon!(TAG), name.clone()),
+            KaraBy::Tag((name, Some(value))) => (icon!(TAGS), format!("{name}:{value}")),
+            KaraBy::SongType(song_type) => (icon!(HASHTAG), song_type.to_string()),
+            KaraBy::SongOrigin(song_origin) => (icon!(HASHTAG), song_origin.to_string()),
+            KaraBy::Author(author) => (icon!(USER), author.clone()),
+            KaraBy::Playlist(playlist) => (icon!(FOLDER), playlist.clone()),
         };
 
         widget::row::with_capacity(2)
-            .push_maybe(maybe_icon)
+            .push(icon)
             .push(widget::text(text))
             .align_items(Alignment::Center)
             .spacing(theme::active().cosmic().space_xxxs())
@@ -72,54 +69,24 @@ impl FilterAtom {
 }
 
 impl Filter {
-    fn can_be_clear(&self) -> bool {
-        !self.1.is_empty() || !self.0.is_empty()
-    }
-
-    fn can_be_submited(&self) -> bool {
-        !self.1.is_empty()
-    }
-
-    fn view(&self) -> Element<Message> {
-        let space_xxs = theme::active().cosmic().space_xxs();
-
-        let staging_text_input = widget::text_input(
+    fn view_staging_row(&self) -> impl Into<Element<Message>> {
+        widget::text_input(
             "@author | tag:value | tag: | #playlist | OP | anime | ...",
             &self.0,
         )
         .on_submit(Message::AddFilterAtomFromStageBuffer)
         .on_input(Message::ChangeFilterStageBuffer)
-        .on_clear(Message::ChangeFilterStageBuffer(String::default()));
+        .on_clear(Message::ChangeFilterStageBuffer(String::default()))
+    }
 
-        let button_commit_filters = widget::button::custom(icon!(FILTER))
-            .on_press_maybe(self.can_be_submited().then_some(Message::QueryWithFilters))
-            .padding(space_xxs)
-            .height(32)
-            .width(32);
+    fn view_filters_row(&self) -> impl Into<Element<Message>> {
+        (self.1.iter().map(FilterAtom::view).collect::<Vec<_>>())
+            .apply(widget::flex_row)
+            .spacing(theme::active().cosmic().space_xxs())
+    }
 
-        let button_clear_filters = widget::button::custom(icon!(METEOR))
-            .on_press_maybe(self.can_be_clear().then_some(Message::ClearFilters))
-            .style(style::Button::Destructive)
-            .padding(space_xxs)
-            .height(32)
-            .width(32);
-
-        let staging_row = widget::row::with_capacity(3)
-            .push(staging_text_input)
-            .push(button_commit_filters)
-            .push(button_clear_filters)
-            .align_items(Alignment::Center)
-            .spacing(space_xxs)
-            .into();
-
-        Element::from(widget::column::with_children(vec![
-            staging_row,
-            vertical_space(),
-            (self.1.iter().map(FilterAtom::view).collect::<Vec<_>>())
-                .apply(widget::flex_row)
-                .spacing(space_xxs)
-                .apply(Element::<Message>::from),
-        ]))
+    pub fn is_empty(&self) -> bool {
+        self.1.is_empty()
     }
 
     pub fn clear(&mut self) {
@@ -132,11 +99,17 @@ impl Filter {
     }
 
     pub fn commit(&mut self) {
-        let atom: FilterAtom = self.0.parse().expect("infallible");
-        if !(self.1.iter()).any(|commited| commited.0 == atom.0 || commited.1 == atom.1) {
-            self.1.push(atom);
+        if !self.0.is_empty() {
+            let atom: FilterAtom = self.0.parse().expect("infallible");
+            if !(self.1.iter()).any(|commited| commited.0 == atom.0 || commited.1 == atom.1) {
+                self.1.push(atom);
+            }
+            self.0.clear();
         }
-        self.0.clear();
+    }
+
+    pub fn iter_cloned(&self) -> impl Iterator<Item = KaraBy> + '_ {
+        self.1.iter().map(|FilterAtom(_, filter)| filter.clone())
     }
 
     pub fn remove(&mut self, id: FilterAtomId) {
@@ -148,15 +121,22 @@ impl Filter {
     }
 }
 
-pub fn view(app: &AppModel) -> Element<Message> {
-    widget::column::with_capacity(4)
-        .push(pages::page_label(fl!("search")))
-        .push(app.search_filter.view())
-        .push(vertical_space())
-        .push_maybe((!app.search_results.is_empty()).then(|| {
-            (app.search_results.iter()).fold(widget::list_column(), |list, kid| {
+/// Display the search page.
+pub fn view(app: &AppModel) -> impl Into<Element<Message>> {
+    PageView::default()
+        .custom_title(app.search_filter.view_staging_row())
+        .controls([
+            PageViewControl::new(crate::icons::FILTER, Message::QueryWithFilters).need_content(),
+            PageViewControl::new(crate::icons::METEOR, Message::ClearFilters)
+                .need_content()
+                .destructive(),
+        ])
+        .has_content(!app.search_filter.is_empty() || !app.search_results.is_empty())
+        .push_and_ignore_for_empty(app.search_filter.view_filters_row())
+        .push_when(!app.search_results.is_empty(), || {
+            (app.search_results.iter()).fold(widget::list_column(), |list, &kid| {
                 list.add(kard::view(app.store.get(kid)))
             })
-        }))
-        .into()
+        })
+        .view()
 }
diff --git a/amadeus/src/app/progress_bar.rs b/amadeus/src/app/progress_bar.rs
index 67ad1d34..2bfde63c 100644
--- a/amadeus/src/app/progress_bar.rs
+++ b/amadeus/src/app/progress_bar.rs
@@ -1,10 +1,15 @@
+//! Contains things to display the progress bar of the currently playing kara.
+
 use crate::app::{AppModel, Message};
-use cosmic::{iced::Alignment, style, theme, widget, Apply as _, Element};
+use cosmic::{font, iced::Alignment, prelude::*, style, theme, widget};
 
+/// Utility to format the time in a `MM:SS` way. Note that we can show more that 59 minutes, no
+/// kara should be an hour long anyway.
 fn format_time(secs: f32) -> String {
     format!("{}:{}", secs / 60.0, secs % 60.0)
 }
 
+/// View the progress bar for the currently playing kara.
 pub fn view(app: &AppModel) -> Element<Message> {
     let Some((time, duration)) = app.lektord_state.current_times() else {
         return widget::row().into();
@@ -13,7 +18,7 @@ pub fn view(app: &AppModel) -> Element<Message> {
     widget::row::with_children(vec![
         format_time(time)
             .apply(widget::text::monotext)
-            .font(cosmic::font::FONT_LIGHT)
+            .font(font::light())
             .style(style::Text::Accent)
             .into(),
         widget::progress_bar(0.0..=duration, time)
@@ -23,7 +28,7 @@ pub fn view(app: &AppModel) -> Element<Message> {
             .into(),
         format_time(duration)
             .apply(widget::text::monotext)
-            .font(cosmic::font::FONT_LIGHT)
+            .font(font::light())
             .style(style::Text::Accent)
             .into(),
     ])
diff --git a/amadeus/src/subscriptions.rs b/amadeus/src/app/subscriptions.rs
similarity index 100%
rename from amadeus/src/subscriptions.rs
rename to amadeus/src/app/subscriptions.rs
diff --git a/amadeus/src/subscriptions/playback.rs b/amadeus/src/app/subscriptions/playback.rs
similarity index 87%
rename from amadeus/src/subscriptions/playback.rs
rename to amadeus/src/app/subscriptions/playback.rs
index df1ad3b4..c5329911 100644
--- a/amadeus/src/subscriptions/playback.rs
+++ b/amadeus/src/app/subscriptions/playback.rs
@@ -23,12 +23,12 @@ impl Suscription {
             loop {
                 match requests::get_status(config.read().await.as_ref()).await {
                     Ok(state) => {
-                        _ = channel.send(Lektord(PlaybackUpdate(state))).await;
+                        _ = channel.send(LektordUpdate(PlaybackUpdate(state))).await;
                         tokio::time::sleep(Duration::from_secs(1)).await;
                     }
                     Err(err) => {
                         log::debug!("failed to connect to lektord: {err}");
-                        _ = channel.send(Lektord(Disconnected)).await;
+                        _ = channel.send(LektordUpdate(Disconnected)).await;
                         tokio::time::sleep(config.read().await.retry).await;
                     }
                 }
diff --git a/amadeus/src/subscriptions/updates.rs b/amadeus/src/app/subscriptions/updates.rs
similarity index 90%
rename from amadeus/src/subscriptions/updates.rs
rename to amadeus/src/app/subscriptions/updates.rs
index 5a4b77ed..1ddc792d 100644
--- a/amadeus/src/subscriptions/updates.rs
+++ b/amadeus/src/app/subscriptions/updates.rs
@@ -23,11 +23,11 @@ impl Suscription {
             'connect: loop {
                 log::debug!("try initial connection");
                 let Ok(infos) = requests::get_infos(config.read().await.as_ref()).await else {
-                    _ = channel.send(Lektord(Disconnected)).await;
+                    _ = channel.send(LektordUpdate(Disconnected)).await;
                     tokio::time::sleep(config.read().await.retry).await;
                     continue 'connect;
                 };
-                _ = channel.send(Lektord(Connected(infos))).await;
+                _ = channel.send(LektordUpdate(Connected(infos))).await;
 
                 loop {
                     log::debug!("here we want to query updates for the queue, history, etc…");
diff --git a/amadeus/src/config.rs b/amadeus/src/config.rs
index ba840ea8..24e21a14 100644
--- a/amadeus/src/config.rs
+++ b/amadeus/src/config.rs
@@ -8,6 +8,7 @@ use std::{
     time::Duration,
 };
 
+/// The configuration struct of our application.
 #[derive(Debug, Clone, CosmicConfigEntry, Eq, PartialEq)]
 #[version = 1]
 pub struct Config {
@@ -22,6 +23,7 @@ pub struct Config {
     kurisu_url: String,
 }
 
+/// The default URL for the Kurisu website.
 pub const KURISU_URL: &str = "https://kurisu.iiens.net";
 
 impl Config {
@@ -72,6 +74,7 @@ impl Default for Config {
     }
 }
 
+/// Wrapper to get a default address which is the localhost one.
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Display, From, Into, Serialize, Deserialize)]
 #[display("{socket_addr}")]
 #[repr(transparent)]
@@ -88,6 +91,7 @@ impl Default for Host {
     }
 }
 
+/// Wrapper to get a default log level, and merge the warn and error ones.
 #[derive(Debug, Clone, Copy, Eq, PartialEq, Display, Into)]
 #[display("{_0}")]
 #[repr(transparent)]
diff --git a/amadeus/src/i18n.rs b/amadeus/src/i18n.rs
index 4769970a..1b893e0c 100644
--- a/amadeus/src/i18n.rs
+++ b/amadeus/src/i18n.rs
@@ -14,16 +14,18 @@ pub fn init(requested_languages: &[LanguageIdentifier]) {
     }
 }
 
-// Get the `Localizer` to be used for localizing this library.
+/// Get the `Localizer` to be used for localizing this library.
 #[must_use]
 pub fn localizer() -> Box<dyn Localizer> {
     Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations))
 }
 
+/// Localizations for amadeus.
 #[derive(rust_embed::RustEmbed)]
 #[folder = "i18n/"]
 struct Localizations;
 
+/// The language loader for the application.
 pub static LANGUAGE_LOADER: LazyLock<FluentLanguageLoader> = LazyLock::new(|| {
     let loader: FluentLanguageLoader = fluent_language_loader!();
     loader
diff --git a/amadeus/src/icons.rs b/amadeus/src/icons.rs
index 26396513..28320f56 100644
--- a/amadeus/src/icons.rs
+++ b/amadeus/src/icons.rs
@@ -4,6 +4,7 @@
 macro_rules! icon {
     ($icon:ident : $file:literal) => {
         #[allow(unused)]
+        #[doc = concat!("The ", stringify!($icon), " icon, from 'rsc/icons/", $file, ".svg'")]
         pub const $icon: &[u8] = include_bytes!(concat!("../rsc/icons/", $file, ".svg"));
     };
 }
@@ -44,3 +45,5 @@ icon!(STOP:         "fontawesome/stop");
 icon!(SHUFFLE:      "fontawesome/shuffle");
 icon!(PLAY:         "fontawesome/play");
 icon!(PAUSE:        "fontawesome/pause");
+icon!(RETRY:        "fontawesome/retry");
+icon!(CROP:         "fontawesome/crop");
diff --git a/amadeus/src/lib.rs b/amadeus/src/lib.rs
new file mode 100644
index 00000000..eef7803a
--- /dev/null
+++ b/amadeus/src/lib.rs
@@ -0,0 +1,8 @@
+pub mod app;
+pub mod config;
+pub mod i18n;
+mod icons;
+mod playlist;
+mod store;
+
+include!(concat!(env!("OUT_DIR"), "/amadeus_build_infos.rs"));
diff --git a/amadeus/src/main.rs b/amadeus/src/main.rs
index 0978fba8..973b25d7 100644
--- a/amadeus/src/main.rs
+++ b/amadeus/src/main.rs
@@ -1,17 +1,10 @@
-mod app;
-mod config;
-mod i18n;
-mod icons;
-mod store;
-mod subscriptions;
-
-include!(concat!(env!("OUT_DIR"), "/amadeus_build_infos.rs"));
-
+use amadeus::{app, config, i18n};
 use anyhow::Context as _;
 use cosmic::cosmic_config::{self, CosmicConfigEntry as _};
 use lektor_utils::{appimage, logger};
 
 fn main() -> anyhow::Result<()> {
+    // Read the config. Also keep the cosmic thing that will allow us to write said config.
     let config = cosmic_config::Config::new(app::APP_ID, config::Config::VERSION).map(|ctx| {
         match config::Config::get_entry(&ctx)
             .inspect_err(|(errs, _)| errs.iter().for_each(|e| log::error!("config error: {e}")))
@@ -20,6 +13,7 @@ fn main() -> anyhow::Result<()> {
         }
     })?;
 
+    // Setup the logger. We ignore specific things around libcosmic and reqwest.
     logger::Builder::default()
         .level(config.0.log_level())
         .filter_targets([
@@ -28,9 +22,11 @@ fn main() -> anyhow::Result<()> {
         .init()
         .context("failed to init logger")?;
 
+    // On linux, detect if we are inside an AppImage.
     #[cfg(target_os = "linux")]
     appimage::detect_appimage()?;
 
+    // Launch the application.
     i18n::init(&i18n_embed::DesktopLanguageRequester::requested_languages());
     cosmic::app::run::<app::AppModel>(cosmic::app::Settings::default(), config)?;
     Ok(())
diff --git a/amadeus/src/playlist.rs b/amadeus/src/playlist.rs
new file mode 100644
index 00000000..dcff1f07
--- /dev/null
+++ b/amadeus/src/playlist.rs
@@ -0,0 +1,27 @@
+use lektor_payloads::{KId, PlaylistInfo};
+use std::mem;
+
+#[derive(Debug, Default, PartialEq, Eq)]
+pub struct Playlist {
+    infos: Option<PlaylistInfo>,
+    content: Vec<KId>,
+}
+
+impl Playlist {
+    pub fn name(&self) -> Option<&str> {
+        let infos = self.infos.as_ref()?;
+        (!infos.name().is_empty()).then_some(infos.name())
+    }
+
+    pub fn content(&self) -> impl Iterator<Item = KId> + '_ {
+        self.content.iter().copied()
+    }
+
+    pub fn set_content(&mut self, new: Vec<KId>) {
+        _ = mem::replace(&mut self.content, new);
+    }
+
+    pub fn infos(&self) -> Option<&PlaylistInfo> {
+        self.infos.as_ref()
+    }
+}
diff --git a/amadeus/src/store.rs b/amadeus/src/store.rs
index c6df4c90..6c1c7fba 100644
--- a/amadeus/src/store.rs
+++ b/amadeus/src/store.rs
@@ -4,12 +4,12 @@
 mod history;
 mod playlist_content;
 mod playlists;
-mod queue;
 mod queue_level;
 
+use crate::playlist::Playlist;
 use hashbrown::HashMap;
-use lektor_payloads::{KId, Kara, Playlist, PlaylistName, Priority, PRIORITY_LENGTH};
-use std::{fmt, mem};
+use lektor_payloads::{KId, Kara, Priority, PRIORITY_LENGTH};
+use std::mem;
 
 /// Stores the kara or its id if the [Kara] struct was not already cached in the [Store].
 #[derive(Debug, Clone)]
@@ -30,7 +30,7 @@ pub struct Store {
     karas: HashMap<KId, Kara>,
 
     /// All the informations about the playlists.
-    playlists: HashMap<PlaylistName, Playlist>,
+    playlists: HashMap<KId, Playlist>,
 
     /// The history, insert at the begin, remove at the end.
     history: Vec<KId>,
@@ -52,22 +52,30 @@ impl Store {
 
     /// Set the content of a playlist. Insert it if it didn't exists. Update it if the entry
     /// existed in the store.
-    pub fn set_playlist(&mut self, name: PlaylistName, plt: Playlist) {
-        _ = self.playlists.insert(name, plt);
+    pub fn set_playlist_content(&mut self, id: KId, plt: Vec<KId>) {
+        self.playlists.entry(id).or_default().set_content(plt);
     }
 
-    /// Keep playlists only if their name is specified in the passed [Vec].
-    pub fn keep_playlists(&mut self, names: Vec<PlaylistName>) {
-        self.playlists.retain(|key, _| names.contains(key));
+    /// Keep playlists only if their name is specified in the passed [Vec]. Returns the new
+    /// playlists in a vector.
+    pub fn keep_playlists(&mut self, ids: Vec<KId>) -> Vec<KId> {
+        self.playlists.retain(|key, _| ids.contains(key));
+        Vec::from_iter(ids.into_iter().flat_map(|id| {
+            (!self.playlists.contains_key(&id)).then(|| {
+                self.playlists.insert(id, Default::default());
+                id
+            })
+        }))
     }
-}
 
-impl Store {
-    /// Get the iterator to query all the levels of the queue.
-    pub fn iter_queue(&self) -> queue::QueueIter {
-        queue::QueueIter::new(self)
+    /// Set the metadata informations about a kar in the [Store]. Any previous information is
+    /// overwritten.
+    pub fn set(&mut self, kara: Kara) {
+        let _ = self.karas.insert(kara.id, kara);
     }
+}
 
+impl Store {
     /// Get the iterator to query a specific level of the queue.
     pub fn iter_queue_level(&self, level: Priority) -> queue_level::QueueLevelIter {
         queue_level::QueueLevelIter::new(self, level)
@@ -84,27 +92,22 @@ impl Store {
     }
 
     /// Get the iterator to list the playlist's content.
-    pub fn iter_playlist_content<S>(&self, name: S) -> playlist_content::PlaylistContentIter
-    where
-        S: TryInto<PlaylistName>,
-        <S as TryInto<PlaylistName>>::Error: fmt::Display,
-    {
-        match name.try_into() {
-            Ok(name) => playlist_content::PlaylistContentIter::new(self, name),
-            Err(err) => {
-                log::error!("{err}");
-                playlist_content::PlaylistContentIter::empty(self)
-            }
-        }
+    pub fn iter_playlist_content(&self, id: KId) -> playlist_content::PlaylistContentIter {
+        playlist_content::PlaylistContentIter::new(self, id)
     }
 
     /// Get a kara from the store. If the [Kara] was not already cached in the store, returns its
     /// [KId]... Note that if the passed [KId] is not valid, then we return a [KId] that doesn't
     /// really exists as we can't know if it's valid or not…
-    pub fn get(&self, kid: &KId) -> KaraOrId {
+    pub fn get(&self, kid: KId) -> KaraOrId {
         self.karas
-            .get(kid)
+            .get(&kid)
             .map(KaraOrId::Kara)
-            .unwrap_or_else(|| KaraOrId::Id(kid.clone()))
+            .unwrap_or_else(|| KaraOrId::Id(kid))
+    }
+
+    /// Get the informations about a playlist.
+    pub fn playlist(&self, id: KId) -> Option<&Playlist> {
+        self.playlists.get(&id)
     }
 }
diff --git a/amadeus/src/store/history.rs b/amadeus/src/store/history.rs
index b7e18576..eae05c48 100644
--- a/amadeus/src/store/history.rs
+++ b/amadeus/src/store/history.rs
@@ -1,3 +1,5 @@
+//! Implements the iterator for the history.
+
 use super::Store;
 use lektor_payloads::KId;
 
@@ -10,19 +12,25 @@ impl<'a> HistoryIter<'a> {
     pub(super) fn new(store: &'a Store) -> Self {
         Self(store, 0)
     }
+
+    /// Tells if the iterator is empty without consuming it.
+    pub fn is_empty(&self) -> bool {
+        self.count() == 0
+    }
 }
 
 impl<'a> Iterator for HistoryIter<'a> {
-    type Item = &'a KId;
+    type Item = KId;
 
     fn next(&mut self) -> Option<Self::Item> {
         (self.0.history)
             .get(self.1)
             .inspect(|_| self.1 += 1)
+            .copied()
     }
 
     fn last(self) -> Option<Self::Item> {
-        self.0.history.last()
+        self.0.history.last().copied()
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
diff --git a/amadeus/src/store/playlist_content.rs b/amadeus/src/store/playlist_content.rs
index 8f389bf1..1b6c7a20 100644
--- a/amadeus/src/store/playlist_content.rs
+++ b/amadeus/src/store/playlist_content.rs
@@ -1,42 +1,42 @@
+//! Implements the iterator for the content of a playlist.
+
 use super::Store;
-use lektor_payloads::{KId, PlaylistName};
+use lektor_payloads::KId;
 
 /// Wrapper struct to get the content of a playlist.
 #[derive(Debug, Clone)]
-pub struct PlaylistContentIter<'a>(&'a Store, Option<PlaylistName>, usize);
+pub struct PlaylistContentIter<'a>(&'a Store, KId, usize);
 
 impl<'a> PlaylistContentIter<'a> {
-    pub(super) fn empty(store: &'a Store) -> Self {
-        Self(store, None, 0)
+    pub(super) fn new(store: &'a Store, id: KId) -> Self {
+        Self(store, id, 0)
     }
 
-    pub(super) fn new(store: &'a Store, name: PlaylistName) -> Self {
-        Self(store, Some(name), 0)
+    /// Tells if the iterator is empty without consuming it.
+    pub fn is_empty(&self) -> bool {
+        self.size_hint().1.unwrap() == 0
     }
 }
 
 impl<'a> Iterator for PlaylistContentIter<'a> {
-    type Item = &'a KId;
+    type Item = KId;
 
     fn next(&mut self) -> Option<Self::Item> {
         (self.0.playlists)
-            .get(self.1.as_ref()?)?
+            .get(&self.1)?
             .content()
-            .get(self.2)
+            .nth(self.2)
             .inspect(|_| self.2 += 1)
     }
 
     fn last(self) -> Option<Self::Item> {
-        (self.0.playlists).get(self.1.as_ref()?)?.content().last()
+        (self.0.playlists).get(&self.1)?.content().last()
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
-        let Some(name) = self.1.as_ref() else {
-            return (0, Some(0));
-        };
         let size = (self.0.playlists)
-            .get(name)
-            .map(|plt| plt.content().len() - self.2)
+            .get(&self.1)
+            .map(|plt| plt.content().count() - self.2)
             .unwrap_or_default();
         (size, Some(size))
     }
diff --git a/amadeus/src/store/playlists.rs b/amadeus/src/store/playlists.rs
index 8734b713..2d51c8c2 100644
--- a/amadeus/src/store/playlists.rs
+++ b/amadeus/src/store/playlists.rs
@@ -1,10 +1,12 @@
-use super::Store;
+//! Implements the iterator for all the playlists and their meta-informations.
+
+use crate::{playlist::Playlist, store::Store};
 use hashbrown::hash_map;
-use lektor_payloads::{Playlist, PlaylistName};
+use lektor_payloads::KId;
 
 /// Wrapper struct to get the playlists.
 #[derive(Debug, Clone)]
-pub struct PlaylistsIter<'a>(&'a Store, hash_map::Keys<'a, PlaylistName, Playlist>);
+pub struct PlaylistsIter<'a>(&'a Store, hash_map::Keys<'a, KId, Playlist>);
 
 impl<'a> PlaylistsIter<'a> {
     pub(super) fn new(store: &'a Store) -> Self {
@@ -13,16 +15,14 @@ impl<'a> PlaylistsIter<'a> {
 }
 
 impl<'a> Iterator for PlaylistsIter<'a> {
-    type Item = (PlaylistName, &'a Playlist);
+    type Item = &'a Playlist;
 
     fn next(&mut self) -> Option<Self::Item> {
-        let key = self.1.next()?;
-        self.0.playlists.get(key).map(|plt| (key.clone(), plt))
+        self.0.playlists.get(self.1.next()?)
     }
 
     fn last(self) -> Option<Self::Item> {
-        let key = self.1.last()?;
-        self.0.playlists.get(key).map(|plt| (key.clone(), plt))
+        self.0.playlists.get(self.1.last()?)
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
diff --git a/amadeus/src/store/queue.rs b/amadeus/src/store/queue.rs
deleted file mode 100644
index 9805720e..00000000
--- a/amadeus/src/store/queue.rs
+++ /dev/null
@@ -1,37 +0,0 @@
-use super::Store;
-use lektor_payloads::{KId, Priority};
-
-/// Wrapper struct to get the content of the queue, any level. Implements [Iterator], use the
-/// [Iterator::next] functions to query the state. Returns kara from the one closest to being
-/// played to the farest.
-#[derive(Debug, Clone, Copy)]
-pub struct QueueIter<'a>(&'a Store, usize);
-
-impl<'a> QueueIter<'a> {
-    pub(super) fn new(store: &'a Store) -> Self {
-        Self(store, 0)
-    }
-}
-
-impl<'a> Iterator for QueueIter<'a> {
-    type Item = &'a KId;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        (self.0.queue.iter().rev().flatten())
-            .nth(self.1)
-            .inspect(|_| self.1 += 1)
-    }
-
-    fn last(self) -> Option<Self::Item> {
-        self.0.queue[Priority::min().index()].last()
-    }
-
-    fn size_hint(&self) -> (usize, Option<usize>) {
-        let size = self.0.queue.iter().map(|lvl| lvl.len()).sum::<usize>() - self.1;
-        (size, Some(size))
-    }
-
-    fn count(self) -> usize {
-        self.size_hint().1.unwrap()
-    }
-}
diff --git a/amadeus/src/store/queue_level.rs b/amadeus/src/store/queue_level.rs
index 2cabf0cb..c90f9952 100644
--- a/amadeus/src/store/queue_level.rs
+++ b/amadeus/src/store/queue_level.rs
@@ -1,3 +1,5 @@
+//! Implements the iterator to iterate only over a single level of the queue at once.
+
 use super::Store;
 use lektor_payloads::{KId, Priority};
 
@@ -11,19 +13,24 @@ impl<'a> QueueLevelIter<'a> {
     pub(super) fn new(store: &'a Store, level: Priority) -> Self {
         Self(store, level, 0)
     }
+
+    pub fn is_empty(&self) -> bool {
+        self.count() == 0
+    }
 }
 
 impl<'a> Iterator for QueueLevelIter<'a> {
-    type Item = &'a KId;
+    type Item = KId;
 
     fn next(&mut self) -> Option<Self::Item> {
         (self.0.queue[self.1.index()])
             .get(self.2)
             .inspect(|_| self.2 += 1)
+            .copied()
     }
 
     fn last(self) -> Option<Self::Item> {
-        (self.0.queue[self.1.index()]).last()
+        (self.0.queue[self.1.index()]).last().copied()
     }
 
     fn size_hint(&self) -> (usize, Option<usize>) {
diff --git a/kurisu_api/Cargo.toml b/kurisu_api/Cargo.toml
index b2d25cc3..5c6996e5 100644
--- a/kurisu_api/Cargo.toml
+++ b/kurisu_api/Cargo.toml
@@ -1,19 +1,22 @@
 [package]
 name = "kurisu_api"
+description = "Crate used to deserialize what Kurisu returns"
+rust-version.workspace = true
+
 version.workspace = true
 edition.workspace = true
 authors.workspace = true
 license.workspace = true
-rust-version.workspace = true
-description = "Crate used to deserialize what Kurisu returns"
 
 [lib]
 doctest = false
 
 [dependencies]
+log.workspace = true
 serde.workspace = true
 sha256.workspace = true
 hashbrown.workspace = true
+derive_more.workspace = true
 lektor_utils = { path = "../lektor_utils" }
 
 [dev-dependencies]
diff --git a/kurisu_api/src/error.rs b/kurisu_api/src/error.rs
new file mode 100644
index 00000000..dad7ece0
--- /dev/null
+++ b/kurisu_api/src/error.rs
@@ -0,0 +1,20 @@
+use derive_more::Display;
+use std::borrow::Cow;
+
+#[derive(Debug, Clone, Display)]
+#[display("{_0}")]
+pub struct Error(pub(crate) Cow<'static, str>);
+
+impl std::error::Error for Error {
+    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
+        None
+    }
+
+    fn description(&self) -> &str {
+        self.0.as_ref()
+    }
+
+    fn cause(&self) -> Option<&dyn std::error::Error> {
+        self.source()
+    }
+}
diff --git a/kurisu_api/src/lib.rs b/kurisu_api/src/lib.rs
index 44ce6a88..6f2839a7 100644
--- a/kurisu_api/src/lib.rs
+++ b/kurisu_api/src/lib.rs
@@ -2,6 +2,7 @@
 
 #![forbid(unsafe_code)]
 
+pub mod error;
 mod hash;
 pub mod v2;
 pub use hash::*;
diff --git a/kurisu_api/src/v2.rs b/kurisu_api/src/v2.rs
index ca9f93e7..35c9cae8 100644
--- a/kurisu_api/src/v2.rs
+++ b/kurisu_api/src/v2.rs
@@ -1,10 +1,10 @@
 //! Object rules for the Kurisu's V2 API
 
-use std::str::FromStr;
-
-use crate::SHA256;
-use hashbrown::HashSet;
+use crate::{error::Error, SHA256};
+use derive_more::Display;
+use hashbrown::{HashMap, HashSet};
 use serde::{Deserialize, Serialize};
+use std::{borrow, cmp, collections::BTreeSet, str::FromStr};
 
 /// We can have multiple kurisu repos.
 #[derive(Debug, PartialEq, Eq)]
@@ -14,24 +14,150 @@ pub struct Repo {
     pub infos: Infos,
 }
 
+/// Some infos about criteras. This is just an API specific thing, won't be accessible in other
+/// ways than getters.
+#[derive(Debug, Deserialize, PartialEq, Eq, Default)]
+struct Criterias {
+    /// Authors present in the database.
+    #[serde(default)]
+    author: HashSet<String>,
+
+    /// People that have favorites, will be imported as playlists.
+    #[serde(default)]
+    favorite_haver: HashSet<String>,
+
+    /// List of languages possible. The first item is the code, the second the name.
+    #[serde(default)]
+    language: Vec<(String, String)>,
+
+    /// Available song origins.
+    #[serde(default)]
+    song_origin: HashSet<SongOrigin>,
+
+    /// Available song types.
+    #[serde(default)]
+    song_type: HashSet<SongType>,
+}
+
+impl Criterias {
+    /// Merges two database [Criterias], can show warnings if some bizare things happens…
+    pub fn merge(mut self, mut other: Criterias) -> Self {
+        (self.language.iter())
+            .flat_map(|(c, name)| {
+                let idx = (other.language.iter().enumerate())
+                    .find_map(|(idx, (code, _))| (c.as_str() == code.as_str()).then_some(idx))?;
+                let (_, other) = other.language.remove(idx);
+                Some((c, name, other))
+            })
+            .for_each(|(c, name, other)| {
+                log::warn!("language with code '{c}' have multiple names: '{name}', '{other}'")
+            });
+
+        self.language.extend(other.language);
+        self.author.extend(other.author);
+        self.favorite_haver.extend(other.favorite_haver);
+        self.song_origin.extend(other.song_origin);
+        self.song_type.extend(other.song_type);
+        self
+    }
+}
+
 /// Some infos about the repo and its tags.
 #[derive(Debug, Deserialize, PartialEq, Eq, Default)]
 pub struct Infos {
     /// The current epoch of the database.
-    pub dbepoch: u64,
+    #[serde(default)]
+    dbepoch: u64,
 
     /// The tags with keys.
-    #[serde(rename = "TagKeys")]
-    pub tag_keys: HashSet<String>,
+    #[serde(rename = "valuefullTagKeys")]
+    #[serde(default)]
+    valuefull_tag_keys: HashSet<String>,
 
     /// The tags without keys.
-    #[serde(rename = "ValuelessTagKeys")]
-    pub valueless_tag_keys: HashSet<String>,
+    #[serde(rename = "valuelessTagKeys")]
+    #[serde(default)]
+    valueless_tag_keys: HashSet<String>,
+
+    /// The criterias.
+    #[serde(default)]
+    criterias: Criterias,
+}
+
+impl Infos {
+    /// Merges two database [Infos], can show warnings if some bizare things happens…
+    pub fn merge(mut self, other: Infos) -> Self {
+        if !(self.valueless_tag_keys).is_disjoint(&other.valuefull_tag_keys)
+            || !(self.valuefull_tag_keys).is_disjoint(&other.valueless_tag_keys)
+        {
+            log::warn!("valuefull and valueless tags share some keys…")
+        }
+
+        self.valueless_tag_keys.extend(other.valueless_tag_keys);
+        self.valuefull_tag_keys.extend(other.valuefull_tag_keys);
+        Self {
+            dbepoch: cmp::max(self.dbepoch, other.dbepoch),
+            valuefull_tag_keys: self.valuefull_tag_keys,
+            valueless_tag_keys: self.valueless_tag_keys,
+            criterias: self.criterias.merge(other.criterias),
+        }
+    }
+
+    /// Warn when there is intersection between valuefull and valueless tags.
+    pub fn warn_on_conflicting_tags(&self) {
+        if !(self.valuefull_tag_keys).is_disjoint(&self.valueless_tag_keys) {
+            log::warn!("valueless and valuefull tags have some shared keys…")
+        }
+    }
+
+    pub fn epoch(&self) -> u64 {
+        self.dbepoch
+    }
+
+    pub fn song_types(&self) -> impl Iterator<Item = SongType> + '_ {
+        self.criterias.song_type.iter().copied()
+    }
+
+    pub fn song_origins(&self) -> impl Iterator<Item = SongOrigin> + '_ {
+        self.criterias.song_origin.iter().copied()
+    }
+
+    pub fn languages(&self) -> impl Iterator<Item = &str> {
+        (self.criterias.language.iter()).map(|(code, _)| code.as_str())
+    }
+
+    pub fn language_code(&self, code: &str) -> Option<&str> {
+        (self.criterias.language.iter())
+            .find_map(|(c, n)| (code == c.as_str()).then_some(n.as_str()))
+    }
+
+    pub fn authors(&self) -> impl Iterator<Item = &str> {
+        self.criterias.author.iter().map(String::as_str)
+    }
+
+    pub fn favorites(&self) -> impl Iterator<Item = &str> {
+        self.criterias.favorite_haver.iter().map(String::as_str)
+    }
+
+    pub fn tags(&self) -> impl Iterator<Item = &str> {
+        self.valueless_tag_keys
+            .union(&self.valuefull_tag_keys)
+            .map(String::as_str)
+    }
+
+    pub fn valuefull_tags(&self) -> impl Iterator<Item = &str> {
+        self.valuefull_tag_keys.iter().map(String::as_str)
+    }
+
+    pub fn valueless_tags(&self) -> impl Iterator<Item = &str> {
+        self.valueless_tag_keys.iter().map(String::as_str)
+    }
 }
 
 /// The type of a song. One the the following, one per kara.
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash, Display)]
 #[serde(rename_all = "UPPERCASE")]
+#[display("{}", self.as_str())]
 pub enum SongType {
     OP,
     ED,
@@ -41,8 +167,9 @@ pub enum SongType {
 }
 
 /// The origin of a song's source. One the the following, one per kara.
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash)]
+#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Copy, Hash, Display)]
 #[serde(rename_all = "lowercase")]
+#[display("{}", self.as_str())]
 pub enum SongOrigin {
     Anime,
     VN,
@@ -76,7 +203,7 @@ impl SongOrigin {
 }
 
 impl FromStr for SongType {
-    type Err = String;
+    type Err = Error;
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
             "OP" => Ok(Self::OP),
@@ -84,13 +211,13 @@ impl FromStr for SongType {
             "IS" => Ok(Self::IS),
             "MV" => Ok(Self::MV),
             "OT" => Ok(Self::OT),
-            _ => Err(format!("unknown song type: {s}")),
+            _ => Err(Error(format!("unknown song type: {s}").into())),
         }
     }
 }
 
 impl FromStr for SongOrigin {
-    type Err = String;
+    type Err = Error;
     fn from_str(s: &str) -> Result<Self, Self::Err> {
         match s {
             "anime" => Ok(Self::Anime),
@@ -98,35 +225,52 @@ impl FromStr for SongOrigin {
             "game" => Ok(Self::Game),
             "music" => Ok(Self::Music),
             "other" => Ok(Self::Other),
-            _ => Err(format!("unknown song origin: {s}")),
+            _ => Err(Error(format!("unknown song origin: {s}").into())),
         }
     }
 }
 
-impl AsRef<str> for SongType {
-    fn as_ref(&self) -> &str {
+impl borrow::Borrow<str> for SongType {
+    fn borrow(&self) -> &str {
         self.as_str()
     }
 }
 
-impl AsRef<str> for SongOrigin {
+impl AsRef<str> for SongType {
     fn as_ref(&self) -> &str {
         self.as_str()
     }
 }
 
-impl std::fmt::Display for SongType {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.as_str())
+impl borrow::Borrow<str> for SongOrigin {
+    fn borrow(&self) -> &str {
+        self.as_str()
     }
 }
 
-impl std::fmt::Display for SongOrigin {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.as_str())
+impl AsRef<str> for SongOrigin {
+    fn as_ref(&self) -> &str {
+        self.as_str()
     }
 }
 
+/// The struct returned by kurisu when downloading a group of karas.
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+pub struct Karas {
+    /// The epoch.
+    #[serde(default)]
+    dbepoch: u64,
+
+    /// The validity (in seconds) of this epoch.
+    #[serde(rename = "dbepochValidity")]
+    #[serde(default)]
+    dbepoch_validity: u64,
+
+    /// All of the karas returned.
+    #[serde(default)]
+    karas: BTreeSet<Kara>,
+}
+
 /// The struct returned by kurisu. Intended to be deserialized from json.
 #[derive(Debug, Deserialize, PartialEq, Eq)]
 pub struct Kara {
@@ -142,13 +286,14 @@ pub struct Kara {
 
     /// The list of authors that contributed to the kara.
     #[serde(rename = "author")]
-    pub kara_makers: Vec<String>,
+    pub kara_makers: HashSet<String>,
 
     /// Is the kara a new one?
     pub is_new: bool,
 
     /// Is the kara a virtual one?
     #[serde(rename = "virtual")]
+    #[serde(default)]
     pub is_virtual: bool,
 
     /// The type of the song. Is it an ending, opening, etc.
@@ -170,11 +315,75 @@ pub struct Kara {
     pub epoch: u64,
 
     /// The size of the file.
+    #[serde(default)]
     pub filesize: u64,
 
     /// The sha256 hash of the file.
-    pub file_hash: SHA256,
+    #[serde(default)]
+    pub file_hash: Option<SHA256>,
 
     /// The tags ised in the kara.
+    #[serde(default)]
     pub tags: Vec<[String; 2]>,
 }
+
+impl Ord for Kara {
+    fn cmp(&self, other: &Self) -> cmp::Ordering {
+        PartialOrd::partial_cmp(&self, &other).unwrap()
+    }
+}
+impl PartialOrd for Kara {
+    #[allow(clippy::non_canonical_partial_ord_impl)]
+    fn partial_cmp(&self, other: &Self) -> Option<cmp::Ordering> {
+        PartialOrd::partial_cmp(&self.id, &other.id)
+    }
+}
+
+#[derive(Debug, Deserialize)]
+pub struct Favorites(HashMap<String, HashSet<u64>>);
+
+impl Favorites {
+    /// Names of favorite lists.
+    pub fn lists(&self) -> impl Iterator<Item = &str> {
+        self.0.keys().map(String::as_str)
+    }
+
+    /// The content of a favorite list.
+    pub fn list_content(&self, user: &str) -> impl Iterator<Item = u64> + '_ {
+        (self.0.get(user))
+            .map(|list| list.iter().copied())
+            .into_iter()
+            .flatten()
+    }
+}
+
+impl IntoIterator for Favorites {
+    type Item = (String, HashSet<u64>);
+    type IntoIter = <HashMap<String, HashSet<u64>> as IntoIterator>::IntoIter;
+
+    fn into_iter(self) -> Self::IntoIter {
+        self.0.into_iter()
+    }
+}
+
+#[derive(Debug, Deserialize, PartialEq, Eq)]
+pub struct Playlist {
+    /// The ID of the playlist.
+    id: u64,
+
+    /// The title of the playlist.
+    title: String,
+
+    /// The optional description of the playlist, can be empty.
+    #[serde(default)]
+    description: String,
+
+    /// The owners of this playlist, can be empty.
+    #[serde(default)]
+    owners: Vec<String>,
+
+    /// The content of this playlist, can be empty. The order is important and there can be
+    /// repetitions of karas.
+    #[serde(default)]
+    karas: Vec<u64>,
+}
diff --git a/kurisu_api/tests/dbinfo.json b/kurisu_api/tests/dbinfo.json
index cd9a5491..8bea1212 100644
--- a/kurisu_api/tests/dbinfo.json
+++ b/kurisu_api/tests/dbinfo.json
@@ -1,23 +1,199 @@
 {
-    "dbepoch": 1,
-    "TagKeys": [
-        "Version",
-        "Composer",
-        "VTuber",
-        "Version",
-        "Composer",
-        "VTuber",
-        "Version",
-        "Composer",
-        "VTuber",
-        "Version",
-        "Composer",
-        "VTuber"
+  "dbepoch": 95,
+  "criterias": {
+    "author": [ "Elliu", "fooPseudo", "TheOneAuthor", "TheOneAuthor2", "TTheOneAuthor2" ],
+    "language": [
+      [ "aa", "Afar" ],
+      [ "ab", "Abkhazian" ],
+      [ "af", "Afrikaans" ],
+      [ "ak", "Akan" ],
+      [ "sq", "Albanian" ],
+      [ "am", "Amharic" ],
+      [ "ar", "Arabic" ],
+      [ "an", "Aragonese" ],
+      [ "hy", "Armenian" ],
+      [ "as", "Assamese" ],
+      [ "av", "Avaric" ],
+      [ "ae", "Avestan" ],
+      [ "ay", "Aymara" ],
+      [ "az", "Azerbaijani" ],
+      [ "ba", "Bashkir" ],
+      [ "bm", "Bambara" ],
+      [ "eu", "Basque" ],
+      [ "be", "Belarusian" ],
+      [ "bn", "Bengali" ],
+      [ "bh", "Bihari languages" ],
+      [ "bi", "Bislama" ],
+      [ "bs", "Bosnian" ],
+      [ "br", "Breton" ],
+      [ "bg", "Bulgarian" ],
+      [ "my", "Burmese" ],
+      [ "ca", "Catalan" ],
+      [ "ch", "Chamorro" ],
+      [ "ce", "Chechen" ],
+      [ "zh", "Chinese" ],
+      [ "cu", "Old Slavonic" ],
+      [ "cv", "Chuvash" ],
+      [ "kw", "Cornish" ],
+      [ "co", "Corsican" ],
+      [ "cr", "Cree" ],
+      [ "cs", "Czech" ],
+      [ "da", "Danish" ],
+      [ "dv", "Maldivian" ],
+      [ "nl", "Dutch" ],
+      [ "dz", "Dzongkha" ],
+      [ "en", "English" ],
+      [ "eo", "Esperanto" ],
+      [ "et", "Estonian" ],
+      [ "ee", "Ewe" ],
+      [ "fo", "Faroese" ],
+      [ "fj", "Fijian" ],
+      [ "fi", "Finnish" ],
+      [ "fr", "French" ],
+      [ "fy", "Western Frisian" ],
+      [ "ff", "Fulah" ],
+      [ "ka", "Georgian" ],
+      [ "de", "German" ],
+      [ "gd", "Gaelic" ],
+      [ "ga", "Irish" ],
+      [ "gl", "Galician" ],
+      [ "gv", "Manx" ],
+      [ "el", "Greek" ],
+      [ "gn", "Guarani" ],
+      [ "gu", "Gujarati" ],
+      [ "ht", "Haitian" ],
+      [ "ha", "Hausa" ],
+      [ "he", "Hebrew" ],
+      [ "hz", "Herero" ],
+      [ "hi", "Hindi" ],
+      [ "ho", "Hiri Motu" ],
+      [ "hr", "Croatian" ],
+      [ "hu", "Hungarian" ],
+      [ "ig", "Igbo" ],
+      [ "is", "Icelandic" ],
+      [ "io", "Ido" ],
+      [ "ii", "Sichuan Yi" ],
+      [ "iu", "Inuktitut" ],
+      [ "ie", "Interlingue" ],
+      [ "ia", "Interlingua" ],
+      [ "id", "Indonesian" ],
+      [ "ik", "Inupiaq" ],
+      [ "it", "Italian" ],
+      [ "jv", "Javanese" ],
+      [ "ja", "Japanese" ],
+      [ "kl", "Kalaallisut" ],
+      [ "kn", "Kannada" ],
+      [ "ks", "Kashmiri" ],
+      [ "kr", "Kanuri" ],
+      [ "kk", "Kazakh" ],
+      [ "km", "Central Khmer" ],
+      [ "ki", "Kikuyu" ],
+      [ "rw", "Kinyarwanda" ],
+      [ "ky", "Kirghiz" ],
+      [ "kv", "Komi" ],
+      [ "kg", "Kongo" ],
+      [ "ko", "Korean" ],
+      [ "kj", "Kuanyama" ],
+      [ "ku", "Kurdish" ],
+      [ "lo", "Lao" ],
+      [ "la", "Latin" ],
+      [ "lv", "Latvian" ],
+      [ "li", "Limburgan" ],
+      [ "ln", "Lingala" ],
+      [ "lt", "Lithuanian" ],
+      [ "lb", "Luxembourgish" ],
+      [ "lu", "Luba-Katanga" ],
+      [ "lg", "Ganda" ],
+      [ "mk", "Macedonian" ],
+      [ "mh", "Marshallese" ],
+      [ "ml", "Malayalam" ],
+      [ "mi", "Maori" ],
+      [ "mr", "Marathi" ],
+      [ "ms", "Malay" ],
+      [ "mg", "Malagasy" ],
+      [ "mt", "Maltese" ],
+      [ "mn", "Mongolian" ],
+      [ "na", "Nauru" ],
+      [ "nv", "Navajo" ],
+      [ "nr", "South Ndebele" ],
+      [ "nd", "North Ndebele" ],
+      [ "ng", "Ndonga" ],
+      [ "ne", "Nepali" ],
+      [ "nn", "Norwegian Nynorsk" ],
+      [ "nb", "Norwegian Bokmål" ],
+      [ "no", "Norwegian" ],
+      [ "ny", "Chichewa" ],
+      [ "oc", "Occitan" ],
+      [ "oj", "Ojibwa" ],
+      [ "or", "Oriya" ],
+      [ "om", "Oromo" ],
+      [ "os", "Ossetian" ],
+      [ "pa", "Punjabi" ],
+      [ "fa", "Persian" ],
+      [ "pi", "Pali" ],
+      [ "pl", "Polish" ],
+      [ "pt", "Portuguese" ],
+      [ "ps", "Pashto" ],
+      [ "qu", "Quechua" ],
+      [ "rm", "Romansh" ],
+      [ "ro", "Romanian / Moldavian" ],
+      [ "rn", "Rundi" ],
+      [ "ru", "Russian" ],
+      [ "sg", "Sango" ],
+      [ "sa", "Sanskrit" ],
+      [ "si", "Sinhala" ],
+      [ "sk", "Slovak" ],
+      [ "sl", "Slovenian" ],
+      [ "se", "Northern Sami" ],
+      [ "sm", "Samoan" ],
+      [ "sn", "Shona" ],
+      [ "sd", "Sindhi" ],
+      [ "so", "Somali" ],
+      [ "st", "Southern Sotho" ],
+      [ "es", "Spanish" ],
+      [ "sc", "Sardinian" ],
+      [ "sr", "Serbian" ],
+      [ "ss", "Swati" ],
+      [ "su", "Sundanese" ],
+      [ "sw", "Swahili" ],
+      [ "sv", "Swedish" ],
+      [ "ty", "Tahitian" ],
+      [ "ta", "Tamil" ],
+      [ "tt", "Tatar" ],
+      [ "te", "Telugu" ],
+      [ "tg", "Tajik" ],
+      [ "tl", "Tagalog" ],
+      [ "th", "Thai" ],
+      [ "bo", "Tibetan" ],
+      [ "ti", "Tigrinya" ],
+      [ "to", "Tonga (Tonga Islands)" ],
+      [ "tn", "Tswana" ],
+      [ "ts", "Tsonga" ],
+      [ "tk", "Turkmen" ],
+      [ "tr", "Turkish" ],
+      [ "tw", "Twi" ],
+      [ "ug", "Uighur" ],
+      [ "uk", "Ukrainian" ],
+      [ "ur", "Urdu" ],
+      [ "uz", "Uzbek" ],
+      [ "ve", "Venda" ],
+      [ "vi", "Vietnamese" ],
+      [ "vo", "Volapük" ],
+      [ "cy", "Welsh" ],
+      [ "wa", "Walloon" ],
+      [ "wo", "Wolof" ],
+      [ "xh", "Xhosa" ],
+      [ "yi", "Yiddish" ],
+      [ "yo", "Yoruba" ],
+      [ "za", "Zhuang" ],
+      [ "zu", "Zulu" ],
+      [ "fx", "Fictional" ],
+      [ "ot", "Joker" ]
     ],
-    "ValuelessTagKeys": [
-        "VTuber",
-        "VTuber",
-        "VTuber",
-        "VTuber"
-    ]
+    "song_origin": [ "anime", "vn", "game", "music", "other" ],
+    "song_type": [ "OP", "ED", "IS", "MV", "OT" ],
+    "favorites_haver": [ "fooPseudo" ]
+  },
+  "valuefullTagKeys": [ "Number", "Composer", "ComposerButSuperLongToFaireChier" ],
+  "valuelessTagKeys": [ "VTuber" ]
 }
diff --git a/kurisu_api/tests/favorites.json b/kurisu_api/tests/favorites.json
new file mode 100644
index 00000000..d755994c
--- /dev/null
+++ b/kurisu_api/tests/favorites.json
@@ -0,0 +1,5 @@
+{
+  "fooPseudo": [ 6, 9544 ],
+  "zooBar": [ 5 ]
+}
+
diff --git a/kurisu_api/tests/karas.json b/kurisu_api/tests/karas.json
new file mode 100644
index 00000000..4636710d
--- /dev/null
+++ b/kurisu_api/tests/karas.json
@@ -0,0 +1,150 @@
+{
+  "dbepoch": 95,
+  "dbepochValidity": 2024072806,
+  "karas": [
+    {
+      "id": 9544,
+      "song_title": "Hyper",
+      "source_name": "Under Ninja",
+      "song_type": "OP",
+      "song_origin": "anime",
+      "is_new": false,
+      "created_at": 65974410587,
+      "updated_at": 65974410587,
+      "epoch": 0,
+      "file_hash": "01567e864bf255dab39245b5ab0c521ac960080f45b69406bba434dc5ba7985b",
+      "nextFilepath": "",
+      "filesize": 24693413,
+      "virtual": false,
+      "language": [ "ru" ],
+      "author": [ "Elliu" ],
+      "tags": [
+        [ "Number", "1" ],
+        [ "Composer", "xcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklak" ],
+        [ "Composer", "ejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzakl" ]
+      ]
+    },
+    {
+      "id": 5,
+      "song_title": "Shion",
+      "source_name": "niKu",
+      "song_type": "MV",
+      "song_origin": "vn",
+      "is_new": false,
+      "created_at": 1724741313,
+      "updated_at": 1724741320,
+      "epoch": 0,
+      "file_hash": "054c85b4e39b62897e24922af5c21593325100a5d75a57f5fa56c194e83eee67",
+      "nextFilepath": "",
+      "filesize": 44060958,
+      "virtual": false,
+      "language": [ "ab" ],
+      "author": [ "fooPseudo", "Elliu", "TheOneAuthor" ],
+      "tags": [
+        [ "VTuber", "" ],
+        [ "Composer", "xcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklaxcklak" ],
+        [ "Composer", "ejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzaklejzakl" ],
+        [ "ComposerButSuperLongToFaireChier", "aze" ]
+      ]
+    },
+    {
+      "id": 4,
+      "song_title": "Gif ni Ted",
+      "source_name": "Henjin no Salad Bowl",
+      "song_type": "OP",
+      "song_origin": "vn",
+      "is_new": false,
+      "created_at": 1724740583,
+      "updated_at": 1724740596,
+      "epoch": 0,
+      "file_hash": "91b3afcfba5d9022f9b72e0b94127d9f1a11cc047f8540e2393c702429de80ce",
+      "nextFilepath": "",
+      "filesize": 24201908,
+      "virtual": false,
+      "language": [ "af" ],
+      "author": [ "TheOneAuthor", "Elliu", "fooPseudo" ],
+      "tags": [
+        [ "Number", "1" ],
+        [ "VTuber", "" ],
+        [ "Composer", "WaouhSuperJ'adore" ]
+      ]
+    },
+    {
+      "id": 2,
+      "song_title": "Pre-Romance",
+      "source_name": "Natsu e no Tunnel, Sayonara no Deguchi",
+      "song_type": "MV",
+      "song_origin": "game",
+      "is_new": true,
+      "created_at": 1723986494,
+      "updated_at": 1723986500,
+      "epoch": 0,
+      "file_hash": "6f3df02ee1f000295deaab0fe89e6908ae1ca3a25a533bc99b3d59fa60caf497",
+      "nextFilepath": "",
+      "filesize": 9784315,
+      "virtual": false,
+      "language": [ "aa" ],
+      "author": [ "fooPseudo" ],
+      "tags": []
+    },
+    {
+      "id": 1,
+      "song_title": "Rap God ft. Momoi (46 Brian)",
+      "source_name": "Eminem",
+      "song_type": "MV",
+      "song_origin": "game",
+      "is_new": false,
+      "created_at": 1723986487,
+      "updated_at": 1723986496,
+      "epoch": 0,
+      "file_hash": "6b512e13388623a1aed0967a449320818d4eb4c91ffec20014cc0ee02f0fbd4f",
+      "nextFilepath": "",
+      "filesize": 5488980,
+      "virtual": false,
+      "language": [ "af" ],
+      "author": [ "fooPseudo" ],
+      "tags": []
+    },
+    {
+      "id": 3,
+      "song_title": "Every Second (Japanese Version)",
+      "source_name": "Hananoi-kun to Koi no Yamai",
+      "song_type": "ED",
+      "song_origin": "game",
+      "is_new": false,
+      "created_at": 1724143207,
+      "updated_at": 1724740456,
+      "epoch": 0,
+      "file_hash": "5c78cc58610da5ac5212e8d48a61eb2f8f5ff89c97e47372e0142d5f59de8039",
+      "nextFilepath": "",
+      "filesize": 8197649,
+      "virtual": false,
+      "language": [ "ak" ],
+      "author": [ "fooPseudo" ],
+      "tags": [
+        [ "Number", "1" ],
+        [ "VTuber", "" ]
+      ]
+    },
+    {
+      "id": 6,
+      "song_title": "Reversal",
+      "source_name": "Tsuki ga Michibiku Isekai Douchuu 2nd Season",
+      "song_type": "OP",
+      "song_origin": "game",
+      "is_new": false,
+      "created_at": 1725547250,
+      "updated_at": 1726757878,
+      "epoch": 0,
+      "file_hash": "19f0f6ab891c8a565f6483d6148782efd2d0e935b1e092951529cb59bcc35f34",
+      "nextFilepath": "",
+      "filesize": 13453030,
+      "virtual": false,
+      "language": [ "af" ],
+      "author": [ "fooPseudo" ],
+      "tags": [
+        [ "Number", "2" ]
+      ]
+    }
+  ]
+}
diff --git a/kurisu_api/tests/playlists.json b/kurisu_api/tests/playlists.json
new file mode 100644
index 00000000..46af1f8b
--- /dev/null
+++ b/kurisu_api/tests/playlists.json
@@ -0,0 +1,37 @@
+[
+  {
+    "id": 5,
+    "title": "Playlist 1",
+    "description": "This is an awesome playlist",
+    "owners": [ "fooPseudo" ],
+    "karas": [ 6, 9544, 5 ]
+  },
+  {
+    "id": 6,
+    "title": "Playlist 2",
+    "description": "This playlist is not yours",
+    "owners": [ "fooPseudo<wow>" ],
+    "karas": [ 9544, 5 ]
+  },
+  {
+    "id": 8,
+    "title": "Playlist with quite long name",
+    "description": "Short description",
+    "owners": [],
+    "karas": [ 6 ]
+  },
+  {
+    "id": 9,
+    "title": "Kinda The longest The longest The longest The longest The longest The longest The longest The longest The longest The longest",
+    "description": "aze",
+    "owners": [ "fooPseudo", "fooPseudo<wow>" ],
+    "karas": []
+  },
+  {
+    "id": 10,
+    "title": "aze",
+    "description": "aze",
+    "owners": [ "fooPseudo" ],
+    "karas": []
+  }
+]
diff --git a/kurisu_api/tests/sample.json b/kurisu_api/tests/sample.json
deleted file mode 100644
index 609d1bfb..00000000
--- a/kurisu_api/tests/sample.json
+++ /dev/null
@@ -1,323 +0,0 @@
-[
-  {
-    "id": 9022,
-    "song_title": "hA HA honk2",
-    "source_name": "senzawa",
-    "song_type": "OP",
-    "song_origin": "anime",
-    "language": [
-      "br",
-      "de"
-    ],
-    "author": [
-      "Elliu"
-    ],
-    "tags": [
-      [
-        "Version",
-        "Short"
-      ],
-      [
-        "Composer",
-        "aze"
-      ]
-    ],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-27 20:12:55",
-    "file_hash": "d1d2033d760e93158fc64426e8b7ec3535fbf92d41e48bc16fca2f5fd8b2a926",
-    "filesize": 1465654,
-    "epoch": 1,
-    "updated_at": 1677967139,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9026,
-    "song_title": "Chikatto Chika Chika",
-    "source_name": "Kaguya-sama wa Kokurasetai",
-    "song_type": "ED",
-    "song_origin": "vn",
-    "language": [
-      "ca",
-      "ja"
-    ],
-    "author": [
-      "Kubat"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-27 20:16:20",
-    "file_hash": "def21dad50c0d4d9c477591cd9de039a7ab3c0d65d52b390417edbbb0776052f",
-    "filesize": 17122564,
-    "epoch": 1,
-    "updated_at": 1683208523,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9027,
-    "song_title": "last sparkle",
-    "source_name": "Pop Team Epic Special",
-    "song_type": "OP",
-    "song_origin": "anime",
-    "language": [
-      "zh",
-      "br"
-    ],
-    "author": [
-      "Salixor"
-    ],
-    "tags": [
-      [
-        "Composer",
-        "aze"
-      ]
-    ],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-10 13:11:48",
-    "file_hash": "3da80457a071047896c833a0ddde64a82b524e78cf33e2de0a312160ad0e40c2",
-    "filesize": 16883894,
-    "epoch": 8,
-    "updated_at": 1683208648,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9028,
-    "song_title": "Piyo",
-    "source_name": "Higepiyo",
-    "song_type": "OP",
-    "song_origin": "anime",
-    "language": [
-      "en"
-    ],
-    "author": [
-      "Elliu"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-10 13:07:20",
-    "file_hash": "c285aa8b1bd3c21bf158b6bbb2c8798febc43ed9d707fb6fd79a074d5f359b41",
-    "filesize": 463172,
-    "epoch": 0,
-    "updated_at": 1683209107,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9029,
-    "song_title": "Maka went gao",
-    "source_name": "Soul Eater",
-    "song_type": "IS",
-    "song_origin": "game",
-    "language": [
-      "la"
-    ],
-    "author": [
-      "Deurstann"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": false,
-    "author_year": "2023-09-10 13:08:02",
-    "file_hash": "0a0baa8de3696955a695fdf816abb42539bceadbfb4c79cde7d43c081ff6d9c0",
-    "filesize": 196032,
-    "epoch": 0,
-    "updated_at": 1683209132,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9030,
-    "song_title": "Chijou no Senshi",
-    "source_name": "Sakura Taisen New York, New York",
-    "song_type": "OP",
-    "song_origin": "anime",
-    "language": [
-      "ja"
-    ],
-    "author": [
-      "Bisquette"
-    ],
-    "tags": [
-      [
-        "VTuber",
-        ""
-      ]
-    ],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-24 14:48:30",
-    "file_hash": "974583d04d8fe2485b64ec8de97785cc34536cd8a69166d3be2cd907148154d2",
-    "filesize": 11905649,
-    "epoch": 0,
-    "updated_at": 1683209134,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9031,
-    "song_title": "Hare Hare Yukai",
-    "source_name": "Suzumiya Haruhi no Yuuutsu",
-    "song_type": "ED",
-    "song_origin": "vn",
-    "language": [
-      "ja"
-    ],
-    "author": [
-      "Sting"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-10 13:08:05",
-    "file_hash": "99a280ae6fde376dc6f933e7ff86e377863bc18a2c775265eba387b217c660fb",
-    "filesize": 10130414,
-    "epoch": 0,
-    "updated_at": 1683209135,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9032,
-    "song_title": "All Alone With You",
-    "source_name": "Psycho-Pass",
-    "song_type": "ED",
-    "song_origin": "vn",
-    "language": [
-      "fr"
-    ],
-    "author": [
-      "Colgate"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-10 13:07:50",
-    "file_hash": "47a5585c5fe4d1e5ea85c80f3a01d465aa393a1dbbd1bd38f2021df90dd44fec",
-    "filesize": 10044203,
-    "epoch": 0,
-    "updated_at": 1683209136,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9033,
-    "song_title": "Mes rêves",
-    "source_name": "Pokémon",
-    "song_type": "OP",
-    "song_origin": "anime",
-    "language": [
-      "fr"
-    ],
-    "author": [
-      "Deurstann"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-10 13:07:24",
-    "file_hash": "4036f7f60c08bf70741f118eca95dff61facfb66199351e9e40900071cd5322d",
-    "filesize": 10247536,
-    "epoch": 0,
-    "updated_at": 1683209137,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9035,
-    "song_title": "Shiny tale",
-    "source_name": "Danshi Koukousei no Nichijou",
-    "song_type": "OP",
-    "song_origin": "anime",
-    "language": [
-      "fr"
-    ],
-    "author": [
-      "Sting"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-10 13:07:17",
-    "file_hash": "c6d19655f65777e8bcae8394289bd1ba5e986d31cd53725ea590a22e1c285419",
-    "filesize": 22148477,
-    "epoch": 0,
-    "updated_at": 1683209138,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9036,
-    "song_title": "Wind",
-    "source_name": "Naruto",
-    "song_type": "ED",
-    "song_origin": "vn",
-    "language": [
-      "ru"
-    ],
-    "author": [
-      "Deurstann"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-09 19:35:48",
-    "file_hash": "be4f6b70ef0c3fce3744d397e7c8a4292f1e029a2b82512832ece0f632641976",
-    "filesize": 6533611,
-    "epoch": 0,
-    "updated_at": 1683209138,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9037,
-    "song_title": "Get Wild",
-    "source_name": "City Hunter",
-    "song_type": "ED",
-    "song_origin": "vn",
-    "language": [
-      "ja"
-    ],
-    "author": [
-      "Deurstann"
-    ],
-    "tags": [],
-    "upload_comment": "",
-    "is_new": true,
-    "author_year": "2023-09-09 19:22:00",
-    "file_hash": "d6bad8ec540853d71669912e2731c197bdb150cf05f00c3d7fb2aaecd55b9a6e",
-    "filesize": 6857963,
-    "epoch": 0,
-    "updated_at": 1683209227,
-    "created_at": 1677967139,
-    "virtual": false
-  },
-  {
-    "id": 9041,
-    "song_title": "hA HA honk",
-    "source_name": "senzawa",
-    "song_type": "IS",
-    "song_origin": "game",
-    "language": [
-      "ja"
-    ],
-    "author": [
-      "Elliu"
-    ],
-    "tags": [],
-    "upload_comment": "oops reupload cause <>",
-    "is_new": true,
-    "author_year": "2023-09-10 12:38:27",
-    "file_hash": "86c2eb4fe6a78afb0a3c6d5e769154fdf4e00cf4a6e0e0ff5bb2ee74d5f3a750",
-    "filesize": 1465654,
-    "epoch": 0,
-    "updated_at": 1694290141,
-    "created_at": 1677967139,
-    "virtual": false
-  }
-]
diff --git a/kurisu_api/tests/v2.rs b/kurisu_api/tests/v2.rs
index 88fcbffa..1fc0c141 100644
--- a/kurisu_api/tests/v2.rs
+++ b/kurisu_api/tests/v2.rs
@@ -3,8 +3,7 @@ use lektor_utils::{assert_err, assert_ok};
 
 #[test]
 fn test_kurisu_v2_dbinfos() {
-    const SAMPLE: &str = include_str!("dbinfo.json");
-    assert_ok!(serde_json::from_str::<Infos>(SAMPLE));
+    assert_ok!(serde_json::from_str::<Infos>(include_str!("dbinfo.json")));
 }
 
 #[test]
@@ -43,6 +42,19 @@ fn test_kurisu_v2_simple_enums() {
 
 #[test]
 fn test_kurisu_v2_get_karas() {
-    const SAMPLE: &str = include_str!("sample.json");
-    assert_ok!(serde_json::from_str::<Vec<Kara>>(SAMPLE));
+    assert_ok!(serde_json::from_str::<Karas>(include_str!("karas.json")));
+}
+
+#[test]
+fn test_kurisu_v2_get_favorites() {
+    assert_ok!(serde_json::from_str::<Favorites>(include_str!(
+        "favorites.json"
+    )));
+}
+
+#[test]
+fn test_kurisu_v2_get_playlists() {
+    assert_ok!(serde_json::from_str::<Vec<Playlist>>(include_str!(
+        "playlists.json"
+    )));
 }
diff --git a/lektor_lib/Cargo.toml b/lektor_lib/Cargo.toml
index c6f37da0..e882fd53 100644
--- a/lektor_lib/Cargo.toml
+++ b/lektor_lib/Cargo.toml
@@ -8,17 +8,14 @@ rust-version.workspace = true
 description = "Client library for lektord, used to factorize the code between lkt and amadeus"
 
 [dependencies]
-serde.workspace = true
+serde.workspace      = true
 serde_json.workspace = true
 
-log.workspace = true
-url.workspace = true
-anyhow.workspace = true
-
+log.workspace     = true
+url.workspace     = true
+anyhow.workspace  = true
 reqwest.workspace = true
-
 futures.workspace = true
-async-trait.workspace = true
 
-lektor_utils = { path = "../lektor_utils" }
+lektor_utils    = { path = "../lektor_utils" }
 lektor_payloads = { path = "../lektor_payloads" }
diff --git a/lektor_lib/src/requests.rs b/lektor_lib/src/requests.rs
index 719f9262..fc66474a 100644
--- a/lektor_lib/src/requests.rs
+++ b/lektor_lib/src/requests.rs
@@ -1,5 +1,9 @@
 use crate::ConnectConfig;
 use anyhow::{bail, Context, Result};
+use futures::{
+    stream::{self, FuturesUnordered},
+    StreamExt,
+};
 use lektor_payloads::*;
 use lektor_utils::{encode_base64, encode_base64_value};
 use reqwest::{
@@ -66,14 +70,23 @@ pub async fn get_status(config: impl AsRef<ConnectConfig>) -> Result<PlayStateWi
     request!(config; GET @ "/playback/state" => PlayStateWithCurrent)
 }
 
-pub async fn get_kara_by_kid(config: impl AsRef<ConnectConfig>, kid: KId) -> Result<Kara> {
-    let (len, kid) = encode_base64(kid.as_str())?;
-    let kid = std::str::from_utf8(&kid[..len])?;
-    request!(config; GET @ "/get/kid/{kid}" => Kara)
+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())
 }
 
-pub async fn get_kara_by_id(config: impl AsRef<ConnectConfig>, id: u64) -> Result<Kara> {
-    request!(config; GET @ "/get/id/{id}" => Kara)
+pub async fn get_kara_by_kid(config: impl AsRef<ConnectConfig>, kid: KId) -> Result<Kara> {
+    log::info!("try to get kara {kid}");
+    request!(config; GET @ "/get/kid/{}", encode_base64(kid.to_string())? => Kara)
 }
 
 pub async fn toggle_playback_state(config: impl AsRef<ConnectConfig>) -> Result<()> {
@@ -132,33 +145,25 @@ pub async fn remove_level_from_queue(
     config: impl AsRef<ConnectConfig>,
     level: Priority,
 ) -> Result<()> {
-    request!(config; DELETE @ "/queue/{level}")
+    request!(config; DELETE @ "/queue/level/{level}")
 }
 
 pub async fn remove_range_from_queue(
     config: impl AsRef<ConnectConfig>,
     range: impl Into<Range>,
 ) -> Result<()> {
-    let (len, range) = encode_base64(format!("{}", range.into()))?;
-    let range = std::str::from_utf8(&range[..len])?;
-    request!(config; DELETE @ "/queue?range={range}")
+    request!(config; DELETE @ "/queue?range={}", encode_base64(format!("{}", range.into()))?)
 }
 
 pub async fn shuffle_level_queue(config: impl AsRef<ConnectConfig>, prio: Priority) -> Result<()> {
-    request!(config; PUT @ "/queue/{prio}")
-}
-
-pub async fn shuffle_queue(config: impl AsRef<ConnectConfig>) -> Result<()> {
-    shuffle_queue_range(config, ..).await
+    request!(config; PUT @ "/queue/level/{prio}")
 }
 
 pub async fn shuffle_queue_range(
     config: impl AsRef<ConnectConfig>,
     range: impl Into<Range>,
 ) -> Result<()> {
-    let (len, range) = encode_base64(format!("{}", range.into()))?;
-    let range = std::str::from_utf8(&range[..len])?;
-    request!(config; POST(QueueUpdateAction::Shuffle) @ "/queue?range={range}")
+    request!(config; POST(QueueUpdateAction::Shuffle) @ "/queue?range={}", encode_base64(format!("{}", range.into()))?)
 }
 
 // ================================================== //
@@ -169,9 +174,7 @@ pub async fn remove_range_from_history(
     config: impl AsRef<ConnectConfig>,
     range: impl Into<Range>,
 ) -> Result<()> {
-    let (len, range) = encode_base64(format!("{}", range.into()))?;
-    let range = std::str::from_utf8(&range[..len])?;
-    request!(config; DELETE @ "/history?range={range}")
+    request!(config; DELETE @ "/history?range={}", encode_base64(format!("{}", range.into()))?)
 }
 
 // ================================================== //
@@ -183,10 +186,7 @@ pub async fn remove_kid_from_playlist(
     name: Arc<str>,
     id: KId,
 ) -> Result<()> {
-    let action = PlaylistUpdateAction::Remove(KaraFilter::KId(id));
-    let (len, name) = encode_base64(name)?;
-    let name = std::str::from_utf8(&name[..len])?;
-    request!(config; PATCH(action) @ "/playlist/{name}")
+    request!(config; PATCH(PlaylistUpdateAction::Remove(KaraFilter::KId(id))) @ "/playlist/{}", encode_base64(name)?)
 }
 
 pub async fn add_to_playlist(
@@ -194,9 +194,7 @@ pub async fn add_to_playlist(
     name: Arc<str>,
     what: KaraFilter,
 ) -> Result<()> {
-    let (len, name) = encode_base64(name)?;
-    let name = std::str::from_utf8(&name[..len])?;
-    request!(config; PATCH(PlaylistUpdateAction::Add(what)) @ "/playlist/{name}")
+    request!(config; PATCH(PlaylistUpdateAction::Add(what)) @ "/playlist/{}", encode_base64(name)?)
 }
 
 pub async fn remove_from_playlist(
@@ -204,41 +202,34 @@ pub async fn remove_from_playlist(
     name: Arc<str>,
     what: KaraFilter,
 ) -> Result<()> {
-    let (len, name) = encode_base64(name)?;
-    let name = std::str::from_utf8(&name[..len])?;
-    request!(config; PATCH(PlaylistUpdateAction::Remove(what)) @ "/playlist/{name}")
+    request!(config; PATCH(PlaylistUpdateAction::Remove(what)) @ "/playlist/{}", encode_base64(name)?)
 }
 
 pub async fn create_playlist(config: impl AsRef<ConnectConfig>, name: Arc<str>) -> Result<()> {
-    let (len, name) = encode_base64(name)?;
-    let name = std::str::from_utf8(&name[..len])?;
-    request!(config; PUT @ "/playlist/{name}")
+    request!(config; PUT @ "/playlist/{}", encode_base64(name)?)
 }
 
 pub async fn delete_playlist(config: impl AsRef<ConnectConfig>, name: Arc<str>) -> Result<()> {
-    let (len, name) = encode_base64(name)?;
-    let name = std::str::from_utf8(&name[..len])?;
-    request!(config; DELETE @ "/playlist/{name}")
+    request!(config; DELETE @ "/playlist/{}", encode_base64(name)?)
 }
 
 // ================================================== //
 //                  Search functions                  //
 // ================================================== //
 
-pub async fn get_queue(config: impl AsRef<ConnectConfig>) -> Result<Vec<(Priority, KId)>> {
-    get_queue_range(config, ..).await
-}
-
-pub async fn get_history(config: impl AsRef<ConnectConfig>) -> Result<Vec<KId>> {
-    get_history_range(config, ..).await
-}
-
 pub async fn get_queue_count(
     config: impl AsRef<ConnectConfig>,
 ) -> Result<[usize; PRIORITY_LENGTH]> {
     request!(config; GET @ "/queue/count" => [usize; PRIORITY_LENGTH])
 }
 
+pub async fn get_queue_level(
+    config: impl AsRef<ConnectConfig>,
+    prio: Priority,
+) -> Result<Vec<KId>> {
+    request!(config; GET @ "/queue/level/{prio}" => Vec<KId>)
+}
+
 pub async fn get_history_count(config: impl AsRef<ConnectConfig>) -> Result<usize> {
     request!(config; GET @ "/history/count" => usize)
 }
@@ -247,44 +238,41 @@ pub async fn get_queue_range(
     config: impl AsRef<ConnectConfig>,
     range: impl Into<Range>,
 ) -> Result<Vec<(Priority, KId)>> {
-    let (len, range) = encode_base64(format!("{}", range.into()))?;
-    let range = std::str::from_utf8(&range[..len])?;
-    request!(config; GET @ "/queue?range={range}" => Vec<(Priority, KId)>)
+    request!(config; GET @ "/queue?range={}", encode_base64(range.into().to_string())? => Vec<(Priority, KId)>)
 }
 
 pub async fn get_history_range(
     config: impl AsRef<ConnectConfig>,
     range: impl Into<Range>,
 ) -> Result<Vec<KId>> {
-    let (len, range) = encode_base64(format!("{}", range.into()))?;
-    let range = std::str::from_utf8(&range[..len])?;
-    request!(config; GET @ "/history?range={range}" => Vec<KId>)
+    request!(config; GET @ "/history?range={}", encode_base64(range.into().to_string())? => Vec<KId>)
 }
 
-pub async fn get_playlists(
-    config: impl AsRef<ConnectConfig>,
-) -> Result<Vec<(PlaylistName, PlaylistInfo)>> {
-    request!(config; GET @ "/playlist" => Vec<(PlaylistName, PlaylistInfo)>)
+pub async fn get_playlists(config: impl AsRef<ConnectConfig>) -> Result<Vec<(KId, String)>> {
+    request!(config; GET @ "/playlists" => Vec<(KId, String)>)
 }
 
-pub async fn get_playlist_content(
-    config: impl AsRef<ConnectConfig>,
-    name: Arc<str>,
-) -> Result<Vec<KId>> {
-    let (len, name) = encode_base64(name)?;
-    let name = std::str::from_utf8(&name[..len])?;
-    request!(config; GET @ "/playlist/{name}" => Vec<KId>)
+pub async fn get_playlist(config: impl AsRef<ConnectConfig>, id: KId) -> Result<Playlist> {
+    request!(config; GET @ "/playlist/{id}" => Playlist)
+}
+
+pub async fn get_playlist_info(config: impl AsRef<ConnectConfig>, id: KId) -> Result<PlaylistInfo> {
+    request!(config; GET @ "/playlist/{id}/info" => PlaylistInfo)
+}
+
+pub async fn get_playlist_content(config: impl AsRef<ConnectConfig>, id: KId) -> Result<Vec<KId>> {
+    request!(config; GET @ "/playlist/{id}/content" => Vec<KId>)
 }
 
 pub async fn search_karas(
     config: impl AsRef<ConnectConfig>,
     from: SearchFrom,
-    by: KaraBy,
+    by: impl IntoIterator<Item = KaraBy>,
 ) -> Result<Vec<KId>> {
-    let regex = vec![by];
-    let (len, obj) = encode_base64_value(SearchData { from, regex })?;
-    let get = std::str::from_utf8(&obj[..len])?;
-    request!(config; GET @ "/search/{get}" => Vec<KId>)
+    request!(config;
+        GET @ "/search/{}", encode_base64_value(SearchData { from, regex: by.into_iter().collect() })?
+        => Vec<KId>
+    )
 }
 
 pub async fn count_karas(
@@ -292,8 +280,8 @@ pub async fn count_karas(
     from: SearchFrom,
     by: KaraBy,
 ) -> Result<usize> {
-    let regex = vec![by];
-    let (len, obj) = encode_base64_value(SearchData { from, regex })?;
-    let get = std::str::from_utf8(&obj[..len])?;
-    request!(config; GET @ "/count/{get}" => usize)
+    request!(config;
+        GET @ "/count/{}", encode_base64_value(SearchData { from, regex: vec![by] })?
+        => usize
+    )
 }
diff --git a/lektor_nkdb/Cargo.toml b/lektor_nkdb/Cargo.toml
index 4e18c45e..e7eaef27 100644
--- a/lektor_nkdb/Cargo.toml
+++ b/lektor_nkdb/Cargo.toml
@@ -1,30 +1,28 @@
 [package]
 name = "lektor_nkdb"
+description = "New database implementation for lektord (New Kara DataBase)"
+rust-version.workspace = true
+
 version.workspace = true
 edition.workspace = true
 authors.workspace = true
 license.workspace = true
-rust-version.workspace = true
-description = "New database implementation for lektord (New Kara DataBase)"
 
 [dependencies]
-serde.workspace = true
-serde_json.workspace = true
-
-log.workspace = true
-url.workspace = true
-rand.workspace = true
-regex.workspace = true
-chrono.workspace = true
-sha256.workspace = true
-anyhow.workspace = true
-hashbrown.workspace = true
-
-tokio.workspace = true
-futures.workspace = true
-async-trait.workspace = true
+serde.workspace        = true
+serde_json.workspace   = true
+log.workspace          = true
+url.workspace          = true
+rand.workspace         = true
+regex.workspace        = true
+chrono.workspace       = true
+sha256.workspace       = true
+anyhow.workspace       = true
+hashbrown.workspace    = true
+derive_more.workspace  = true
+tokio.workspace        = true
+futures.workspace      = true
 tokio-stream.workspace = true
-
-kurisu_api = { path = "../kurisu_api" }
-lektor_utils = { path = "../lektor_utils" }
-lektor_procmacros = { path = "../lektor_procmacros" }
+kurisu_api             = { path = "../kurisu_api" }
+lektor_utils           = { path = "../lektor_utils" }
+lektor_procmacros      = { path = "../lektor_procmacros" }
diff --git a/lektor_nkdb/src/database/epoch.rs b/lektor_nkdb/src/database/epoch.rs
index 36c63d2b..059a569e 100644
--- a/lektor_nkdb/src/database/epoch.rs
+++ b/lektor_nkdb/src/database/epoch.rs
@@ -6,7 +6,7 @@ use crate::*;
 /// The epoch contains all available karas at a certain point in time. It can be submitted and
 /// available to all readers of the database or unsubmited and only one writter can edit it.
 #[derive(Debug, Default)]
-pub(crate) struct Epoch(EpochData, u64);
+pub struct Epoch(EpochData, u64);
 
 /// Represent the data contained in an epoch.
 pub type EpochData = HashMap<KId, Kara>;
@@ -19,15 +19,8 @@ impl From<(EpochData, u64)> for Epoch {
 
 impl Epoch {
     /// Get all the tuples (KId, RemoteKId) for the kara present in the epoch.
-    pub fn ids(&self) -> impl Iterator<Item = (&KId, &RemoteKId)> + '_ {
-        self.0.values().map(|kara| (&kara.id, &kara.remote))
-    }
-
-    /// Get a [Kara] by its local id [u64]. The [KId] is the unique id [u64] for this epoch plus
-    /// the path from the data folder. A single [u64] can have two [KId] representations if a kara
-    /// was changed in an epoch and that epoch is loaded in memory.
-    pub fn get_kara_by_u64(&self, id: u64) -> Option<&Kara> {
-        self.0.values().find(|kara| id.eq(&kara.id.local_id()))
+    pub fn ids(&self) -> impl Iterator<Item = (KId, RemoteKId)> + '_ {
+        self.0.values().map(|kara| (kara.id, kara.remote.clone()))
     }
 
     /// Get the list of [Kara] corresponding to the [KId]. If we failed to find one kara we log the
@@ -45,11 +38,6 @@ impl Epoch {
         self.0.get(&id)
     }
 
-    /// Returns whever the [Epoch] contains the [KId] or not.
-    pub fn contains(&self, id: &KId) -> bool {
-        self.0.contains_key(id)
-    }
-
     /// Get access to the karas in the epoch.
     pub fn data(&self) -> &EpochData {
         &self.0
@@ -61,7 +49,7 @@ impl Epoch {
     }
 
     /// Get the number of the epoch
-    pub fn epoch_num(&self) -> u64 {
+    pub fn num(&self) -> u64 {
         self.1
     }
 }
diff --git a/lektor_nkdb/src/database/mod.rs b/lektor_nkdb/src/database/mod.rs
index aa7598d8..357442d3 100644
--- a/lektor_nkdb/src/database/mod.rs
+++ b/lektor_nkdb/src/database/mod.rs
@@ -1,15 +1,7 @@
 //! Base definition of how we store kara in different epochs. Even if it's called "database", it's
 //! only a small, but fundational, part of the database system.
 
-mod epoch;
-mod kara;
-mod pool;
-mod update;
-
-pub use kara::*;
-pub use kurisu_api::v2::{SongOrigin, SongType};
-pub use pool::{KId, RemoteKId};
-pub use update::UpdateHandler;
-
-pub(crate) use epoch::*;
-pub(crate) use pool::Pool;
+pub(crate) mod epoch;
+pub(crate) mod kara;
+pub(crate) mod pool;
+pub(crate) mod update;
diff --git a/lektor_nkdb/src/database/pool.rs b/lektor_nkdb/src/database/pool.rs
index 332db3f2..5af0585d 100644
--- a/lektor_nkdb/src/database/pool.rs
+++ b/lektor_nkdb/src/database/pool.rs
@@ -4,109 +4,65 @@
 //! representation for searching plus some mapping. A pool is common for all the epochs. For epoch
 //! specific things like the [u64] to [Kid] / [Kara] mappings, do that in the epoch struct.
 
-use crate::{EpochData, Playlist};
-use hashbrown::HashMap;
-use serde::{Deserialize, Serialize};
-use std::{collections::hash_map::DefaultHasher, hash::Hasher, sync::Arc};
+use crate::{EpochData, KId, RemoteKId};
+use derive_more::Into;
+use hashbrown::{HashMap, HashSet};
+use std::sync::{
+    atomic::{AtomicU64, Ordering},
+    Arc,
+};
 use tokio::sync::RwLock;
 
-/// A pool of all the available kara for every epochs
+/// A pool of all the available kara for every epochs. Contains the mapping for remote id to the
+/// local id representations.
 #[derive(Debug, Default)]
 pub(crate) struct Pool {
-    /// Cache for strings, to reduce memory load.
-    string_cache: RwLock<HashMap<u64, Arc<str>>>,
+    /// ID mapping from local to remote. Note that there is a one-to-one relation between
+    /// [RemoteKId] and [KId], even with metadata updates or file update.
+    id_mapping: RwLock<HashMap<RemoteKId, KId>>,
 
-    /// The mapping for remote id <-> local id
-    id_mapping: RwLock<Vec<(KId, RemoteKId)>>,
+    /// The next local ID.
+    next_id: AtomicU64,
+
+    /// Cache strings for authors and languages, and tags.
+    strings: RwLock<HashSet<Arc<str>>>,
 }
 
 impl Pool {
     /// Create the pool storage from an iterator of id/remote_id.
     pub(crate) async fn from_iter<T: IntoIterator<Item = (KId, RemoteKId)>>(iter: T) -> Self {
-        // Compress things if needed!
-        let this = Self::default();
-        let vec = futures::future::join_all(iter.into_iter().map(|(kid, rkid)| async {
-            let kid = this.get_str::<KId>(kid.0).await;
-            let rkid = this.get_str::<RemoteKId>(rkid.0).await;
-            (kid, rkid)
-        }))
-        .await;
-        let _ = std::mem::replace(&mut (*this.id_mapping.write().await), vec);
-        this
+        let id_mapping = iter
+            .into_iter()
+            .map(|(kid, rkid)| (rkid, kid))
+            .collect::<HashMap<_, _>>();
+        Self {
+            next_id: AtomicU64::new(
+                (id_mapping.values())
+                    .map(|KId(id)| *id + 1)
+                    .max()
+                    .unwrap_or(1),
+            ),
+            id_mapping: RwLock::new(id_mapping),
+            ..Default::default()
+        }
     }
 
-    /// Get other possible ids from an id, we may want to do that to get all the local ids for a
-    /// remote id, to get a more up-to-date version of a kara.
-    pub(crate) async fn get_other_locals(&self, id: KId) -> Vec<KId> {
-        let mapping = self.id_mapping.read().await;
-
-        let find_remote = |(kid, rkid): &_| id.eq(kid).then_some(rkid).cloned();
-        let remote = mapping.iter().find_map(find_remote).expect("no remote id");
-
-        let find_locals = |(kid, rkid): &(KId, _)| remote.eq(rkid).then_some(kid.clone());
-        mapping.iter().filter_map(find_locals).collect()
+    /// Get a new and unique [KId]
+    pub(crate) fn next_kid(&self) -> KId {
+        KId(self.next_id.fetch_add(1, Ordering::AcqRel))
     }
 
     /// Get the tuple (local, remote) from the string representation of the remote id. Id the local
     /// id is not present, returns none, else some(local_id).
     pub(crate) async fn get_from_remote(&self, rkid: impl AsRef<str>) -> (Option<KId>, RemoteKId) {
-        let rkid = self.get_str::<RemoteKId>(rkid).await;
-        let id = (self.id_mapping.read().await.iter())
-            .find_map(|(id, remote)| remote.eq(&rkid).then_some(id))
-            .cloned();
-        (id, rkid)
-    }
-
-    /// Get a pointer to the string by its value. Used to reduce the amount of memory allocated for
-    /// string representation. The heuristic is that we can share the song's origins and the tag
-    /// keys and values. This is the sync version where we don't need async because we have a mut
-    /// reference to the pool (thus exclusive).
-    fn get_str_sync<I: sealed::Id>(&mut self, id: Arc<str>) -> I {
-        let mut hash = DefaultHasher::new();
-        hash.write(id.as_ref().as_bytes());
-        (self.string_cache.get_mut())
-            .entry(hash.finish())
-            .or_insert(id)
-            .clone()
-            .into()
-    }
-
-    /// Get a pointer to the string by its value. Used to reduce the amount of memory allocated for
-    /// string representation. The heuristic is that we can share the song's origins and the tag
-    /// keys and values.
-    pub(crate) async fn get_str<I: sealed::Id>(&self, id: impl AsRef<str>) -> I {
-        let mut hash = DefaultHasher::new();
-        hash.write(id.as_ref().as_bytes());
-        let mut this = self.string_cache.write().await;
-        this.entry(hash.finish())
-            .or_insert_with(|| Arc::<str>::from(id.as_ref()))
-            .clone()
-            .into()
-    }
-
-    /// Same as [Pool::get_str], but don't create the id if not present to avoid being DOSed...
-    pub(crate) async fn try_get_str<I: sealed::Id>(&self, id: impl AsRef<str>) -> Option<I> {
-        let mut hash = DefaultHasher::new();
-        hash.write(id.as_ref().as_bytes());
-        let this = self.string_cache.write().await;
-        this.get(&hash.finish()).cloned().map(Into::into)
-    }
-
-    /// Get the maximal id present in the pool.
-    pub(crate) async fn maximal_id(&self) -> u64 {
-        let list = self.id_mapping.read().await;
-        list.iter()
-            .map(|(id, _)| id.local_id())
-            .max()
-            .unwrap_or_default()
+        let id = self.id_mapping.read().await.get(rkid.as_ref()).copied();
+        (id, RemoteKId(rkid.as_ref().into()))
     }
 
     /// Fatcorize the [Arc<str>] for an [EpochData] to have pointer equality. We do that at the
     /// loading time, thus we can have a mut referenceto the pool and skip some locks.
     pub(crate) fn factorize_epoch_data(&mut self, data: &mut EpochData) {
         let content = std::mem::take(data).into_iter().map(|(kid, mut kara)| {
-            kara.id = self.get_str_sync(kara.id.0);
-            kara.remote = self.get_str_sync(kara.remote.0);
             kara.language = kara
                 .language
                 .into_iter()
@@ -122,188 +78,20 @@ impl Pool {
                 let values = values.into_iter().map(|v| self.get_str_sync(v));
                 (key, values.collect())
             }));
-            (self.get_str_sync(kid.0), kara)
+            (kid, kara)
         });
         let _ = std::mem::replace(data, EpochData::from_iter(content));
     }
 
-    /// Fatcorize the [Arc<str>] for a [Playlist] to have pointer equality. We do that at the
-    /// loading time, thus we can have a mut referenceto the pool and skip some locks.
-    pub(crate) fn factorize_playlist(&mut self, plt: &mut Playlist) {
-        let content = std::mem::take(&mut plt.content)
-            .into_iter()
-            .map(|kid| self.get_str_sync(kid.0));
-        let _ = std::mem::replace(&mut plt.content, content.collect());
-    }
-}
-
-/// The string must respect the following format, you can assert this:
-///     "local_id:path"
-/// Note that the path can't contains slashes!
-#[derive(Serialize, Deserialize, Clone)]
-pub struct KId(Arc<str>);
-
-impl KId {
-    /// Create a raw string representation of the [KId], unsafe operation.
-    ///
-    /// ### Safety
-    /// Don't use this function appart from reusing it in the pool to get the pointer from the
-    /// string cache.
-    pub(super) unsafe fn new_raw(id: u64, path: impl std::fmt::Display) -> String {
-        format!("{id}:{path}")
-    }
-
-    /// Split the id in its different parts
-    fn split(&self) -> (&str, &str) {
-        self.0.split_once(':').expect("invalid kid found")
-    }
-
-    /// Get the local path of a kara. The path is relative to the $prefix from the database
-    /// structure.
-    pub fn path(&self) -> &str {
-        self.split().1
-    }
-
-    /// Get the numeric local id of a kara.
-    pub fn local_id(&self) -> u64 {
-        self.split()
-            .0
-            .parse::<u64>()
-            .expect("invalid id part in kid found")
-    }
-
-    /// Get the string representaiton of the [KId].
-    pub fn as_str(&self) -> &str {
-        self.0.as_ref()
-    }
-}
-
-impl From<Arc<str>> for KId {
-    fn from(value: Arc<str>) -> Self {
-        Self(value)
-    }
-}
-
-/// The string must respect the following format, you can assert this:
-///     "remote_id@remote_name"
-/// Note that the path can't contains slashes!
-#[derive(Serialize, Deserialize, Clone)]
-pub struct RemoteKId(Arc<str>);
-
-impl RemoteKId {
-    /// Create a raw string representation of the [RemoteKId], unsafe operation.
-    ///
-    /// ### Safety
-    /// Don't use this function appart from reusing it in the pool to get the pointer from the
-    /// string cache.
-    pub(super) unsafe fn new_raw(remote_id: u64, remote_name: impl std::fmt::Display) -> String {
-        format!("{remote_id}@{remote_name}")
-    }
-
-    /// Split the id in its different parts
-    fn split(&self) -> (&str, &str) {
-        self.0.split_once('@').expect("invalid kid found")
-    }
-
-    /// Get the name of the remote server
-    pub fn remote_name(&self) -> &str {
-        self.split().1
-    }
-
-    /// Get the id of the kara in the remote server
-    pub fn remote_id(&self) -> u64 {
-        self.split()
-            .0
-            .parse::<u64>()
-            .expect("invalid id part in kid found")
-    }
-
-    /// Get the string representaiton of the [RemoteKId].
-    pub fn as_str(&self) -> &str {
-        self.0.as_ref()
-    }
-}
-
-impl From<Arc<str>> for RemoteKId {
-    fn from(value: Arc<str>) -> Self {
-        Self(value)
-    }
-}
-
-/// Protect the types that we cache to do pointer comparaison instead of string comparaisons.
-mod sealed {
-    /// We can create the said id ([super::KId] or [super::RemoteKId] only) from the pointer to the
-    /// character slice.
-    pub trait Id: From<std::sync::Arc<str>> {}
-    impl Id for std::sync::Arc<str> {}
-    impl Id for super::KId {}
-    impl Id for super::RemoteKId {}
-}
-
-// We implement equality and partial equality with pointer equals, we take into account that the
-// strings where created correctly. With this trick we can implement hash just by hashing the
-// address of the pointer, not the underlying string.
-
-impl Eq for KId {}
-impl PartialEq for KId {
-    fn eq(&self, other: &Self) -> bool {
-        Arc::ptr_eq(&self.0, &other.0)
-    }
-}
-
-impl Eq for RemoteKId {}
-impl PartialEq for RemoteKId {
-    fn eq(&self, other: &Self) -> bool {
-        Arc::ptr_eq(&self.0, &other.0)
-    }
-}
-
-impl std::hash::Hash for KId {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        std::ptr::hash(Arc::as_ptr(&self.0), state)
-    }
-}
-
-impl std::hash::Hash for RemoteKId {
-    fn hash<H: Hasher>(&self, state: &mut H) {
-        std::ptr::hash(Arc::as_ptr(&self.0), state)
-    }
-}
-
-// We need to display the ids for urls...
-
-impl std::fmt::Display for KId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.0.as_ref())
-    }
-}
-
-impl std::fmt::Display for RemoteKId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.0.as_ref())
-    }
-}
-
-impl std::fmt::Debug for KId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(&self.0)
-    }
-}
-
-impl std::fmt::Debug for RemoteKId {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(&self.0)
-    }
-}
-
-impl AsRef<str> for KId {
-    fn as_ref(&self) -> &str {
-        self.as_str()
+    pub(crate) async fn get_str(&self, str: impl AsRef<str>) -> Arc<str> {
+        (self.strings.write().await)
+            .get_or_insert_with(str.as_ref(), |str| Arc::from(str))
+            .clone()
     }
-}
 
-impl AsRef<str> for RemoteKId {
-    fn as_ref(&self) -> &str {
-        self.as_str()
+    fn get_str_sync(&mut self, str: impl AsRef<str>) -> Arc<str> {
+        (self.strings.get_mut())
+            .get_or_insert_with(str.as_ref(), |str| Arc::from(str))
+            .clone()
     }
 }
diff --git a/lektor_nkdb/src/database/update.rs b/lektor_nkdb/src/database/update.rs
index d5bdcb64..80eafe08 100644
--- a/lektor_nkdb/src/database/update.rs
+++ b/lektor_nkdb/src/database/update.rs
@@ -1,16 +1,11 @@
 //! The update handle is the only way to write into the database at runtime.
 
-use crate::{database::*, storage::*};
+use crate::*;
 use anyhow::Result;
 use futures::future::join_all;
 use hashbrown::HashMap;
 use kurisu_api::SHA256;
-use lektor_utils::pushvec::*;
-use std::cell::RefCell;
-use std::sync::{
-    atomic::{AtomicU64, Ordering},
-    Arc,
-};
+use std::{cell::RefCell, sync::Arc};
 
 /// A pool handle. Used to add new karas to the pool. The update logic follows the following:
 /// - if a kara was not present, we add it and we download the file.
@@ -34,7 +29,6 @@ pub struct UpdateHandler<'a, Storage: DatabaseStorage> {
     storage: &'a Storage,
     new_epoch: RefCell<PushVecMutNode<Epoch>>,
     last_epoch: Option<&'a Epoch>,
-    last_kid: AtomicU64,
 }
 
 impl<'a, Storage: DatabaseStorage> UpdateHandler<'a, Storage> {
@@ -50,7 +44,6 @@ impl<'a, Storage: DatabaseStorage> UpdateHandler<'a, Storage> {
             storage,
             new_epoch: RefCell::new(node),
             last_epoch: last,
-            last_kid: AtomicU64::new(pool.maximal_id().await + 1),
         }
     }
 
@@ -78,12 +71,9 @@ impl<'a, Storage: DatabaseStorage> UpdateHandler<'a, Storage> {
         self.storage.submit_kara(file, id, hash).await
     }
 
-    /// Add a kara. Can error. On success, if the kara needs to be downloaded returns the couple of
-    /// IDs with the hash of the file.
-    ///
-    /// NOTE: if the file changed, then it's hash will be different, then its local [KId] will also
-    ///       will be different, thus we will download automatically and not re-add the old thing.
-    async fn add_kara<'b>(&'b self, kara: Kara) -> Result<Option<(KId, RemoteKId, SHA256)>>
+    /// Add a kara. On success, if the kara needs to be downloaded returns the couple of IDs with
+    /// the hash of the file.
+    async fn add_kara<'b>(&'b self, kara: Kara) -> Option<(KId, RemoteKId, SHA256)>
     where
         'b: 'a,
     {
@@ -94,93 +84,93 @@ impl<'a, Storage: DatabaseStorage> UpdateHandler<'a, Storage> {
                 .borrow_mut()
                 .content()
                 .data_mut()
-                .insert(kara.id.clone(), kara.clone());
-            Ok(None)
+                .insert(kara.id, kara.clone());
+            None
         };
         let doit = |kara: Kara, new_epoch: &'a RefCell<PushVecMutNode<Epoch>>| async {
             let KaraStatus::Physical { hash, .. } = kara.kara_status else {
                 log::warn!("tried to download a virtual kara: {kara}");
                 return reuse(kara, new_epoch);
             };
-            let (kid, rkid) = (kara.id.clone(), kara.remote.clone());
+            let (kid, rkid) = (kara.id, kara.remote.clone());
             log::debug!("need to download kid {kid} / remote_kid {rkid}");
             new_epoch
                 .borrow_mut()
                 .content()
                 .data_mut()
-                .insert(kara.id.clone(), kara);
-            Ok(Some((kid, rkid, hash)))
+                .insert(kara.id, kara);
+            Some((kid, rkid, hash))
         };
 
         match self.last_epoch {
             None => doit(kara, &self.new_epoch).await, // No way the kara was here
             Some(last_epoch) => match last_epoch.data().get(&kara.id) {
-                Some(old_kara) if !kara.same_file_as(old_kara) => {
-                    doit(kara, &self.new_epoch).await // The file has changed
-                }
-
                 // Not present last time
                 None => doit(kara, &self.new_epoch).await,
 
+                // Present last time, but the file has changed. The KId is reused.
+                Some(old_kara) if !kara.same_file_as(old_kara) => doit(kara, &self.new_epoch).await,
+
                 // Present last time, but we use the new built kara because some informations might
-                // have been updated even if the epoch was not incremented.
+                // have been updated even if the epoch was not incremented. Note that the KId is
+                // reused here.
                 Some(_) => reuse(kara, &self.new_epoch),
             },
         }
     }
 
-    /// Add a kara from the V2 API. Can error. On success, if the kara needs to be downloaded
-    /// returns the couple of IDs with the sha256 hash of the file to download.
+    /// Add a kara from the V2 API. On success, if the kara needs to be downloaded returns the
+    /// couple of IDs with the sha256 hash of the file to download.
     pub async fn add_kara_v2<'b>(
         &'b self,
         repo: &str,
         kara: kurisu_api::v2::Kara,
-    ) -> Result<Option<(KId, RemoteKId, SHA256)>>
+    ) -> Option<(KId, RemoteKId, SHA256)>
     where
         'b: 'a,
     {
         // Convert data from V2 API. Create a local ID if needed.
-        let (id, rkid) = (self.pool)
-            .get_from_remote(unsafe { RemoteKId::new_raw(kara.id, repo) })
+        let (id, remote) = (self.pool)
+            .get_from_remote(RemoteKId::new(kara.id, repo))
             .await;
-        let id = match id {
-            Some(id) => id,
-            None => unsafe {
-                self.pool
-                    .get_str::<KId>(KId::new_raw(
-                        self.last_kid.fetch_add(1, Ordering::SeqCst),
-                        kara.file_hash,
-                    ))
-                    .await
-            },
-        };
 
-        let get_str = |str| self.pool.get_str::<Arc<str>>(str);
         let (language, kara_makers) = tokio::join!(
-            join_all(kara.language.into_iter().map(get_str)),
-            join_all(kara.kara_makers.into_iter().map(get_str))
+            join_all((kara.language.into_iter()).map(|str| self.pool.get_str(str))),
+            join_all((kara.kara_makers.into_iter()).map(|str| self.pool.get_str(str)))
         );
 
         let mut tags = HashMap::<Arc<str>, Vec<Arc<str>>>::default();
         for [key, value] in kara.tags {
             if value.is_empty() {
-                tags.entry(get_str(key).await).or_insert_with(Vec::new);
+                tags.entry(self.pool.get_str(key).await)
+                    .or_insert_with(Vec::new);
             } else {
-                let (key, value) = tokio::join!(get_str(key), get_str(value));
+                let (key, value) = tokio::join!(self.pool.get_str(key), self.pool.get_str(value));
                 tags.entry(key).or_insert_with(Vec::new).push(value);
             }
         }
 
-        let kurisu_api::v2::Kara {
-            file_hash: hash,
-            filesize,
-            ..
-        } = kara;
+        let kara_status = match (kara.is_virtual, kara.file_hash) {
+            (true, None) => KaraStatus::Virtual,
+            (true, Some(hash)) => {
+                log::warn!("kara {remote} has virtual flag but a file hash: {hash}");
+                KaraStatus::Virtual
+            }
+            (false, None) => {
+                log::error!("the kara {remote} is not virtual but don't have a file hash");
+                KaraStatus::Virtual
+            }
+            (false, Some(hash)) => KaraStatus::Physical {
+                filesize: kara.filesize,
+                hash,
+            },
+        };
 
         self.add_kara(Kara {
-            id,
+            id: id.unwrap_or_else(|| self.pool.next_kid()),
             tags,
-            remote: rkid,
+            remote,
+            kara_status,
             song_type: kara.song_type,
             song_title: kara.song_title,
             song_source: kara.song_source,
@@ -192,10 +182,6 @@ impl<'a, Storage: DatabaseStorage> UpdateHandler<'a, Storage> {
                 updated_at: kara.updated_at,
                 epoch: kara.epoch,
             },
-            kara_status: match kara.is_virtual {
-                true => KaraStatus::Virtual,
-                false => KaraStatus::Physical { filesize, hash },
-            },
         })
         .await
     }
diff --git a/lektor_nkdb/src/id.rs b/lektor_nkdb/src/id.rs
new file mode 100644
index 00000000..0dea9e69
--- /dev/null
+++ b/lektor_nkdb/src/id.rs
@@ -0,0 +1,97 @@
+//! Contains ID definitions used for kara and playlists.
+
+use derive_more::{Display, Into};
+use serde::{Deserialize, Serialize};
+use std::{borrow, num, str::FromStr, sync::Arc};
+
+/// The string must respect the following format, you can assert this:
+///     "local_id:path"
+/// Note that the path can't contains slashes!
+#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, Display, Hash, Debug, Into)]
+#[repr(transparent)]
+#[serde(transparent)]
+#[display("{_0}")]
+pub struct KId(pub(crate) u64);
+
+impl PartialEq<KId> for u64 {
+    fn eq(&self, other: &KId) -> bool {
+        other.0 == *self
+    }
+}
+
+impl PartialEq<u64> for KId {
+    fn eq(&self, other: &u64) -> bool {
+        self.0 == *other
+    }
+}
+
+impl FromStr for KId {
+    type Err = num::ParseIntError;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        s.parse().map(Self)
+    }
+}
+
+impl From<u64> for KId {
+    fn from(value: u64) -> Self {
+        Self(value)
+    }
+}
+
+/// The string must respect the following format, you can assert this:
+///     "remote_id@remote_name"
+/// Note that the path can't contains slashes!
+#[derive(Serialize, Deserialize, Clone, PartialEq, Eq, Hash, Display, Debug)]
+#[repr(transparent)]
+#[serde(transparent)]
+#[display("{_0}")]
+pub struct RemoteKId(pub(crate) Arc<str>);
+
+impl RemoteKId {
+    /// Create a raw string representation of the [RemoteKId], unsafe operation.
+    pub(super) fn new(remote_id: u64, remote_name: impl std::fmt::Display) -> Self {
+        Self(format!("{remote_id}@{remote_name}").into())
+    }
+
+    /// Split the id in its different parts
+    fn split(&self) -> (&str, &str) {
+        self.0.split_once('@').expect("invalid kid found")
+    }
+
+    /// Get the name of the remote server
+    pub fn remote_name(&self) -> &str {
+        self.split().1
+    }
+
+    /// Get the id of the kara in the remote server
+    pub fn remote_id(&self) -> u64 {
+        self.split()
+            .0
+            .parse::<u64>()
+            .expect("invalid id part in kid found")
+    }
+
+    /// Get the string representaiton of the [RemoteKId].
+    pub fn as_str(&self) -> &str {
+        self.0.as_ref()
+    }
+}
+
+impl borrow::Borrow<str> for RemoteKId {
+    fn borrow(&self) -> &str {
+        self.as_str()
+    }
+}
+
+impl From<Arc<str>> for RemoteKId {
+    fn from(value: Arc<str>) -> Self {
+        Self(value)
+    }
+}
+
+impl AsRef<str> for RemoteKId {
+    fn as_ref(&self) -> &str {
+        self.as_str()
+    }
+}
diff --git a/lektor_nkdb/src/lib.rs b/lektor_nkdb/src/lib.rs
index e0d164a2..f9e94341 100644
--- a/lektor_nkdb/src/lib.rs
+++ b/lektor_nkdb/src/lib.rs
@@ -1,65 +1,43 @@
-//! A database to store informations about kara, by conserving old versions.
+//! A new implementation of a database to store informations about karas, playlists and such.
 
 pub use crate::{
     database::{
-        KId, Kara, KaraStatus, KaraTimeStamps, RemoteKId, SongOrigin, SongType, UpdateHandler,
+        epoch::Epoch,
+        kara::{Kara, KaraStatus, KaraTimeStamps},
+        update::UpdateHandler,
     },
-    playlist::{Playlist, PlaylistInfo, PlaylistName},
-    queue::{Priority, PRIORITY_LENGTH, PRIORITY_VALUES},
+    id::{KId, RemoteKId},
+    playlists::playlist::{Playlist, PlaylistInfo},
     search::{KaraBy, SearchFrom},
-    storage::*,
+    storage::{DatabaseDiskStorage, DatabaseStorage},
 };
+pub use kurisu_api::v2::{SongOrigin, SongType};
 
-use crate::{database::*, queue::*, search::*};
-use anyhow::{anyhow, bail, Context, Result};
-use futures::{
-    stream::{self, FuturesUnordered},
-    StreamExt,
+use crate::{
+    database::{epoch::EpochData, pool::Pool},
+    search::*,
 };
+use anyhow::{anyhow, Context as _, Result};
 use hashbrown::HashMap;
 use lektor_utils::pushvec::*;
-use playlist::Playlists;
-use serde::{Deserialize, Serialize};
-use std::ops::{RangeBounds, RangeFull};
-use tokio::sync::RwLock;
+use playlists::{Playlists, PlaylistsHandle};
 
 mod database;
-mod playlist;
-mod queue;
+mod id;
+mod playlists;
 mod search;
 mod storage;
+mod strings;
 
 /// The database type, we use interior mutability to handle the MT things.
 #[derive(Debug)]
 pub struct Database<Storage: DatabaseStorage = DatabaseDiskStorage> {
     pool: Pool,
-    queue: Queue,
     playlists: Playlists,
-    playstate: RwLock<PlayState>,
     epochs: PushVec<Epoch>,
     storage: Storage,
 }
 
-/// Play state, stored in the database.
-#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Default)]
-#[serde(rename_all = "lowercase")]
-pub enum PlayState {
-    #[default]
-    Stop,
-    Play,
-    Pause,
-}
-
-impl AsRef<str> for PlayState {
-    fn as_ref(&self) -> &str {
-        match self {
-            PlayState::Stop => "stopped",
-            PlayState::Play => "play",
-            PlayState::Pause => "paused",
-        }
-    }
-}
-
 impl<Storage: DatabaseStorage> Database<Storage> {
     /// Create a new database with the correspondig prefix.
     pub async fn new(prefix: impl Into<Storage::Prefix>) -> Result<Self> {
@@ -72,37 +50,34 @@ impl<Storage: DatabaseStorage> Database<Storage> {
         let mut last_epoch = epochs
             .push(storage.read_last_epoch().await?.unwrap_or_default().into())
             .await;
-        let mut pool = Pool::from_iter(
-            last_epoch
-                .content()
-                .ids()
-                .map(|(a, b)| (a.clone(), b.clone())),
-        )
-        .await;
+        let mut pool = Pool::from_iter(last_epoch.content().ids()).await;
 
         log::info!("fatcorize content in the last_epoch and in the playlists");
-        let mut playlists = storage
-            .read_playlists()
-            .await
-            .with_context(|| "failed to read playlists")?;
         pool.factorize_epoch_data(last_epoch.content().data_mut());
-        playlists
-            .iter_mut()
-            .for_each(|(_, plt)| pool.factorize_playlist(plt));
 
         log::info!("database loaded from disk");
         last_epoch.finished();
+
         Ok(Self {
+            playlists: Playlists::new(
+                storage
+                    .read_playlists()
+                    .await
+                    .with_context(|| "failed to read playlists")?,
+            ),
             pool,
-            queue: Default::default(),
-            playstate: Default::default(),
-            playlists: FromIterator::from_iter(playlists),
             epochs,
             storage,
         })
     }
 
+    /// Get the playlist handle of the database.
+    pub fn playlists(&self) -> PlaylistsHandle<Storage> {
+        PlaylistsHandle::new(&self.playlists, &self.storage)
+    }
+
     /// Get the update handler.
+    #[must_use]
     pub async fn update(&self) -> UpdateHandler<Storage> {
         UpdateHandler::new(
             &self.pool,
@@ -113,429 +88,25 @@ impl<Storage: DatabaseStorage> Database<Storage> {
         .await
     }
 
-    /// Refresh the [KId]s of the playlists to use a most up-to-date version of a kara if
-    /// available. Should be called after a successfull update process. In case of failure we log
-    /// the error and do nothing else...
-    pub async fn refresh_playlist_contents(&self) {
-        let Some(epoch) = self.last_epoch().await else {
-            log::error!("no epoch in database, can't refresh playlists");
-            return;
-        };
-        for name in self.playlists.list_names().await {
-            log::info!("refresh ids for playlist {name}");
-            let ids = stream::iter(self.playlists.get_content(&name).await.unwrap_or_default())
-                .then(|id| async {
-                    let locals = self.pool.get_other_locals(id).await;
-                    locals.into_iter().filter(|id| epoch.contains(id))
-                })
-                .collect::<FuturesUnordered<_>>();
-            self.playlists
-                .refresh(&name, ids.await.into_iter().flatten().collect::<Vec<KId>>())
-                .await
-                .map_err(|err| log::error!("failed to refresh ids of playlist {name}: {err}"))
-                .unwrap_or_default();
-        }
-    }
-
-    /// Get the [KId] out of its string representation. We query the pool to guaranty the pointer
-    /// unicity for faster checks.
-    pub async fn get_kid_from_str(&self, kid: impl AsRef<str>) -> Option<KId> {
-        self.pool.try_get_str(kid).await
-    }
-
-    /// Get a [Kara] by its [u64] representation in the last epoch.
-    pub async fn get_kara_by_id(&self, id: u64) -> Result<&Kara> {
-        self.last_epoch()
-            .await
-            .ok_or(anyhow!("empty epoch"))?
-            .get_kara_by_u64(id)
-            .ok_or(anyhow!("no kara {id}"))
-    }
-
     /// Get a [Kara] by its [KId] representation in the last epoch. Should be more efficient than
     /// the [Self::get_kara_by_id] because we dirrectly use the hash thing and we don't iterate
     /// over all the items in the [HashMap].
     pub async fn get_kara_by_kid(&self, id: KId) -> Result<&Kara> {
-        let local_id = id.local_id();
         self.last_epoch()
             .await
-            .ok_or(anyhow!("empty epoch"))?
+            .context("empty epoch")?
             .get_kara_by_kid(id)
-            .ok_or(anyhow!("no kara {local_id}"))
-    }
-
-    /// Search the database with a specific regex.
-    pub async fn search(&self, from: SearchFrom, regex: Vec<KaraBy>) -> Result<Vec<KId>> {
-        let regex = SearchBy::from_iter(
-            regex
-                .into_iter()
-                .map(SearchBy::new)
-                .collect::<Result<Vec<_>, _>>()?,
-        );
-        let kids = match from {
-            SearchFrom::Playlist(plt) => self.playlists.get_content(plt).await.unwrap_or_default(),
-            SearchFrom::History => self.history(RangeFull).await,
-            SearchFrom::Queue => {
-                let queue = self.queue(RangeFull).await;
-                queue.into_iter().map(|(_, kid)| kid).collect()
-            }
-            SearchFrom::Database => match self.last_epoch().await {
-                Some(epoch) => epoch.ids().map(|(kid, _)| kid.clone()).collect(),
-                None => vec![],
-            },
-        };
-        let mut karas: Vec<KId> = match self.last_epoch().await {
-            Some(epoch) => epoch
-                .get_karas_by_kid(kids)
-                .into_iter()
-                .filter_map(|kara| regex.matches(kara).then_some(kara.id.clone()))
-                .collect(),
-            None => bail!("no epoch to search kara from"),
-        };
-        let plts = regex.into_needed_playlists();
-        if !plts.is_empty() {
-            let filter: Vec<_> = stream::iter(plts.into_iter())
-                .then(|name| self.playlists.get_content(name))
-                .collect::<FuturesUnordered<_>>()
-                .await
-                .into_iter()
-                .flatten()
-                .flatten()
-                .collect();
-            karas.retain(|kara_id| filter.contains(kara_id));
-        }
-        Ok(karas)
-    }
-
-    /// Returns the kara count from the search set.
-    pub async fn count(&self, from: SearchFrom, regex: Vec<KaraBy>) -> usize {
-        log::error!("find a way to not allocate the search result buffer...");
-        self.search(from, regex)
-            .await
-            .map(|vec| vec.len())
-            .unwrap_or_default()
+            .with_context(|| format!("no kara {id}"))
     }
 
     /// Get the last epoch from the database.
-    pub(crate) async fn last_epoch(&self) -> Option<&Epoch> {
+    pub async fn last_epoch(&self) -> Option<&Epoch> {
         self.epochs.last().await
     }
 
-    /// Get the last epoch from the database.
-    pub async fn last_epoch_num(&self) -> Option<u64> {
-        self.epochs.last().await.map(|epoch| epoch.epoch_num())
-    }
-
-    /// Get the current kara with the play state.
-    ///
-    /// We have to do shenanigans. Because between karas we have an idle peridod and the playback
-    /// is stopped, we can't clear the current kara on each idle period because the next one could
-    /// have already be set in the current member.
-    pub async fn current(&self) -> (PlayState, Option<KId>) {
-        use PlayState::*;
-        let (current, playstate) = tokio::join!(self.queue.current(), self.playstate.read());
-        match *playstate {
-            Stop => (Stop, None),
-            sta @ Play | sta @ Pause => (sta, current),
-        }
-    }
-
-    /// Get the number of karas in each level of the queue.
-    pub async fn queue_count(&self) -> [usize; PRIORITY_LENGTH] {
-        self.queue.queue_count_per_level().await
-    }
-
-    /// Get the number of karas in the history.
-    pub async fn history_count(&self) -> usize {
-        self.queue.history_count().await
-    }
-
-    /// Get the play history from the most recent one to the most old one.
-    pub async fn history(&self, range: impl RangeBounds<usize> + Copy) -> Vec<KId> {
-        self.queue.history(range).await
-    }
-
-    /// Remove elements from the history, from the most recent ones to the most old ones.
-    pub async fn history_delete(&self, range: impl RangeBounds<usize> + Copy) -> Vec<KId> {
-        self.queue.history_delete(range).await
-    }
-
-    /// Get the play queue in the correct order.
-    pub async fn queue(&self, range: impl RangeBounds<usize> + Copy) -> Vec<(Priority, KId)> {
-        self.queue.queue(range).await
-    }
-
-    /// Get the play queue in the correct order, only the specified priority level.
-    pub async fn queue_get_level(&self, prio: Priority) -> Vec<KId> {
-        self.queue.queue_get_level(prio).await
-    }
-
-    /// Remove elements from the queue.
-    pub async fn queue_delete(&self, range: impl RangeBounds<usize> + Copy) {
-        self.queue.queue_delete(range).await;
-    }
-
-    /// Remove an entire level from the queue.
-    pub async fn queue_delete_level(&self, prio: Priority) {
-        self.queue.queue_delete_level(prio).await;
-    }
-
-    /// Shuffle the queue.
-    pub async fn queue_shuffle(&self, range: impl RangeBounds<usize> + Copy) {
-        self.queue.queue_shuffle(range).await
-    }
-
-    /// Shuffle the queue.
-    pub async fn queue_shuffle_level(&self, level: Priority) {
-        self.queue.queue_shuffle_level(level).await
-    }
-
-    /// Move a part of the queue after a kara.
-    pub async fn queue_move_after(&self, range: impl RangeBounds<usize> + Copy, after: usize) {
-        self.queue.queue_move_after(range, after).await
-    }
-
-    /// Get the next kara to play. Update the queue and history. It is up to the player to play the
-    /// next file.
-    #[must_use]
-    pub async fn next(&self) -> Option<KId> {
-        self.queue.next().await
-    }
-
-    /// Get the previous kara to play. Update the queue and history. It is up to the player to play
-    /// the previous file.
-    #[must_use]
-    pub async fn previous(&self) -> Option<KId> {
-        self.queue.previous().await
-    }
-
-    /// Play from a position in the queue, remove all previous karas. If the position doesn't
-    /// exists then we returns [None].
-    #[must_use]
-    pub async fn play_from_position(&self, position: usize) -> Option<KId> {
-        self.queue.play_from_position(position).await
-    }
-
-    /// Set the play state.
-    pub async fn set_playstate(&self, new_state: PlayState) {
-        let mut state = self.playstate.write().await;
-        let _ = std::mem::replace(&mut *state, new_state);
-    }
-
-    /// Toggle the play state.
-    pub async fn toggle_playstate(&self) {
-        let mut state = self.playstate.write().await;
-        let new_state = match *state {
-            PlayState::Stop | PlayState::Pause => PlayState::Play,
-            PlayState::Play => PlayState::Pause,
-        };
-        let _ = std::mem::replace(&mut *state, new_state);
-    }
-
     /// Get the path to a kara, try to get the absolute path, so here we append the relative path
     /// to the prefix of the database if possible.
     pub fn get_kara_uri(&self, id: KId) -> Result<url::Url> {
         self.storage.get_kara_uri(id)
     }
-
-    /// Insert a kara in the queue.
-    pub async fn queue_insert(&self, prio: Priority, id: KId) {
-        self.queue.insert(prio, id).await
-    }
-
-    /// Insert all karas in the queue in this order.
-    pub async fn queue_insert_all(&self, prio: Priority, ids: Vec<KId>) {
-        self.queue.insert_all(prio, ids).await
-    }
-
-    /// Insert all karas in the queue in this order.
-    pub async fn queue_insert_slice(&self, prio: Priority, ids: &[KId]) {
-        self.queue.insert_slice(prio, ids).await
-    }
-
-    /// Delete all karas from the queue with this id
-    pub async fn queue_delete_all(&self, id: KId) {
-        self.queue.queue_delete_all(id).await
-    }
-
-    /// Delete all karas from the queue with this id
-    pub async fn queue_delete_all_from_level(&self, prio: Priority, id: KId) {
-        self.queue.queue_delete_all_from_level(prio, id).await
-    }
-
-    /// Delete all karas from the queue with this id. The id was not already parsed, we try to
-    /// parse it here.
-    pub async fn queue_delete_all_maybe_id(&self, id: String) {
-        self.queue
-            .queue_delete_all(self.pool.get_str::<KId>(id).await)
-            .await
-    }
-
-    /// Delete all karas from the history with this id
-    pub async fn history_delete_all(&self, id: KId) {
-        self.queue.history_delete_all(id).await
-    }
-
-    /// Get a playlist by its name.
-    pub async fn playlist_get_content(&self, name: impl AsRef<str>) -> Option<Vec<KId>> {
-        self.playlists.get_content(name.as_ref()).await
-    }
-
-    /// Get a list of all the playlists
-    pub async fn playlists(&self) -> Vec<(PlaylistName, PlaylistInfo)> {
-        self.playlists.list().await
-    }
-
-    /// Rename a playlist.
-    pub async fn playlist_rename(
-        &self,
-        name: PlaylistName,
-        new_name: PlaylistName,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .rename(name.clone(), new_name.clone(), user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(new_name, &self.storage)
-            .await?;
-        self.storage.delete_playlist(name).await?;
-        Ok(())
-    }
-
-    /// Give a playlist to another user.
-    pub async fn playlist_give_to(
-        &self,
-        name: PlaylistName,
-        new_user: impl ToString,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .give_to(name.clone(), new_user.to_string(), user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
-
-    /// Create a new playlist. Returns whever the creation operation was successfull or not.
-    pub async fn playlist_new(
-        &self,
-        name: impl Into<PlaylistName>,
-        plt: impl Into<PlaylistInfo>,
-    ) -> Result<()> {
-        self.playlists.create(name, plt, &self.storage).await
-    }
-
-    /// Delete a playlist. Returns whever the operation was successfull or not.
-    pub async fn playlist_delete(
-        &self,
-        name: impl Into<PlaylistName>,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        let name = name.into();
-        self.storage.delete_playlist(name.as_ref()).await?;
-        self.playlists.delete(name, user, admin).await?;
-        Ok(())
-    }
-
-    /// Delete a kara from a playlist.
-    pub async fn playlist_delete_id(
-        &self,
-        name: impl AsRef<str>,
-        kid: KId,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .remove_id(name.as_ref(), kid, user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
-
-    /// Delete a multiple karas from a playlist.
-    pub async fn playlist_delete_ids(
-        &self,
-        name: impl AsRef<str>,
-        kids: Vec<KId>,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .remove_ids(name.as_ref(), &kids, user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
-
-    /// Add a kara to a playlist.
-    pub async fn playlist_add_id(
-        &self,
-        name: impl AsRef<str>,
-        kid: KId,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .add_id(name.as_ref(), kid, user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
-
-    /// Add multiple karas to a playlist.
-    pub async fn playlist_add_ids(
-        &self,
-        name: impl AsRef<str>,
-        kids: Vec<KId>,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .add_ids(name.as_ref(), &kids, user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
-
-    /// Add karas from a playlist to another playlist.
-    pub async fn playlist_add_playlist(
-        &self,
-        name: impl AsRef<str>,
-        from: impl AsRef<str>,
-        rand: bool,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .add_plt(name.as_ref(), from.as_ref(), rand, user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
-
-    /// Remove karas from a playlist from another playlist.
-    pub async fn playlist_delete_playlist(
-        &self,
-        name: impl AsRef<str>,
-        from: impl AsRef<str>,
-        user: impl AsRef<str>,
-        admin: bool,
-    ) -> Result<()> {
-        self.playlists
-            .remove_plt(name.as_ref(), from.as_ref(), user, admin)
-            .await?;
-        self.playlists
-            .write_playlist_with(name, &self.storage)
-            .await
-    }
 }
diff --git a/lektor_nkdb/src/playlist/infos.rs b/lektor_nkdb/src/playlist/infos.rs
deleted file mode 100644
index e08b0fc7..00000000
--- a/lektor_nkdb/src/playlist/infos.rs
+++ /dev/null
@@ -1,64 +0,0 @@
-use serde::{Deserialize, Serialize};
-
-/// Informations about a playlist.
-#[derive(Debug, Serialize, Deserialize, Clone)]
-pub struct PlaylistInfo {
-    /// The name of the user who created the playlist. If the user who created the playlist wasn't
-    /// identified, then it's an anonymous playlist.
-    pub user: Option<String>,
-
-    /// Creation time of the playlist.
-    pub created_at: i64,
-
-    /// Last update time of the playlist.
-    pub updated_at: i64,
-}
-
-impl PlaylistInfo {
-    pub fn new() -> Self {
-        Self {
-            user: None,
-            created_at: chrono::Utc::now().timestamp(),
-            updated_at: chrono::Utc::now().timestamp(),
-        }
-    }
-
-    pub fn user(self, user: impl ToString) -> Self {
-        Self {
-            user: Some(user.to_string()),
-            ..self
-        }
-    }
-
-    pub fn created_at(self, created_at: i64) -> Self {
-        Self { created_at, ..self }
-    }
-
-    pub fn updated_at(self, updated_at: i64) -> Self {
-        Self { updated_at, ..self }
-    }
-
-    /// Should a user be authorized to modify a playlist?
-    pub fn authorize_write(&self, name: impl AsRef<str>, admin: bool) -> bool {
-        let name = name.as_ref();
-        if admin {
-            log::warn!("bypass author on playlist request by admin {name}");
-        }
-        let ret = admin
-            || (self.user.as_ref())
-                .map(|user| user.eq(name))
-                .unwrap_or_default();
-        if ret {
-            log::info!("authorize {name} to edit playlist");
-        } else {
-            log::info!("authorizen't {name} to edit playlist");
-        }
-        ret
-    }
-}
-
-impl Default for PlaylistInfo {
-    fn default() -> Self {
-        Self::new()
-    }
-}
diff --git a/lektor_nkdb/src/playlist/mod.rs b/lektor_nkdb/src/playlist/mod.rs
deleted file mode 100644
index 1c8cd458..00000000
--- a/lektor_nkdb/src/playlist/mod.rs
+++ /dev/null
@@ -1,10 +0,0 @@
-//! How to store playlists. Because playlists and epochs don't have the same lifespan we must do
-//! some shenanigans.
-
-mod infos;
-mod name;
-mod register;
-mod values;
-
-pub use self::{infos::*, name::*, values::*};
-pub(crate) use register::*;
diff --git a/lektor_nkdb/src/playlist/name.rs b/lektor_nkdb/src/playlist/name.rs
deleted file mode 100644
index 64037992..00000000
--- a/lektor_nkdb/src/playlist/name.rs
+++ /dev/null
@@ -1,140 +0,0 @@
-//! We have some restrictions on playlists' name to have a usable thing...
-
-use anyhow::bail;
-use serde::{Deserialize, Serialize};
-use std::{borrow::Borrow, str::FromStr, sync::Arc};
-
-/// A playlist name, we excract it from the path.
-///
-/// ### Safety
-/// We check before builder the playlist name that the [`u8`] is a valide UTF-8 string.
-#[derive(Clone, PartialEq, Eq, Hash)]
-pub struct PlaylistName(Arc<str>);
-
-impl FromStr for PlaylistName {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        if s.is_empty() || s.len() > Self::MAX_LEN {
-            bail!("invalid playlist name lengh of {}", s.len())
-        } else if s.chars().filter(|c| !c.is_ascii_alphanumeric()).count() > 0 {
-            bail!("invalid playlist name: not ascii alphanumeric")
-        } else {
-            Ok(Self(s.into()))
-        }
-    }
-}
-
-impl PlaylistName {
-    /// The maximal length of the playlist's name.
-    pub const MAX_LEN: usize = 128;
-
-    pub fn as_str(&self) -> &str {
-        self.0.as_ref()
-    }
-}
-
-impl AsRef<str> for PlaylistName {
-    fn as_ref(&self) -> &str {
-        self.as_str()
-    }
-}
-
-impl std::fmt::Display for PlaylistName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.as_ref())
-    }
-}
-
-impl std::fmt::Debug for PlaylistName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        let name = self.as_str();
-        f.debug_tuple("PlaylistName").field(&name).finish()
-    }
-}
-
-impl Borrow<str> for PlaylistName {
-    fn borrow(&self) -> &str {
-        self.as_ref()
-    }
-}
-
-impl Serialize for PlaylistName {
-    fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        serializer.serialize_str(self.as_ref())
-    }
-}
-
-impl<'de> Deserialize<'de> for PlaylistName {
-    fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
-        use serde::de::Visitor;
-        struct PltNameVisitor;
-        impl<'de> Visitor<'de> for PltNameVisitor {
-            type Value = PlaylistName;
-
-            fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
-                formatter.write_str("string slice")
-            }
-
-            fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
-                PlaylistName::from_str(&v).map_err(serde::de::Error::custom)
-            }
-
-            fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
-                PlaylistName::from_str(v).map_err(serde::de::Error::custom)
-            }
-
-            fn visit_borrowed_str<E: serde::de::Error>(
-                self,
-                v: &'de str,
-            ) -> Result<Self::Value, E> {
-                PlaylistName::from_str(v).map_err(serde::de::Error::custom)
-            }
-        }
-
-        deserializer.deserialize_any(PltNameVisitor)
-    }
-}
-
-#[test]
-fn create_playlist_name() {
-    use lektor_utils::assert_ok;
-    assert_eq!(
-        assert_ok!(PlaylistName::from_str("foo42bar69")).as_ref(),
-        "foo42bar69"
-    );
-}
-
-#[test]
-fn invalid_playlist_name() {
-    use lektor_utils::assert_err;
-    assert_err!(PlaylistName::from_str(""));
-    assert_err!(PlaylistName::from_str("Zażółć gęślą jaźń"));
-    assert_err!(PlaylistName::from_str("@"));
-    assert_err!(PlaylistName::from_str("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"));
-}
-
-#[test]
-fn playlist_name_eq() {
-    use lektor_utils::assert_ok;
-    use std::{
-        hash::{DefaultHasher, Hash, Hasher},
-        ptr,
-    };
-    let a = assert_ok!(PlaylistName::from_str("abc"));
-    let b = assert_ok!(PlaylistName::from_str("abc"));
-    assert!(!ptr::addr_eq(a.0.as_ptr(), b.0.as_ptr()));
-    assert_eq!(a, b);
-    assert_eq!(
-        {
-            let mut s = DefaultHasher::new();
-            a.hash(&mut s);
-            s.finish()
-        },
-        {
-            let mut s = DefaultHasher::new();
-            b.hash(&mut s);
-            s.finish()
-        },
-    );
-}
diff --git a/lektor_nkdb/src/playlist/register.rs b/lektor_nkdb/src/playlist/register.rs
deleted file mode 100644
index 85664188..00000000
--- a/lektor_nkdb/src/playlist/register.rs
+++ /dev/null
@@ -1,292 +0,0 @@
-//! A list of playlists. It's different from an epoch because inside an epoch the playlists can
-//! change.
-
-use crate::{DatabaseStorage, KId, Playlist, PlaylistInfo, PlaylistName};
-use anyhow::{anyhow, bail, Result};
-use hashbrown::{hash_map::OccupiedEntry, HashMap};
-use rand::{seq::SliceRandom, thread_rng};
-use std::ops::Deref;
-use tokio::sync::RwLock;
-
-/// This type is just a wrapper around the [PlaylistsContent] with a [RwLock]. For function
-/// documentation see [PlaylistsContent].
-#[derive(Debug)]
-pub(crate) struct Playlists(RwLock<PlaylistsContent>);
-
-/// All the data from the playlists.
-#[derive(Debug)]
-struct PlaylistsContent(HashMap<PlaylistName, Playlist>);
-
-impl Playlists {
-    /// Write the playlist to a storage.
-    pub async fn write_playlist_with(
-        &self,
-        name: impl AsRef<str>,
-        storage: &impl DatabaseStorage,
-    ) -> Result<()> {
-        match self.0.read().await.0.get(name.as_ref()) {
-            Some(plt) => storage.write_playlist(name.as_ref(), plt).await?,
-            None => bail!("failed to get playlist {}", name.as_ref()),
-        }
-        Ok(())
-    }
-
-    /// Add a new playlist if it didn't already exists. Returns whever the creation operation was
-    /// successfull or not.
-    pub async fn create(
-        &self,
-        name: impl Into<PlaylistName>,
-        plt: impl Into<PlaylistInfo>,
-        storage: &impl DatabaseStorage,
-    ) -> Result<()> {
-        let name = name.into();
-        let mut this = self.0.write().await;
-        if !this.0.contains_key(&name) {
-            let plt = Into::<PlaylistInfo>::into(plt).into();
-            storage.write_playlist(&name, &plt).await?;
-            log::info!("create playlist {name}: {plt:#?}");
-            this.0.insert(name, plt);
-            Ok(())
-        } else {
-            bail!("can't create playlist {name} as it already exits")
-        }
-    }
-}
-
-impl FromIterator<(PlaylistName, Playlist)> for PlaylistsContent {
-    fn from_iter<T: IntoIterator<Item = (PlaylistName, Playlist)>>(iter: T) -> Self {
-        Self(HashMap::from_iter(iter))
-    }
-}
-
-impl FromIterator<(PlaylistName, Playlist)> for Playlists {
-    fn from_iter<T: IntoIterator<Item = (PlaylistName, Playlist)>>(iter: T) -> Self {
-        Self(RwLock::new(FromIterator::from_iter(iter)))
-    }
-}
-
-impl PlaylistsContent {
-    /// Get the playlist in write access if allowed.
-    fn write_plt_if_authorizes<T>(
-        &mut self,
-        name: impl AsRef<str>,
-        user: impl AsRef<str>,
-        admin: bool,
-        cb: impl FnOnce(&mut Playlist) -> Result<T>,
-    ) -> Result<T> {
-        let (name, user) = (name.as_ref(), user.as_ref());
-        let Some(playlist) = self.0.get_mut(name) else {
-            bail!("playlist {name} asked by user {user} doesn't exist")
-        };
-        if playlist.authorize_write(user, admin) {
-            cb(playlist)
-        } else {
-            bail!("user {user} is not authorized to write playlist {name}")
-        }
-    }
-
-    /// Get the playlist in write access if allowed. Here we get the whole entry, not just the
-    /// value out of the hashmap.
-    fn write_plt_entry_if_authorizes<T>(
-        &mut self,
-        name: impl Into<PlaylistName>,
-        user: impl AsRef<str>,
-        admin: bool,
-        cb: impl FnOnce(OccupiedEntry<'_, PlaylistName, Playlist>) -> Result<T>,
-    ) -> Result<T> {
-        use hashbrown::hash_map::Entry;
-        let user = user.as_ref();
-        match self.0.entry(name.into()) {
-            Entry::Occupied(entry) if entry.get().authorize_write(user, admin) => cb(entry),
-            Entry::Occupied(entry) => {
-                bail!(
-                    "user {user} is not authorized to write playlist {}",
-                    entry.key()
-                )
-            }
-            Entry::Vacant(entry) => {
-                bail!("failed to get playlist {}", entry.key())
-            }
-        }
-    }
-}
-
-macro_rules! impl_playlists {
-    ($(
-        $(#[$doc:meta])*
-        $operation: ident fn $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)? $body: block
-    )+) => {
-        impl Playlists {
-            $(impl_playlists! {
-                $(#[$doc])*
-                bridge: $operation $name (&$this $(, $arg: $ty)*) $(-> $ret)?
-            })+
-        }
-
-        impl PlaylistsContent {
-            $(impl_playlists! {
-                $(#[$doc])*
-                original: $operation $name (&$this $(, $arg: $ty)*) $(-> $ret)? { $body }
-            })+
-        }
-    };
-
-    (
-        $(#[$doc:meta])*
-        bridge: $operation: ident $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)?
-    ) => {
-        #[allow(dead_code)]
-        $(#[$doc])*
-        pub async fn $name(&$this $(,$arg: $ty)*) $(-> $ret)? {
-            $this.0.$operation().await.$name($($arg),*)
-        }
-    };
-
-    (
-        $(#[$doc:meta])*
-        original: read $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)? { $body: block }
-    ) => {
-        $(#[$doc])*
-        pub fn $name(&$this $(,$arg: $ty)*) $(-> $ret)? { $body }
-    };
-
-    (
-        $(#[$doc:meta])*
-        original: write $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)? { $body: block }
-    ) => {
-        $(#[$doc])*
-        pub fn $name(&mut $this $(,$arg: $ty)*) $(-> $ret)? { $body }
-    };
-}
-
-impl_playlists! {
-    /// Get the total count of playlists
-    read fn count(&self) -> usize {
-        self.0.len()
-    }
-
-    /// Get the list of all the playlists.
-    read fn list(&self) -> Vec<(PlaylistName, PlaylistInfo)> {
-        self.0.iter().map(|(name, plt)| (name.clone(), plt.deref().clone())).collect()
-    }
-
-    /// Get the list of all the playlists.
-    read fn list_names(&self) -> Vec<PlaylistName> {
-        self.0.iter().map(|(name, _)| name.clone()).collect()
-    }
-
-    /// Refresh some ids of a playlist with the new ones. Here we skip the user check thing as this
-    /// function should not be called by a user but as part of the update process.
-    write fn refresh(&self, name: impl AsRef<str>, ids: Vec<KId>) -> Result<()> {
-        let (mut ids, name) = (ids, name.as_ref());
-        let plt = self.0.get_mut(name).ok_or_else(|| anyhow!("no playlist {name} to refresh"))?;
-        ids.retain(|id| !plt.content.contains(id));
-        if ids.is_empty() {
-            log::warn!("no ids to refresh the playlist {name} with...");
-        } else {
-            plt.content.append(&mut ids);
-        }
-        Ok(())
-    }
-
-    /// Rename a playlist.
-    write fn rename(&self, name: PlaylistName, new_name: PlaylistName, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        if self.0.contains_key(&new_name) {
-            bail!("can't rename {name} to {new_name} because the playlist already exists")
-        }
-        let entry = match self.0.entry(name) {
-            hashbrown::hash_map::Entry::Occupied(entry) if entry.get().authorize_write(user.as_ref(), admin) => entry.remove(),
-            hashbrown::hash_map::Entry::Occupied(entry) => bail!("user {} not authorized to write {}", user.as_ref(), entry.key().as_str()),
-            hashbrown::hash_map::Entry::Vacant(entry)   => bail!("failed to get playlist {}", entry.into_key())
-        };
-        match self.0.insert(new_name, entry) {
-            Some(_) => bail!("failed to rename the playlist"),
-            None => Ok(()),
-        }
-    }
-
-    /// Give a playlist to another user.
-    write fn give_to(&self, name: PlaylistName, new_user: impl ToString, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_entry_if_authorizes(name, user, admin, |mut entry| {
-            entry.get_mut().user = Some(new_user.to_string());
-            Ok(())
-        })
-    }
-
-    /// Deletes a playlist, returns whever the operation was successfull or not.
-    write fn delete(&self, name: PlaylistName, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_entry_if_authorizes(name, user, admin, |entry| {
-            log::info!("remove playlist {}", entry.key().as_str());
-            entry.remove();
-            Ok(())
-        })
-    }
-
-    /// Add a kara to a playlist. Returns true if it was successfull, false otherwise.
-    write fn add(&self, name: impl AsRef<str>, id: KId, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_if_authorizes(name, user, admin, |playlist| {
-            playlist.content.push(id);
-            Ok(())
-        })
-    }
-
-    /// Remove a kara from a playlist. Returns true if it was successfull, false otherwise.
-    write fn remove_id(&self, name: impl AsRef<str>, id: KId, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_if_authorizes(name, user, admin, |playlist| {
-            playlist.content.retain(|kid| id.ne(kid));
-            Ok(())
-        })
-    }
-
-    /// Remove karas from a playlist. Returns true if it was successfull, false otherwise.
-    write fn remove_ids(&self, name: impl AsRef<str>, ids: &[KId], user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_if_authorizes(name, user, admin, |playlist| {
-            playlist.content.retain(|kid| !ids.contains(kid));
-            Ok(())
-        })
-    }
-
-    /// Remove a kara from a playlist. Returns true if it was successfull, false otherwise.
-    write fn add_id(&self, name: impl AsRef<str>, id: KId, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_if_authorizes(name, user, admin, |playlist| {
-            playlist.extend(Some(id));
-            Ok(())
-        })
-    }
-
-    /// Remove karas from a playlist. Returns true if it was successfull, false otherwise.
-    write fn add_ids(&self, name: impl AsRef<str>, ids: &[KId], user: impl AsRef<str>, admin: bool) -> Result<()> {
-        self.write_plt_if_authorizes(name, user, admin, |playlist| {
-            playlist.extend(ids.iter().cloned());
-            Ok(())
-        })
-    }
-
-    /// Add the content of a playlist into another one.
-    write fn add_plt(&self, name: impl AsRef<str>, from: impl AsRef<str>, rand: bool, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        let Some(mut ids) = self.get_content(&from) else { bail!("playlist {} doesn't exists", from.as_ref()) };
-        if rand { ids[..].shuffle(&mut thread_rng()) }
-        self.add_ids(name, &ids, user, admin)
-    }
-
-    /// Remove from a playlist the content of another one.
-    write fn remove_plt(&self, name: impl AsRef<str>, from: impl AsRef<str>, user: impl AsRef<str>, admin: bool) -> Result<()> {
-        let Some(ids) = self.get_content(&from) else { bail!("playlist {} doesn't exists", from.as_ref()) };
-        self.remove_ids(name, &ids, user, admin)
-    }
-
-    /// Get the content of a playlist, we need to clone because of the lock...
-    read fn get_content(&self, name: impl AsRef<str>) -> Option<Vec<KId>> {
-        let res = self.0.get(name.as_ref()).map(|plt| plt.content().to_vec());
-        if res.is_none() { log::error!("playlist {} doesn't exist", name.as_ref()); }
-        res
-    }
-
-    /// Count the number of elements in the playlist. If the playlist didn't exist returns [None],
-    /// otherwise returns [Some] of the count.
-    read fn count_playlist(&self, name: impl AsRef<str>) -> Option<usize> {
-        let res = self.0.get(name.as_ref()).map(|plt| plt.content().len());
-        if res.is_none() { log::error!("playlist {} doesn't exist", name.as_ref()); }
-        res
-    }
-}
diff --git a/lektor_nkdb/src/playlist/values.rs b/lektor_nkdb/src/playlist/values.rs
deleted file mode 100644
index 67611570..00000000
--- a/lektor_nkdb/src/playlist/values.rs
+++ /dev/null
@@ -1,54 +0,0 @@
-//! A single playlist.
-
-use crate::{KId, PlaylistInfo};
-use serde::{Deserialize, Serialize};
-
-/// The playlist, with the informations and its content.
-#[derive(Debug, Serialize, Deserialize, Clone, Default)]
-pub struct Playlist {
-    /// Metadata about the playlist.
-    pub(crate) infos: PlaylistInfo,
-
-    /// The content of a playlist. Here the [KId] are about all the epochs. When loading a new
-    /// epoch, if a new [KId] replaces an old one, then we also add it to the playlist. At loading
-    /// time we filter the [KId]s.
-    pub(crate) content: Vec<KId>,
-}
-
-impl From<PlaylistInfo> for Playlist {
-    fn from(infos: PlaylistInfo) -> Self {
-        Self {
-            infos,
-            ..Default::default()
-        }
-    }
-}
-
-impl std::ops::Deref for Playlist {
-    type Target = PlaylistInfo;
-    fn deref(&self) -> &Self::Target {
-        &self.infos
-    }
-}
-
-impl std::ops::DerefMut for Playlist {
-    fn deref_mut(&mut self) -> &mut Self::Target {
-        &mut self.infos
-    }
-}
-
-impl Playlist {
-    pub fn content(&self) -> &[KId] {
-        &self.content
-    }
-}
-
-impl Extend<KId> for Playlist {
-    fn extend<T: IntoIterator<Item = KId>>(&mut self, iter: T) {
-        for kid in iter.into_iter() {
-            if !self.content.contains(&kid) {
-                self.content.push(kid);
-            }
-        }
-    }
-}
diff --git a/lektor_nkdb/src/playlists/mod.rs b/lektor_nkdb/src/playlists/mod.rs
new file mode 100644
index 00000000..290c47d1
--- /dev/null
+++ b/lektor_nkdb/src/playlists/mod.rs
@@ -0,0 +1,112 @@
+//! Contains implementation about a collection of playlists.
+
+pub(crate) mod playlist;
+
+use crate::{playlists::playlist::Playlist, DatabaseStorage, KId};
+use anyhow::{bail, Context as _, Result};
+use hashbrown::{hash_map, HashMap};
+use std::sync::atomic::{AtomicU64, Ordering};
+use tokio::sync::RwLock;
+
+/// This type is just a wrapper around the [PlaylistsContent] with a [RwLock]. For function
+/// documentation see [PlaylistsContent].
+#[derive(Debug)]
+pub(crate) struct Playlists {
+    content: RwLock<HashMap<KId, Playlist>>,
+    epoch: AtomicU64,
+}
+
+pub struct PlaylistsHandle<'a, Storage: DatabaseStorage + Sized> {
+    playlists: &'a Playlists,
+    storage: &'a Storage,
+}
+
+impl<'a, Storage: DatabaseStorage + Sized> PlaylistsHandle<'a, Storage> {
+    pub(crate) fn new(playlists: &'a Playlists, storage: &'a Storage) -> Self {
+        Self { playlists, storage }
+    }
+
+    pub async fn epoch(&self) -> u64 {
+        self.playlists.epoch.load(Ordering::Acquire)
+    }
+
+    pub async fn is_empty(&self) -> bool {
+        self.playlists.content.read().await.is_empty()
+    }
+
+    pub async fn len(&self) -> usize {
+        self.playlists.content.read().await.len()
+    }
+
+    pub async fn read<T>(&self, plt: KId, cb: impl FnOnce(&Playlist) -> T) -> Result<T> {
+        Ok(cb((self.playlists.content.read().await)
+            .get(&plt)
+            .context("playlist not found")?))
+    }
+
+    pub async fn list(&self) -> Vec<(KId, String)> {
+        (self.playlists.content.read().await.iter())
+            .map(|(&id, playlist)| (id, playlist.name().to_string()))
+            .collect()
+    }
+
+    pub async fn write<T>(
+        &self,
+        plt: KId,
+        user: impl AsRef<str>,
+        admin: bool,
+        cb: impl FnOnce(&mut Playlist) -> T,
+    ) -> Result<T> {
+        let mut this = self.playlists.content.write().await;
+        let plt = this
+            .get_mut(&plt)
+            .context("playlist not found")?
+            .authorize_writes(user, admin)
+            .context("user not allowed to modify playlist")?;
+        let res = cb(plt);
+        self.storage.write_playlist(plt).await?;
+        self.playlists.epoch.fetch_add(1, Ordering::AcqRel);
+        Ok(res)
+    }
+
+    /// Add a new playlist if it didn't already exists. Returns whever the creation operation was
+    /// successfull or not.
+    pub async fn create(
+        &self,
+        name: impl ToString,
+        settings: impl FnOnce(Playlist) -> Playlist,
+    ) -> Result<()> {
+        let mut this = self.playlists.content.write().await;
+        let next_id = KId(this.keys().map(|KId(id)| id + 1).max().unwrap_or(1));
+        let plt = settings(Playlist::new(next_id, name)).updated_now();
+        self.playlists.epoch.fetch_add(1, Ordering::AcqRel);
+        self.storage.write_playlist(&plt).await?;
+        this.insert(next_id, plt);
+        Ok(())
+    }
+
+    pub async fn delete(&self, plt: KId, user: impl AsRef<str>, admin: bool) -> Result<()> {
+        match self.playlists.content.write().await.entry(plt) {
+            hash_map::Entry::Vacant(_) => bail!("playlist not found"),
+            hash_map::Entry::Occupied(mut entry) => {
+                (entry.get_mut())
+                    .authorize_writes(user, admin)
+                    .context("user not allowed to modify the playlist")?;
+                entry.remove();
+                if let Err(err) = self.storage.delete_playlist(plt).await {
+                    log::error!("failed to delete playlist '{plt}': {err}")
+                }
+            }
+        }
+        Ok(())
+    }
+}
+
+impl Playlists {
+    pub(crate) fn new(iter: impl IntoIterator<Item = Playlist>) -> Self {
+        Self {
+            content: RwLock::new(iter.into_iter().map(|plt| (plt.local_id(), plt)).collect()),
+            epoch: AtomicU64::new(0),
+        }
+    }
+}
diff --git a/lektor_nkdb/src/playlists/playlist.rs b/lektor_nkdb/src/playlists/playlist.rs
new file mode 100644
index 00000000..78437750
--- /dev/null
+++ b/lektor_nkdb/src/playlists/playlist.rs
@@ -0,0 +1,221 @@
+//! Contains implementation about a single playlist.
+
+use crate::{strings, KId, RemoteKId};
+use hashbrown::HashSet;
+use rand::seq::SliceRandom as _;
+use serde::{Deserialize, Serialize};
+use std::sync::Arc;
+
+/// Informations about a playlist.
+#[derive(Debug, Serialize, Deserialize, Clone)]
+pub struct Playlist {
+    /// The local ID.
+    local_id: KId,
+
+    /// The remote ID. Is [None] if the playlist was not imported.
+    remote_id: Option<RemoteKId>,
+
+    /// The name of the owners of the playlist. If the user who created the playlist wasn't
+    /// identified, then it's an anonymous playlist. The users can't be empty strings.
+    owners: HashSet<Arc<str>>,
+
+    /// The name of the playlist. Note that this is not a primary key, multiple playlists can have
+    /// the same name... Must be only in ascii characters.
+    name: String,
+
+    /// The description of the playlist. Must be only in ascii characters. If it is [Some], then
+    /// the [String] is not empty.
+    description: Option<String>,
+
+    /// Creation time of the playlist. This is a local time, Kurisu doesn't have this notion.
+    created_at: i64,
+
+    /// Last update time of the playlist. This is a local time, Kurisu doesn't have this notion.
+    updated_at: i64,
+
+    /// The content of the playlist.
+    content: Vec<KId>,
+}
+
+// Builder-lite interface
+impl Playlist {
+    pub(crate) fn new(id: KId, name: impl ToString) -> Self {
+        Self {
+            name: name.to_string(),
+            local_id: id,
+            remote_id: None,
+            description: None,
+            owners: Default::default(),
+            content: Default::default(),
+
+            // Be mindfull of the order…
+            created_at: chrono::Utc::now().timestamp(),
+            updated_at: chrono::Utc::now().timestamp(),
+        }
+    }
+
+    pub fn with_owner_sync(mut self, user: impl AsRef<str>) -> Self {
+        self.owners.insert(strings::CACHE.get_sync(user.as_ref()));
+        self.updated_now()
+    }
+
+    pub async fn with_owner(mut self, user: impl AsRef<str>) -> Self {
+        self.owners.insert(strings::CACHE.get(user.as_ref()).await);
+        self.updated_now()
+    }
+
+    pub fn with_description(mut self, desc: impl ToString) -> Self {
+        let description = desc.to_string();
+        self.description = (!description.is_empty()).then_some(description);
+        self.updated_now()
+    }
+
+    pub(super) fn updated_now(mut self) -> Self {
+        self.updated_at = chrono::Utc::now().timestamp();
+        self
+    }
+}
+
+// Setters
+impl Playlist {
+    pub fn set_name(&mut self, name: impl ToString) -> &mut Self {
+        self.name = name.to_string();
+        self
+    }
+
+    pub fn add_owner(&mut self, name: impl AsRef<str>) -> &mut Self {
+        self.owners.insert(Arc::from(name.as_ref()));
+        self
+    }
+
+    pub fn remove_owner(&mut self, name: impl AsRef<str>) -> &mut Self {
+        self.owners.remove(name.as_ref());
+        self
+    }
+
+    pub(crate) fn authorize_writes(
+        &mut self,
+        user: impl AsRef<str>,
+        admin: bool,
+    ) -> Option<&mut Self> {
+        (admin || self.owners.contains(user.as_ref())).then_some(self)
+    }
+
+    pub fn push(&mut self, id: KId) {
+        self.content.push(id)
+    }
+
+    pub fn remove(&mut self, id: KId) {
+        self.retain(|other| *other != id)
+    }
+
+    pub fn retain(&mut self, cb: impl FnMut(&KId) -> bool) {
+        self.content.retain(cb)
+    }
+
+    pub fn append(&mut self, ids: &mut Vec<KId>) {
+        self.content.append(ids)
+    }
+
+    pub fn shuffle(&mut self) {
+        self.content.shuffle(&mut rand::thread_rng())
+    }
+}
+
+// Getters
+impl Playlist {
+    pub fn local_id(&self) -> KId {
+        self.local_id
+    }
+
+    pub fn contains(&self, id: KId) -> bool {
+        self.content.contains(&id)
+    }
+
+    pub fn remote_id(&self) -> Option<RemoteKId> {
+        self.remote_id.clone()
+    }
+
+    pub fn owners(&self) -> impl Iterator<Item = &str> {
+        self.owners.iter().map(Arc::as_ref)
+    }
+
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    pub fn description(&self) -> Option<&str> {
+        self.description.as_deref()
+    }
+
+    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
+        chrono::DateTime::from_timestamp(self.created_at, 0).unwrap_or_default()
+    }
+
+    pub fn updated_at(&self) -> chrono::DateTime<chrono::Utc> {
+        chrono::DateTime::from_timestamp(self.updated_at, 0).unwrap_or_default()
+    }
+
+    pub fn iter_seq_content(&self) -> impl Iterator<Item = KId> + '_ {
+        self.content.iter().copied()
+    }
+
+    pub fn iter_uniq_content(&self) -> impl Iterator<Item = KId> + '_ {
+        HashSet::<KId>::from_iter(self.content.iter().copied()).into_iter()
+    }
+
+    pub fn get_infos(&self) -> PlaylistInfo {
+        PlaylistInfo {
+            local_id: self.local_id,
+            owners: self.owners.clone(),
+            name: self.name.clone(),
+            description: self.description.clone(),
+            created_at: self.created_at,
+            updated_at: self.updated_at,
+        }
+    }
+}
+
+impl Extend<KId> for Playlist {
+    fn extend<T: IntoIterator<Item = KId>>(&mut self, iter: T) {
+        self.content.extend(iter)
+    }
+}
+
+/// Informations relative to a playlist. We don't show everything here. For fields signification,
+/// see the ones in [Playlist].
+#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq)]
+pub struct PlaylistInfo {
+    local_id: KId,
+    owners: HashSet<Arc<str>>,
+    name: String,
+    description: Option<String>,
+    created_at: i64,
+    updated_at: i64,
+}
+
+impl PlaylistInfo {
+    pub fn local_id(&self) -> KId {
+        self.local_id
+    }
+
+    pub fn owners(&self) -> impl Iterator<Item = &str> {
+        self.owners.iter().map(Arc::as_ref)
+    }
+
+    pub fn name(&self) -> &str {
+        self.name.as_ref()
+    }
+
+    pub fn description(&self) -> Option<&str> {
+        self.description.as_deref()
+    }
+
+    pub fn created_at(&self) -> chrono::DateTime<chrono::Utc> {
+        chrono::DateTime::from_timestamp(self.created_at, 0).unwrap_or_default()
+    }
+
+    pub fn updated_at(&self) -> chrono::DateTime<chrono::Utc> {
+        chrono::DateTime::from_timestamp(self.updated_at, 0).unwrap_or_default()
+    }
+}
diff --git a/lektor_nkdb/src/queue/mod.rs b/lektor_nkdb/src/queue/mod.rs
deleted file mode 100644
index 4ae27535..00000000
--- a/lektor_nkdb/src/queue/mod.rs
+++ /dev/null
@@ -1,304 +0,0 @@
-//! Store the state of the queue in memory. For now we don't store it on disk, see latter how we do
-//! that thing.
-
-mod priority;
-
-pub use priority::*;
-
-use crate::*;
-use lektor_utils::{filter_range, BoundedBoundRange};
-use rand::{seq::SliceRandom, thread_rng};
-use std::ops::RangeInclusive;
-use tokio::sync::RwLock;
-
-/// The queue contains the playing kara and the following ones. This type is just a wrapper around
-/// the [QueueContent] with a [RwLock]. For function documentation see [QueueContent].
-#[derive(Debug, Default)]
-pub(crate) struct Queue(RwLock<QueueContent>);
-
-/// The content of the queue. Is protected by a RwLock from tokio.
-#[derive(Debug, Default)]
-struct QueueContent {
-    /// The current kara
-    current: Option<KId>,
-
-    /// The queue, stored as a priority list, stored in reverse order from the priority.
-    queue: [Vec<KId>; PRIORITY_LENGTH],
-
-    /// The history, we push back into it, so the most recent one is the one at the end of the
-    /// vector.
-    history: Vec<KId>,
-}
-
-macro_rules! impl_queue {
-    ($(
-        $(#[$doc:meta])*
-        $operation: ident fn $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)? $body: block
-    )+) => {
-        impl Queue {
-            $(impl_queue! {
-                $(#[$doc])*
-                bridge: $operation $name (&$this $(, $arg: $ty)*) $(-> $ret)?
-            })+
-        }
-
-        impl QueueContent {
-            $(impl_queue! {
-                $(#[$doc])*
-                original: $operation $name (&$this $(, $arg: $ty)*) $(-> $ret)? { $body }
-            })+
-        }
-    };
-
-    (
-        $(#[$doc:meta])*
-        bridge: $operation: ident $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)?
-    ) => {
-        #[allow(dead_code)]
-        $(#[$doc])*
-        pub async fn $name(&$this $(,$arg: $ty)*) $(-> $ret)? {
-            $this.0.$operation().await.$name($($arg),*)
-        }
-    };
-
-    (
-        $(#[$doc:meta])*
-        original: read $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)? { $body: block }
-    ) => {
-        $(#[$doc])*
-        pub fn $name(&$this $(,$arg: $ty)*) $(-> $ret)? { $body }
-    };
-
-    (
-        $(#[$doc:meta])*
-        original: write $name: ident (&$this: ident $(, $arg: ident: $ty: ty)*) $(-> $ret: ty)? { $body: block }
-    ) => {
-        $(#[$doc])*
-        pub fn $name(&mut $this $(,$arg: $ty)*) $(-> $ret)? { $body }
-    };
-}
-
-impl_queue! {
-    /// Get the current kara if it exists.
-    read fn current(&self) -> Option<KId> {
-        self.current.clone()
-    }
-
-    read fn split_range_per_queue_level(&self, range: impl RangeBounds<usize>) -> Vec<(Priority, RangeInclusive<usize>)> {
-        let (mut start, mut end) = (match range.start_bound() {
-            std::ops::Bound::Included(pos) => *pos,
-            std::ops::Bound::Excluded(pos) => pos + 1,
-            std::ops::Bound::Unbounded => 0,
-        }, match range.end_bound() {
-            std::ops::Bound::Included(pos) => *pos,
-            std::ops::Bound::Excluded(pos) => pos - 1,
-            std::ops::Bound::Unbounded => self.queue_count(),
-        });
-        self.queue.iter().enumerate().rev().flat_map(|(idx, lvl)| {
-            let ret = (start <= lvl.len()).then(|| (
-                Priority::from(idx + 1),
-                start..=(lvl.len() - 1).min(end)
-            ));
-            (start, end) = (start - lvl.len(), end - lvl.len());
-            ret
-        }).collect()
-    }
-
-    /// Clear the queue up to the position specified, returning it into the current kara.
-    write fn play_from_position(&self, position: usize) -> Option<KId> {
-        (position < self.queue_count()).then(|| {
-            self.split_range_per_queue_level(0..=position).into_iter().fold(None, |_, (prio, range)| {
-                self.queue[prio.index()].drain(range).last()
-            })
-        })
-        .flatten()
-    }
-
-    /// Shuffle the elemenst in the queue, the kara won't change priorities.
-    write fn queue_shuffle(&self, range: impl RangeBounds<usize> + Copy) {
-        self.split_range_per_queue_level(range).into_iter().for_each(|(prio, range)| {
-            self.queue[prio.index()][range]
-                .shuffle(&mut thread_rng());
-        });
-    }
-
-    /// Shuffle the elemenst in the level of the queue.
-    write fn queue_shuffle_level(&self, level: Priority) {
-        self.queue[level.index()]
-            .shuffle(&mut thread_rng());
-    }
-
-    /// Get the number of element in the range. Handles the out of bound stuff.
-    read fn queue_range_count(&self, range: impl RangeBounds<usize> + Copy) -> usize {
-        let (start, end) = (match range.start_bound() {
-            std::ops::Bound::Included(pos) => *pos,
-            std::ops::Bound::Excluded(pos) => pos + 1,
-            std::ops::Bound::Unbounded => 0,
-        }, match range.end_bound() {
-            std::ops::Bound::Included(pos) => *pos,
-            std::ops::Bound::Excluded(pos) => pos - 1,
-            std::ops::Bound::Unbounded => self.queue_count(),
-        });
-        end - start
-    }
-
-    /// Move a kara after a position.
-    write fn queue_move_after(&self, range: impl RangeBounds<usize> + Copy, after: usize) {
-        if !range.contains(&after) {
-            let start = match range.start_bound() {
-                std::ops::Bound::Included(start) => *start,
-                std::ops::Bound::Excluded(start) => start - 1,
-                std::ops::Bound::Unbounded => 0,
-            };
-            let mut after = if after < start { after } else { after - start };
-            let count = self.queue_range_count(range);
-            let ids = self.queue_delete_get(range).into_iter().map(|(_, id)| id);
-            for lvl in self.queue.iter_mut().rev() {
-                if after > lvl.len() {
-                    after -= lvl.len();
-                    continue;
-                } else {
-                    lvl.reserve(count);
-                    ids.rev().for_each(|id| lvl.insert(after, id));
-                    break;
-                }
-            }
-        } else {
-            log::error!("can't move a range after a position included in it");
-        }
-    }
-
-    /// Remove elements from a level of the queue.
-    write fn queue_delete_level(&self, level: Priority) {
-        self.queue[level.index()].clear()
-    }
-
-    /// Remove elements from the queue by their position.
-    write fn queue_delete(&self, range: impl RangeBounds<usize> + Copy) {
-        self.split_range_per_queue_level(range).into_iter().for_each(|(prio, range)| {
-            self.queue[prio.index()].drain(range);
-        })
-    }
-
-    /// Remove elements from the queue by their position and returns what was deleted.
-    write fn queue_delete_get(&self, range: impl RangeBounds<usize> + Copy) -> Vec<(Priority, KId)> {
-        self.split_range_per_queue_level(range).into_iter().flat_map(|(prio, range)| {
-            self.queue[prio.index()]
-                .drain(range)
-                .map(move |id| (prio, id))
-                .collect::<Vec<_>>()
-        }).collect()
-    }
-
-    /// Get the content of the level of the queue.
-    read fn queue_get_level(&self, prio: Priority) -> Vec<KId> {
-        self.queue[prio.index()].clone()
-    }
-
-    /// Iterate over the queue.
-    read fn queue(&self, range: impl RangeBounds<usize> + Copy) -> Vec<(Priority, KId)> {
-        let iter = self.queue.iter().enumerate().map(|(idx, list)| {
-            list.iter().map(move |id| (Priority::from(idx + 1), id.clone()))
-        }).rev().flatten();
-        filter_range(iter, range).collect::<Vec<_>>()
-    }
-
-    /// Returns the number of karas present in the queue.
-    read fn queue_count(&self) -> usize {
-        self.queue.iter().map(|lvl| lvl.len()).sum()
-    }
-
-    /// Returns the number of elements in each level of the queue
-    read fn queue_count_per_level(&self) -> [usize; PRIORITY_LENGTH] {
-        [ self.queue[Priority::Add    .index()].len()
-        , self.queue[Priority::Suggest.index()].len()
-        , self.queue[Priority::Insert .index()].len()
-        , self.queue[Priority::Enforce.index()].len()
-        ]
-    }
-
-    /// Insert something into one priority of the queue.
-    write fn insert(&self, prio: Priority, kara: KId) {
-        self.queue[prio.index()].insert(0, kara);
-    }
-
-    /// Insert something into one priority of the queue.
-    write fn insert_slice(&self, prio: Priority, kara: &[KId]) {
-        let level = &mut self.queue[prio.index()];
-        level.reserve(kara.len());
-        kara.iter().rev().for_each(|id| level.insert(0, id.clone()));
-    }
-
-    /// Insert something into one priority of the queue.
-    write fn insert_all(&self, prio: Priority, kara: Vec<KId>) {
-        let level = &mut self.queue[prio.index()];
-        level.reserve(kara.len());
-        kara.into_iter().rev().for_each(|id| level.insert(0, id.clone()));
-    }
-
-    /// Returns the number of karas present in the history.
-    read fn history_count(&self) -> usize {
-        self.history.len()
-    }
-
-    /// Remove elements from the history by their position.
-    write fn history_delete(&self, range: impl RangeBounds<usize> + Copy) -> Vec<KId> {
-        use std::ops::Bound::{*, self};
-        let count = self.history_count();
-        let inverse = |bound: Bound<&usize>| match bound {
-            Included(idx) => Included(count - idx - 1),
-            Excluded(idx) => Excluded(count - idx - 1),
-            Unbounded => Unbounded,
-        };
-        let range = BoundedBoundRange::from((
-            inverse(range.start_bound()),
-            inverse(range.end_bound())
-        ));
-        self.history.drain(range).collect()
-    }
-
-    /// Iterate over the history. We iterate from the most recent one to the most old one.
-    read fn history(&self, range: impl RangeBounds<usize> + Copy) -> Vec<KId> {
-        filter_range(self.history.iter().rev(), range).cloned().collect()
-    }
-
-    /// Get the next kara to play from the queue and insert the old-playing one into the history.
-    /// The returned kara is the new current one.
-    write fn next(&self) -> Option<KId> {
-        let mut iter = self.queue.iter_mut().rev();
-        let current = iter.find_map(|lvl| (!lvl.is_empty()).then(|| lvl.remove(0)))?;
-        if let Some(current) = self.current.replace(current.clone()) {
-            self.history.push(current);
-        }
-        Some(current)
-    }
-
-    /// Get the previous kara to play from the history and insert the old-playing one into the
-    /// queue at the same priority as the next one. The returned kara is the new current one. If
-    /// the queue is empty we add as the default priority.
-    write fn previous(&self) -> Option<KId> {
-        let current = self.history.pop()?;
-        if let Some(current) = self.current.replace(current.clone()) {
-            match self.queue.iter_mut().rev().find(|lvl| !lvl.is_empty()) {
-                Some(lvl) => lvl.insert(0, current),
-                None => self.insert(Default::default(), current),
-            }
-        }
-        Some(current)
-    }
-
-    /// Delete all karas by and their id in the queue
-    write fn queue_delete_all(&self, id: KId) {
-        self.queue.iter_mut().for_each(|lvl| lvl.retain(|kid| id.ne(kid)))
-    }
-
-    /// Delete all karas by and their id from a level in the queue
-    write fn queue_delete_all_from_level(&self, prio: Priority, id: KId) {
-        self.queue[prio.index()].retain(|kid| id.ne(kid))
-    }
-
-    /// Delete all karas by and their id in the history
-    write fn history_delete_all(&self, id: KId) {
-        self.history.retain(|kid| id.ne(kid))
-    }
-}
diff --git a/lektor_nkdb/src/search/mod.rs b/lektor_nkdb/src/search/mod.rs
index d93ecd57..6c7b416b 100644
--- a/lektor_nkdb/src/search/mod.rs
+++ b/lektor_nkdb/src/search/mod.rs
@@ -5,7 +5,7 @@ mod kara_by;
 
 pub use kara_by::*;
 
-use crate::{playlist::PlaylistName, Kara};
+use crate::{KId, Kara};
 use anyhow::Result;
 use kurisu_api::v2::{SongOrigin, SongType};
 use regex::Regex;
@@ -17,7 +17,7 @@ pub enum SearchFrom {
     Queue,
     Database,
     History,
-    Playlist(PlaylistName),
+    Playlist(KId),
 }
 
 /// Structure used to tell how to do the search, either by a regex, or by applying another way
@@ -62,7 +62,7 @@ impl SearchBy {
     pub(crate) fn matches(&self, kara: &Kara) -> bool {
         match &self {
             SearchBy::Query(regex) => regex.is_match(&kara.to_title_string()),
-            SearchBy::Id(id) => kara.id.local_id().eq(id),
+            SearchBy::Id(id) => kara.id == *id,
             SearchBy::SongType(ty) => kara.song_type.eq(ty),
             SearchBy::SongOrigin(ori) => kara.song_origin.eq(ori),
             SearchBy::Author(author) => kara.kara_makers.contains(author.as_str()),
diff --git a/lektor_nkdb/src/storage/disk_storage.rs b/lektor_nkdb/src/storage/disk_storage.rs
index 85543be1..7917e517 100644
--- a/lektor_nkdb/src/storage/disk_storage.rs
+++ b/lektor_nkdb/src/storage/disk_storage.rs
@@ -1,7 +1,7 @@
 //! We store things in the disk.
 
 use crate::*;
-use anyhow::{bail, Context, Result};
+use anyhow::{bail, ensure, Result};
 use futures::{
     stream::{self, FuturesUnordered},
     StreamExt,
@@ -11,7 +11,8 @@ use kurisu_api::SHA256;
 use regex::Regex;
 use std::{
     path::{Path, PathBuf},
-    sync::{atomic::AtomicU64, Arc, LazyLock},
+    str::FromStr,
+    sync::{atomic::AtomicU64, LazyLock},
 };
 use tokio::{
     fs::{create_dir_all, read, read_dir, write, OpenOptions},
@@ -42,8 +43,8 @@ pub struct DatabaseDiskStorage {
 }
 
 enum PlaylistWriteEvent {
-    Write(Arc<str>, Playlist),
-    Delete(Arc<str>),
+    Write(KId, Playlist),
+    Delete(KId),
 }
 
 macro_rules! regex {
@@ -52,26 +53,16 @@ macro_rules! regex {
     };
 }
 
-fn get_regex_epoch_ok() -> &'static Regex {
+fn get_regex_ok() -> &'static Regex {
     static REGEX: LazyLock<Regex> = regex!(r"^([0123456789]+).ok$");
     &REGEX
 }
 
-fn get_regex_epoch_json() -> &'static Regex {
+fn get_regex_json() -> &'static Regex {
     static REGEX: LazyLock<Regex> = regex!(r"^([0123456789]+).json$");
     &REGEX
 }
 
-fn get_regex_playlist_ok() -> &'static Regex {
-    static REGEX: LazyLock<Regex> = regex!(r"^([a-zA-Z0123456789]+).ok$");
-    &REGEX
-}
-
-fn get_regex_playlist_json() -> &'static Regex {
-    static REGEX: LazyLock<Regex> = regex!(r"^([a-zA-Z0123456789]+).json$");
-    &REGEX
-}
-
 impl DatabaseDiskStorage {
     // Get a path from the root. Note that it's not really safe as something can be crafter to add
     // `/../` strings to escape the root...
@@ -88,7 +79,7 @@ impl DatabaseDiskStorage {
     /// Create a new kara file with a specific extension. If a file with the same path exists it
     /// will be overwritten.
     async fn new_kara_file(&self, id: KId) -> Result<(PathBuf, tokio::fs::File)> {
-        let path = self.path_from_root(format!("data/{}.mkv", id.path()));
+        let path = self.path_from_root(format!("data/{id}.mkv"));
         if path.exists() {
             log::warn!("overwriting file {}", path.to_string_lossy())
         }
@@ -189,29 +180,25 @@ impl DatabaseDiskStorage {
     async fn pool_playlist_write_events(prefix: PathBuf, mut recv: Receiver<PlaylistWriteEvent>) {
         let prefix = prefix.as_path();
         let write = |name, plt| async move {
-            let data = match serde_json::to_string(&plt) {
-                Ok(data) => data,
-                Err(err) => {
-                    log::error!("failed to serialize playlist {name} to write it to disk: {err:?}");
-                    return;
+            match serde_json::to_string(&plt) {
+                Ok(data) => {
+                    if let Err(err) = Self::write_json_from_root(prefix, &name, data).await {
+                        log::error!("can't write playlist {name}: {err:?}")
+                    }
                 }
+                Err(e) => log::error!("failed to write playlist {name} to disk: {e:?}"),
             };
-            if let Err(err) = Self::write_json_from_root(prefix, &name, data).await {
-                log::error!("can't write playlist {name}: {err:?}")
-            }
         };
-        let delete = |name: Arc<str>| async move {
-            let mut path = prefix.join(name.as_ref());
-            path.set_extension("json");
-            if let Err(err) = tokio::fs::remove_file(path).await {
-                log::error!("failed to delete playlist {name}: {err:?}")
+        let delete = |id: KId| async move {
+            if let Err(err) = tokio::fs::remove_file(prefix.join(format!("{id}.json"))).await {
+                log::error!("failed to delete playlist {id}: {err:?}")
             }
         };
 
         while let Some(event) = recv.recv().await {
             match event {
-                PlaylistWriteEvent::Write(name, plt) => write(name, plt).await,
-                PlaylistWriteEvent::Delete(name) => delete(name).await,
+                PlaylistWriteEvent::Write(id, plt) => write(id, plt).await,
+                PlaylistWriteEvent::Delete(id) => delete(id).await,
             }
         }
     }
@@ -241,10 +228,8 @@ macro_rules! read_json_folder {
                     continue;
                 };
                 match what.as_str().parse() {
-                    Err(err) => anyhow::bail!("invalid regex, problem with file: {name}: {err}"),
-                    Ok(name) => {
-                        sets[i].insert(name);
-                    }
+                    Err(err) => bail!("invalid regex, problem with file: {name}: {err}"),
+                    Ok(name) => _ = sets[i].insert(name),
                 }
             }
         }
@@ -256,27 +241,24 @@ macro_rules! read_json_folder {
     };
 }
 
-#[async_trait::async_trait]
 impl DatabaseStorage for DatabaseDiskStorage {
     type Prefix = PathBuf;
     type File = (PathBuf, tokio::fs::File);
 
     async fn load_from_prefix(prefix: PathBuf) -> Result<Self> {
-        let regex = get_regex_epoch_ok();
+        let regex = get_regex_ok();
 
         // Prepare folders.
         for folder in ["data", "epoch", "playlists"] {
             let folder = prefix.join(folder);
             match create_dir_all(&folder).await {
                 Err(err) if err.kind() != std::io::ErrorKind::AlreadyExists => {
-                    return Err(err).with_context(|| {
-                        format!("failed to create folder {}", folder.to_string_lossy())
-                    })
+                    return Err(err)
+                        .with_context(|| format!("failed to create folder {}", folder.display()))
                 }
                 _ => {}
             }
-            let file = folder.join(".touch");
-            write(&file, chrono::Local::now().to_string()).await?;
+            write(folder.join(".touch"), chrono::Local::now().to_string()).await?;
         }
 
         // We find the maximal epoch number for this computer, to be sure to not override any
@@ -319,26 +301,29 @@ impl DatabaseStorage for DatabaseDiskStorage {
 
     async fn read_last_epoch(&self) -> Result<Option<(EpochData, u64)>> {
         read_json_folder! {
-            (self) "epoch" [get_regex_epoch_ok(), get_regex_epoch_json()] => u64;
+            (self) "epoch" [get_regex_ok(), get_regex_json()] => u64;
             valid_epochs => { match valid_epochs.into_iter().max() {
-                None => {
-                    log::warn!("no epoch to load, new install?");
-                    Ok(None)
-                }
+                None => Ok(None),
                 Some(id) => {
                     let path = self.path_from_root(format!("epoch/{id}.json"));
                     let mut data = serde_json::from_slice::<EpochData>(&read(&path).await?)?;
-                    stream::iter(data.keys()).then(|id| async move {
-                        let hash = id.path();
-                        let path = self.path_from_root(format!("data/{hash}.mkv"));
-                        Ok::<_, anyhow::Error>(sha256::try_async_digest(&path).await?.ne(hash).then_some(id.clone()))
+                    stream::iter(data.iter()).then(|(id, kara): (&KId, &Kara)| async move {
+                        let KaraStatus::Physical { hash, .. } = &kara.kara_status else {
+                            return None;
+                        };
+                        match sha256::try_async_digest(self.path_from_root(format!("data/{id}.mkv"))).await
+                            .context("failed to digest file")
+                            .map(|str| SHA256::from_str(&str).map_err(|err| anyhow!("{err}")))
+                        {
+                            Ok(Ok(file_hash)) => (file_hash != *hash).then_some(*id),
+                            Ok(Err(err)) | Err(err) => {
+                                log::error!("kara with id {id} is corrupted: {err}");
+                                Some(*id)
+                            }
+                        }
                     })
-                    .collect::<FuturesUnordered<_>>().await.into_iter()
-                    .filter_map(|res| res.map_err(|err| log::error!("{err}")).ok()).flatten()
-                    .for_each(|kid| {
-                        log::warn!("kara {kid} from epoch {id} is corrupted, remove it from epoch");
-                        data.remove(&kid);
-                    });
+                    .collect::<FuturesUnordered<_>>().await
+                    .into_iter().flatten().for_each(|kid: KId| { data.remove(&kid); });
                     log::info!("load epoch {id} with {} karas from path: {}", data.len(), path.to_string_lossy());
                     Ok(Some((data, *id)))
                 }
@@ -346,15 +331,16 @@ impl DatabaseStorage for DatabaseDiskStorage {
         }
     }
 
-    async fn read_playlists(&self) -> Result<Vec<(PlaylistName, Playlist)>> {
+    async fn read_playlists(&self) -> Result<Vec<Playlist>> {
         read_json_folder! {
-            (self) "playlists" [get_regex_playlist_ok(), get_regex_playlist_json()] => PlaylistName;
+            (self) "playlists" [get_regex_ok(), get_regex_json()] => KId;
             valid_playlists => {
-                stream::iter(valid_playlists).then(|name: &PlaylistName| async move {
-                    let path = self.path_from_root(format!("playlists/{name}.json"));
+                stream::iter(valid_playlists).then(|id: &KId| async move {
+                    let path = self.path_from_root(format!("playlists/{id}.json"));
                     log::info!("load playlist from path: {}", path.to_string_lossy());
                     let json: Playlist = serde_json::from_slice(&read(path).await?)?;
-                    Ok((name.clone(), json))
+                    ensure!(json.local_id() == *id, "");
+                    Ok(json)
                 })
                 .collect::<FuturesUnordered<_>>().await
                 .into_iter().collect::<Result<Vec<_>, _>>()
@@ -370,16 +356,21 @@ impl DatabaseStorage for DatabaseDiskStorage {
         Ok(())
     }
 
-    async fn write_playlist(&self, name: impl AsRef<str> + Send, data: &Playlist) -> Result<()> {
-        let event = PlaylistWriteEvent::Write(name.as_ref().into(), data.clone());
-        self.playlist_pipeline.send(event).await?;
-        Ok(())
+    async fn write_playlist(&self, playlist: &Playlist) -> Result<()> {
+        self.playlist_pipeline
+            .send(PlaylistWriteEvent::Write(
+                playlist.local_id(),
+                playlist.clone(),
+            ))
+            .await
+            .context("failed to send event")
     }
 
-    async fn delete_playlist(&self, name: impl AsRef<str> + Send) -> Result<()> {
-        let event = PlaylistWriteEvent::Delete(name.as_ref().into());
-        self.playlist_pipeline.send(event).await?;
-        Ok(())
+    async fn delete_playlist(&self, id: KId) -> Result<()> {
+        self.playlist_pipeline
+            .send(PlaylistWriteEvent::Delete(id))
+            .await
+            .context("failed to send event")
     }
 
     async fn prepare_kara(&self, id: KId) -> Result<Self::File> {
@@ -392,17 +383,19 @@ impl DatabaseStorage for DatabaseDiskStorage {
     }
 
     async fn submit_kara(&self, (path, _): Self::File, id: KId, hash: SHA256) -> Result<()> {
-        let digest = SHA256::try_from(sha256::try_async_digest(&path).await?).unwrap();
-        let path = path.display();
-        if digest != hash {
-            bail!("invalid digest for {path}, expected {hash}, got {digest}",)
-        }
-        log::info!("finished writing file for kara {id:?}: {path}");
+        let digest = SHA256::try_from(sha256::try_async_digest(&path).await?)
+            .map_err(|err| anyhow!("invalid sha256 computed: {err}"))?;
+        ensure!(
+            digest == hash,
+            "invalid digest for {}, expected {hash}, got {digest}",
+            path.display()
+        );
+        log::info!("finished writing file for kara {id:?}: {}", path.display());
         Ok(())
     }
 
     fn get_kara_uri(&self, id: KId) -> Result<url::Url> {
-        let path = self.path_from_root(format!("data/{}.mkv", id.path()));
+        let path = self.path_from_root(format!("data/{id}.mkv"));
         Ok(url::Url::parse(&format!("file://{}", path.display()))?)
     }
 }
diff --git a/lektor_nkdb/src/storage/mod.rs b/lektor_nkdb/src/storage/mod.rs
index e4348092..ad841439 100644
--- a/lektor_nkdb/src/storage/mod.rs
+++ b/lektor_nkdb/src/storage/mod.rs
@@ -12,12 +12,13 @@ pub use disk_storage::DatabaseDiskStorage;
 #[cfg(test)]
 mod test_storage;
 #[cfg(test)]
+#[allow(unused_imports)]
 pub use test_storage::DatabaseTestStorage;
 
 /// Describes needed interactions with the database storage. All interactions should only be done
 /// throu this trait. The [url::Url] returned by the [Self::get_kara_uri] function should be usable by mpv to
 /// read the file (file://, http://, etc...)
-#[async_trait::async_trait]
+#[allow(async_fn_in_trait)]
 pub trait DatabaseStorage: Sized + std::fmt::Debug {
     /// The type of prefix to use for this loaded.
     type Prefix;
@@ -44,13 +45,13 @@ pub trait DatabaseStorage: Sized + std::fmt::Debug {
     // =============== //
 
     /// Read the playlists from the storage.
-    async fn read_playlists(&self) -> Result<Vec<(PlaylistName, Playlist)>>;
+    async fn read_playlists(&self) -> Result<Vec<Playlist>>;
 
     /// Write a playlist to the storage.
-    async fn write_playlist(&self, name: impl AsRef<str> + Send, data: &Playlist) -> Result<()>;
+    async fn write_playlist(&self, playlist: &Playlist) -> Result<()>;
 
     /// Delete a playlist.
-    async fn delete_playlist(&self, name: impl AsRef<str> + Send) -> Result<()>;
+    async fn delete_playlist(&self, id: KId) -> Result<()>;
 
     // ========== //
     // Kara stuff //
diff --git a/lektor_nkdb/src/storage/test_storage.rs b/lektor_nkdb/src/storage/test_storage.rs
index 49b41847..b9e30c60 100644
--- a/lektor_nkdb/src/storage/test_storage.rs
+++ b/lektor_nkdb/src/storage/test_storage.rs
@@ -2,9 +2,14 @@
 //! tests and we don't depend on the fs to do things... Note that the [Uri] returned by this
 //! storage are in `void://` which is not something usable!
 
-use crate::{database::*, storage::DatabaseStorage, Playlist, PlaylistName};
+use crate::{
+    database::{epoch::*, kara::*},
+    id::*,
+    storage::DatabaseStorage,
+    Playlist,
+};
 use anyhow::{Context, Result};
-use futures::future::join_all;
+use futures::future;
 use hashbrown::HashMap;
 use kurisu_api::SHA256;
 use tokio::sync::RwLock;
@@ -14,7 +19,6 @@ use tokio::sync::RwLock;
 #[derive(Debug)]
 pub struct DatabaseTestStorage(RwLock<HashMap<KId, Kara>>);
 
-#[async_trait::async_trait]
 impl DatabaseStorage for DatabaseTestStorage {
     type Prefix = ();
     type File = ();
@@ -28,22 +32,22 @@ impl DatabaseStorage for DatabaseTestStorage {
     }
 
     async fn write_epoch(&self, data: &EpochData) -> Result<()> {
-        join_all(data.iter().map(|(id, kara)| async {
-            self.0.write().await.insert(id.clone(), (*kara).clone());
+        future::join_all(data.iter().map(|(id, kara)| async {
+            self.0.write().await.insert(*id, kara.clone());
         }))
         .await;
         Ok(())
     }
 
-    async fn read_playlists(&self) -> Result<Vec<(PlaylistName, Playlist)>> {
+    async fn read_playlists(&self) -> Result<Vec<Playlist>> {
         unimplemented!()
     }
 
-    async fn write_playlist(&self, _: impl AsRef<str> + Send, _: &Playlist) -> Result<()> {
+    async fn write_playlist(&self, _: &Playlist) -> Result<()> {
         Ok(())
     }
 
-    async fn delete_playlist(&self, _: impl AsRef<str> + Send) -> Result<()> {
+    async fn delete_playlist(&self, _: KId) -> Result<()> {
         Ok(())
     }
 
@@ -60,7 +64,6 @@ impl DatabaseStorage for DatabaseTestStorage {
     }
 
     fn get_kara_uri(&self, id: KId) -> Result<url::Url> {
-        url::Url::parse(&format!("void://{}", id.path()))
-            .with_context(|| format!("invalid url void://{}", id.path()))
+        url::Url::parse(&format!("void://{id}")).with_context(|| format!("invalid url void://{id}"))
     }
 }
diff --git a/lektor_nkdb/src/strings.rs b/lektor_nkdb/src/strings.rs
new file mode 100644
index 00000000..d5892eeb
--- /dev/null
+++ b/lektor_nkdb/src/strings.rs
@@ -0,0 +1,34 @@
+//! Contains things to store and cache strings to reduce memory footprint and have simpler
+//! comparaisons: string comparaisons will become pointer —e.g. usize— comparaisons.
+
+use hashbrown::HashSet;
+use std::sync::{Arc, LazyLock};
+use tokio::sync::RwLock;
+
+pub(crate) static CACHE: LazyLock<StringCache> = LazyLock::new(StringCache::default);
+
+/// The string cache. Used to cache strings as [Arc<str>] to reduce memory footprint.
+#[derive(Default)]
+pub(crate) struct StringCache(RwLock<HashSet<Arc<str>>>);
+
+impl StringCache {
+    /// Get a string, in an async way.
+    pub async fn get(&self, str: impl AsRef<str>) -> Arc<str> {
+        match self.0.read().await.get(str.as_ref()).cloned() {
+            Some(str) => str,
+            None => (self.0.write().await)
+                .get_or_insert_with(str.as_ref(), |str| Arc::from(str))
+                .clone(),
+        }
+    }
+
+    /// Get a string, in a sync way.
+    pub fn get_sync(&self, str: impl AsRef<str>) -> Arc<str> {
+        match self.0.blocking_read().get(str.as_ref()).cloned() {
+            Some(str) => str,
+            None => (self.0.blocking_write())
+                .get_or_insert_with(str.as_ref(), |str| Arc::from(str))
+                .clone(),
+        }
+    }
+}
diff --git a/lektor_payloads/Cargo.toml b/lektor_payloads/Cargo.toml
index 315a1e2e..bfebc835 100644
--- a/lektor_payloads/Cargo.toml
+++ b/lektor_payloads/Cargo.toml
@@ -1,21 +1,24 @@
 [package]
 name = "lektor_payloads"
+description = "The payloads of the requests used to exchange messages between lektord and its client, this is a utility crate, you can build your own thing because they are all serialized to json under the hood"
+
+rust-version.workspace = true
+
 version.workspace = true
 edition.workspace = true
 authors.workspace = true
 license.workspace = true
-rust-version.workspace = true
-description = "The payloads of the requests used to exchange messages between lektord and its client, this is a utility crate, you can build your own thing because they are all serialized to json under the hood"
 
 [dependencies]
-serde.workspace = true
+serde.workspace      = true
 serde_json.workspace = true
 
 anyhow.workspace = true
 
-futures.workspace = true
-axum.workspace = true
+futures.workspace     = true
+axum.workspace        = true
 async-trait.workspace = true
 
-lektor_nkdb = { path = "../lektor_nkdb" }
-lektor_utils = { path = "../lektor_utils" }
+lektor_nkdb.workspace       = true
+lektor_utils.workspace      = true
+lektor_procmacros.workspace = true
diff --git a/lektor_payloads/src/lib.rs b/lektor_payloads/src/lib.rs
index 3ad79de1..8825f98a 100644
--- a/lektor_payloads/src/lib.rs
+++ b/lektor_payloads/src/lib.rs
@@ -1,15 +1,22 @@
 //! Crate containing structs/enums that are used as payloads to communicate with the lektord
 //! daemon. Some things are re-exports.
 
-mod playlist_name;
+mod play_state;
+mod priority;
 mod range;
 mod search;
 mod userid;
 
-pub use crate::{playlist_name::*, range::*, search::*, userid::LektorUser};
+pub use crate::{
+    priority::{Priority, PRIORITY_LENGTH, PRIORITY_VALUES},
+    play_state::PlayState,
+    range::*,
+    search::*,
+    userid::LektorUser,
+};
 pub use lektor_nkdb::{
-    KId, Kara, KaraBy, KaraStatus, KaraTimeStamps, PlayState, Playlist, PlaylistInfo, Priority,
-    RemoteKId, SearchFrom, SongOrigin, SongType, PRIORITY_LENGTH, PRIORITY_VALUES,
+    KId, Kara, KaraBy, KaraStatus, KaraTimeStamps, Playlist, PlaylistInfo, RemoteKId, SearchFrom,
+    SongOrigin, SongType,
 };
 
 use anyhow::{anyhow, ensure};
@@ -73,7 +80,7 @@ pub enum PlaylistUpdateAction {
     GiveTo(Box<str>),
 
     /// Rename the playlist.
-    Rename(PlaylistName),
+    Rename(String),
 
     /// Add a kara to the playlist.
     Add(KaraFilter),
diff --git a/lektor_payloads/src/play_state.rs b/lektor_payloads/src/play_state.rs
new file mode 100644
index 00000000..2b2b2571
--- /dev/null
+++ b/lektor_payloads/src/play_state.rs
@@ -0,0 +1,23 @@
+//! The playstate of the player.
+
+use serde::{Deserialize, Serialize};
+
+/// Play state of the player.
+#[derive(Debug, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Default)]
+#[serde(rename_all = "lowercase")]
+pub enum PlayState {
+    #[default]
+    Stop = 0,
+    Play = 1,
+    Pause = 2,
+}
+
+impl AsRef<str> for PlayState {
+    fn as_ref(&self) -> &str {
+        match self {
+            PlayState::Stop => "stopped",
+            PlayState::Play => "play",
+            PlayState::Pause => "paused",
+        }
+    }
+}
diff --git a/lektor_payloads/src/playlist_name.rs b/lektor_payloads/src/playlist_name.rs
deleted file mode 100644
index 68c71ca4..00000000
--- a/lektor_payloads/src/playlist_name.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-use anyhow::anyhow;
-use axum::{
-    extract::FromRequestParts,
-    http::{request::Parts, StatusCode},
-};
-use lektor_nkdb::PlaylistName as NKDBPlaylistName;
-use lektor_utils::decode_base64;
-use serde::{Deserialize, Serialize};
-use std::str::FromStr;
-
-/// A playlist name, we excract it from the path. We wrap the thing from the database.
-///
-/// ### Safety
-/// We check before builder the playlist name that the [`u8`] is a valide UTF-8 string.
-#[derive(Debug, PartialEq, Eq, Serialize, Deserialize, Clone, Hash)]
-#[repr(transparent)]
-pub struct PlaylistName(NKDBPlaylistName);
-
-/// The prefix of the playlists requests.
-const PLAYLIST_PREFIX: &str = "/playlist/";
-
-impl AsRef<str> for PlaylistName {
-    fn as_ref(&self) -> &str {
-        self.0.as_ref()
-    }
-}
-
-impl FromStr for PlaylistName {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        NKDBPlaylistName::from_str(s).map(Self)
-    }
-}
-
-impl TryFrom<&str> for PlaylistName {
-    type Error = anyhow::Error;
-
-    fn try_from(value: &str) -> Result<Self, Self::Error> {
-        value.parse()
-    }
-}
-
-impl From<PlaylistName> for NKDBPlaylistName {
-    fn from(value: PlaylistName) -> Self {
-        value.0
-    }
-}
-
-impl From<NKDBPlaylistName> for PlaylistName {
-    fn from(value: NKDBPlaylistName) -> Self {
-        Self(value)
-    }
-}
-
-impl std::fmt::Display for PlaylistName {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        f.write_str(self.as_ref())
-    }
-}
-
-#[async_trait::async_trait]
-impl<S> FromRequestParts<S> for PlaylistName {
-    type Rejection = (StatusCode, String);
-
-    async fn from_request_parts(parts: &mut Parts, _: &S) -> Result<Self, Self::Rejection> {
-        let err = |err: anyhow::Error| (StatusCode::NOT_ACCEPTABLE, err.to_string());
-        let path = parts.uri.path();
-        if !path.starts_with(PLAYLIST_PREFIX) {
-            Err(err(anyhow!("invalid playlist path: {path}")))
-        } else {
-            decode_base64(&path[PLAYLIST_PREFIX.len()..])
-                .map_err(err)?
-                .parse()
-                .map_err(err)
-        }
-    }
-}
diff --git a/lektor_nkdb/src/queue/priority.rs b/lektor_payloads/src/priority.rs
similarity index 98%
rename from lektor_nkdb/src/queue/priority.rs
rename to lektor_payloads/src/priority.rs
index f8cd69ba..1fd1894d 100644
--- a/lektor_nkdb/src/queue/priority.rs
+++ b/lektor_payloads/src/priority.rs
@@ -1,5 +1,5 @@
-use crate::*;
 use lektor_procmacros::{EnumVariantCount, EnumVariantIter};
+use serde::{Deserialize, Serialize};
 use std::str::FromStr;
 
 /// Priorities to insert into the queue. We are one based because that's how humans thinks about
diff --git a/lektor_payloads/src/search.rs b/lektor_payloads/src/search.rs
index bc4567b5..4468c995 100644
--- a/lektor_payloads/src/search.rs
+++ b/lektor_payloads/src/search.rs
@@ -18,7 +18,7 @@ pub enum KaraFilter {
     List(bool, Vec<KId>),
 
     /// The content of a playlist.
-    Playlist(bool, PlaylistName),
+    Playlist(bool, KId),
 }
 
 #[cfg(test)]
diff --git a/lektor_payloads/src/userid.rs b/lektor_payloads/src/userid.rs
index 28ec6f3e..502483c6 100644
--- a/lektor_payloads/src/userid.rs
+++ b/lektor_payloads/src/userid.rs
@@ -1,6 +1,7 @@
 //! Specify who is the person making the request in the header. Extractor for when an
 //! identification is needed or optional.
 
+use anyhow::{anyhow, bail};
 use axum::{
     extract::FromRequestParts,
     http::header,
@@ -17,32 +18,6 @@ pub enum LektorUser {
     Unverified(Box<str>, Box<str>),
 }
 
-pub struct Error(anyhow::Error);
-
-impl std::fmt::Debug for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-impl std::fmt::Display for Error {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.0)
-    }
-}
-
-impl<E: Into<anyhow::Error>> From<E> for Error {
-    fn from(err: E) -> Self {
-        Self(err.into())
-    }
-}
-
-impl IntoResponse for Error {
-    fn into_response(self) -> axum::response::Response {
-        (StatusCode::NOT_ACCEPTABLE, format!("{self}")).into_response()
-    }
-}
-
 impl LektorUser {
     /// Is the user an admin?
     pub fn is_admin(&self) -> bool {
@@ -93,6 +68,51 @@ impl LektorUser {
         let (name, token) = self.into_parts();
         Self::User(name, token)
     }
+
+    /// Returns [Some] user if it is an admin, [None] otherwise.
+    pub fn maybe_admin(self) -> anyhow::Result<Self> {
+        match self {
+            user @ LektorUser::Admin(..) => Ok(user),
+            LektorUser::User(n, _) | LektorUser::Unverified(n, _) => {
+                bail!("user '{n}' is not an admin")
+            }
+        }
+    }
+
+    /// Returns [Some] user if it is an admin or a simple user, [None] otherwise (i.e. the user is
+    /// not authentified.)
+    pub fn maybe_user(self) -> anyhow::Result<Self> {
+        match self {
+            user @ (LektorUser::Admin(..) | LektorUser::User(..)) => Ok(user),
+            LektorUser::Unverified(n, _) => bail!("user '{n}' is not authentified"),
+        }
+    }
+}
+
+pub struct Error(anyhow::Error);
+
+impl std::fmt::Debug for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl std::fmt::Display for Error {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{}", self.0)
+    }
+}
+
+impl<E: Into<anyhow::Error>> From<E> for Error {
+    fn from(err: E) -> Self {
+        Self(err.into())
+    }
+}
+
+impl IntoResponse for Error {
+    fn into_response(self) -> axum::response::Response {
+        (StatusCode::NOT_ACCEPTABLE, format!("{self}")).into_response()
+    }
 }
 
 #[async_trait::async_trait]
@@ -103,11 +123,11 @@ impl<S> FromRequestParts<S> for LektorUser {
         match parts.headers.get(header::AUTHORIZATION) {
             Some(header) => {
                 let Some((name, token)) = header.to_str()?.split_once('=') else {
-                    return Err(anyhow::Error::msg("invalid header").into());
+                    return Err(anyhow!("invalid header").into());
                 };
                 Ok(LektorUser::Unverified(name.into(), token.into()))
             }
-            None => Err(anyhow::Error::msg("headers not found").into()),
+            None => Err(anyhow!("headers not found").into()),
         }
     }
 }
diff --git a/lektor_repo/src/downloader.rs b/lektor_repo/src/downloader.rs
index 87dc8d63..f02ee940 100644
--- a/lektor_repo/src/downloader.rs
+++ b/lektor_repo/src/downloader.rs
@@ -6,7 +6,6 @@ use futures::{
     stream::{self, FuturesUnordered},
     StreamExt,
 };
-use hashbrown::HashSet;
 use kurisu_api::{
     v2::{Infos, Kara},
     SHA256,
@@ -21,32 +20,14 @@ use serde::Deserialize;
 
 pub async fn download_dbinfos(config: &LektorRepoConfig) -> Result<Infos> {
     let name = config.name.as_str();
-    let (infos, epochs) = stream::iter(config.urls.iter().map(get_dbinfos))
+    let infos = stream::iter(config.urls.iter().map(get_dbinfos))
         .then(|url| async { reqwest::get(url).await?.json::<Infos>().await })
         .collect::<FuturesUnordered<_>>()
         .await
         .into_iter()
         .filter_map(|r| r.map_err(|e| log::error!("error for {name}: {e}")).ok())
-        .fold(
-            (Infos::default(), HashSet::new()),
-            |(mut res, mut epochs), info| {
-                epochs.insert(info.dbepoch);
-                res.dbepoch = std::cmp::max(res.dbepoch, info.dbepoch);
-                res.tag_keys.extend(info.tag_keys);
-                res.valueless_tag_keys.extend(info.valueless_tag_keys);
-                (res, epochs)
-            },
-        );
-    if epochs.len() >= 2 {
-        log::warn!("urls for {name} are not in syncs, epochs are: {epochs:?}")
-    }
-    let intersection: Vec<_> = infos
-        .valueless_tag_keys
-        .intersection(&infos.tag_keys)
-        .collect();
-    if !intersection.is_empty() {
-        log::warn!("tags are indicated as valueless and with value: {intersection:?}")
-    }
+        .fold(Infos::default(), |res, info| res.merge(info));
+    infos.warn_on_conflicting_tags();
     Ok(infos)
 }
 
@@ -91,7 +72,6 @@ pub async fn download_kara<Storage: DatabaseStorage>(
             .map(|b| get_kara_file(b, rkid.remote_id())),
     )
     .then(|url| {
-        let kid = kid.clone();
         async move {
             // Build the request
             let request = Request::new(
@@ -116,7 +96,7 @@ pub async fn download_kara<Storage: DatabaseStorage>(
             }
 
             // Download the file
-            let mut file = handle.prepare_kara(kid.clone()).await?;
+            let mut file = handle.prepare_kara(kid).await?;
             while let Some(chunk) = res.chunk().await? {
                 handle.write_kara(&mut file, &chunk).await?;
             }
diff --git a/lektor_repo/src/lib.rs b/lektor_repo/src/lib.rs
index 2d8d6f5e..5e0ccebc 100644
--- a/lektor_repo/src/lib.rs
+++ b/lektor_repo/src/lib.rs
@@ -55,14 +55,10 @@ impl Repo {
                 });
                 stream::iter(list).then(move |kara| async move {
                     log::trace!("got kara from {}: {kara:#?}", cfg.name);
-                    match handler.add_kara_v2(&cfg.name, kara).await {
-                        Ok(Some(to_dl)) => {
-                            if let Err(err) = download_kara(cfg, handler, to_dl).await {
-                                log::error!("{err}");
-                            }
+                    if let Some(to_dl) = handler.add_kara_v2(&cfg.name, kara).await {
+                        if let Err(err) = download_kara(cfg, handler, to_dl).await {
+                            log::error!("{err}");
                         }
-                        Err(err) => log::error!("failed to update kara: {err}"),
-                        _ => {}
                     }
                 })
             })
diff --git a/lektor_utils/src/base64.rs b/lektor_utils/src/base64.rs
index 39e341f6..6e80a103 100644
--- a/lektor_utils/src/base64.rs
+++ b/lektor_utils/src/base64.rs
@@ -1,6 +1,8 @@
+use core::fmt;
+
 use anyhow::Result;
 use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine as _};
-use serde::{Deserialize, Serialize};
+use serde::{ser::Error, Deserialize, Serialize};
 
 /// Re-export of the slice decode error
 pub use base64::EncodeSliceError;
@@ -10,19 +12,37 @@ pub use base64::EncodeSliceError;
 /// playlists urls to 128 ($2^7$) characters and the queue/history is limited to u64 integers.
 pub const BASE64_BUFFER_SIZE: usize = 1024;
 
+/// Struct to wrap the result of [encode_base64] and [encode_base64_value]. Can be displayed and
+/// turn into parts.
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub struct Base64Encoded(usize, [u8; BASE64_BUFFER_SIZE]);
+
+impl Base64Encoded {
+    pub fn into_parts(self) -> (usize, [u8; BASE64_BUFFER_SIZE]) {
+        (self.0, self.1)
+    }
+}
+
+impl fmt::Display for Base64Encoded {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        f.write_str(
+            std::str::from_utf8(&self.1[..self.0]).map_err(|_| {
+                fmt::Error::custom("encoded buffer contains invalid utf8 characters")
+            })?,
+        )
+    }
+}
+
 /// Encode a string as a base64 string and store it into a buffer without allocatig any memory.
-pub fn encode_base64(input: impl AsRef<str>) -> Result<(usize, [u8; BASE64_BUFFER_SIZE])> {
+pub fn encode_base64(input: impl AsRef<str>) -> Result<Base64Encoded> {
     let mut buffer = [0_u8; BASE64_BUFFER_SIZE];
     let len = URL_SAFE_NO_PAD.encode_slice(input.as_ref(), &mut buffer)?;
-    Ok((len, buffer))
+    Ok(Base64Encoded(len, buffer))
 }
 
 /// Serialize to json and encode a value in base64.
-pub fn encode_base64_value(value: impl Serialize) -> Result<(usize, [u8; BASE64_BUFFER_SIZE])> {
-    let json = serde_json::to_string(&value)?;
-    let size = encode_base64(&json)?;
-    log::trace!("encoding json {json}");
-    Ok(size)
+pub fn encode_base64_value(value: impl Serialize) -> Result<Base64Encoded> {
+    encode_base64(&serde_json::to_string(&value)?)
 }
 
 /// Decode a base64 encoded string without allocation then deserialize the json into the approriate
diff --git a/lektord/Cargo.toml b/lektord/Cargo.toml
index 7bbd03c3..9b51010e 100644
--- a/lektord/Cargo.toml
+++ b/lektord/Cargo.toml
@@ -17,7 +17,6 @@ anyhow.workspace = true
 hashbrown.workspace = true
 
 futures.workspace = true
-async-trait.workspace = true
 
 axum.workspace = true
 tokio.workspace = true
diff --git a/lektord/src/app/mod.rs b/lektord/src/app/mod.rs
index 15c32be3..be5a06b9 100644
--- a/lektord/src/app/mod.rs
+++ b/lektord/src/app/mod.rs
@@ -3,10 +3,12 @@
 #![forbid(unsafe_code)]
 
 mod mpris;
+mod play_state;
+mod queue;
 mod routes;
 
 use crate::LektorConfig;
-use anyhow::{anyhow, Context, Result};
+use anyhow::{Context, Result};
 use axum::{
     extract::Request,
     http::{response, StatusCode},
@@ -19,6 +21,8 @@ use lektor_nkdb::{Database, DatabaseDiskStorage};
 use lektor_payloads::LektorUser;
 use lektor_repo::Repo;
 use lektor_utils::config::LektorDatabaseConfig;
+use play_state::StoredPlayState;
+use queue::{Queue, QueueHandle};
 use std::sync::{Arc, Weak};
 use tokio::sync::{oneshot::Sender, RwLock};
 
@@ -79,33 +83,38 @@ pub async fn app(
                // Simple search things
                ; "/search/:b64" -> get: routes::search
                ; "/count/:b64"  -> get: routes::count
-               ; "/get/id/:id"  -> get: routes::get_kara_by_id
                ; "/get/kid/:id" -> get: routes::get_kara_by_kid
 
-               // Queue and history functions. You need to specify the range, like:
-               // `/queue?range=..`
-               ; "/queue"            -> get:    routes::get_queue_range
-                                      , post:   routes::add_to_queue
-                                      , patch:  routes::update_queue_range
-                                      , delete: routes::delete_queue_range
-               ; "/history"          -> get:    routes::get_history_range
-                                      , delete: routes::delete_history_range
-               ; "/queue/count"      -> get:    routes::get_queue_count
-               ; "/history/count"    -> get:    routes::get_history_count
-               ; "/queue/:prio/:kid" -> post:   routes::add_kid_to_queue
-                                      , delete: routes::delete_kid_from_queue
-               ; "/queue/:what"      -> delete: routes::delete_from_queue
-                                      , put:    routes::shuffle_queue_level
-                                      , get:    routes::get_from_queue
-               ; "/history/:kid"     -> delete: routes::delete_kid_from_history
+               // Queue functions. You need to specify the range, like: `/queue?range=..`
+               ; "/queue"                  -> get:    routes::get_queue_range
+                                            , post:   routes::add_to_queue
+                                            , patch:  routes::update_queue_range
+                                            , delete: routes::delete_queue_range
+               ; "/queue/count"            -> get:    routes::get_queue_count
+               ; "/queue/level/:prio/:kid" -> post:   routes::add_kid_to_queue
+                                            , delete: routes::delete_kid_from_queue
+               ; "/queue/level/:prio"      -> get:    routes::get_queue_level
+                                            , put:    routes::shuffle_queue_level
+                                            , delete: routes::delete_queue_level
+               ; "/queue/:kid"             -> delete: routes::delete_from_queue
+                                            , put:    routes::shuffle_queue_level
+                                            , get:    routes::get_from_queue
+
+               // History functions. You need to specify the range, like: `/history?range=..`
+               ; "/history"       -> get:    routes::get_history_range
+                                   , delete: routes::delete_history_range
+               ; "/history/count" -> get:    routes::get_history_count
+               ; "/history/:kid"  -> delete: routes::delete_kid_from_history
 
                // Playlists things, to modify or delete a playlist, the user must be the owner or
                // the super user...
-               ; "/playlist"       -> get:    routes::get_playlists
-               ; "/playlist/:name" -> get:    routes::get_playlist_content
-                                    , patch:  routes::update_playlist
-                                    , delete: routes::delete_playlist
-                                    , put:    routes::create_playlist
+               ; "/playlists"              -> get:    routes::get_playlists
+               ; "/playlist/:name/content" -> get:    routes::get_playlist_content
+               ; "/playlist/:name/info"    -> get:    routes::get_playlist_info
+               ; "/playlist/:name"         -> get:    routes::get_playlist
+                                            , patch:  routes::update_playlist
+                                            , delete: routes::delete_playlist
+                                            , put:    routes::create_playlist
 
                // Admin routes, requires to be the super user to do.
                ; "/adm/update"   -> post: routes::adm_update
@@ -143,7 +152,13 @@ async fn log_requests(request: Request, next: Next) -> Response {
 #[derive(Debug)]
 pub struct LektorState {
     /// The database.
-    pub(crate) database: Database,
+    database: Database,
+
+    /// The queue.
+    queue: Queue,
+
+    /// The playstate of the player.
+    playstate: StoredPlayState,
 
     /// The thing that will handle updates for us.
     pub(crate) repo: Repo,
@@ -188,6 +203,8 @@ impl LektorState {
             repo: Repo::new(repo),
             users: users.collect(),
             mpris: Default::default(),
+            queue: Default::default(),
+            playstate: Default::default(),
             shutdown: RwLock::new(Some(shutdown)),
         }));
         if config.mpris {
@@ -205,7 +222,7 @@ impl LektorState {
     }
 
     /// Check whever the user is valid or not.
-    pub fn verify_user(&self, user: LektorUser) -> LektorUser {
+    fn verify_user(&self, user: LektorUser) -> LektorUser {
         let (name, tok) = user.as_parts();
         for config in &self.users {
             match config {
@@ -217,30 +234,27 @@ impl LektorState {
         user
     }
 
+    /// Get a handle to read or write the queue, alongside the playstate.
+    pub(crate) fn queue(&self) -> QueueHandle {
+        QueueHandle::new(&self.queue, &self.playstate)
+    }
+
     /// Play the next kara in the queue.
-    pub async fn play_next(&self) -> Result<()> {
+    pub(crate) async fn play_next(&self) -> Result<()> {
         let id = self
-            .database
-            .next()
-            .await
-            .ok_or(anyhow!("no next kara to play"))?;
-        crate::c_wrapper::player_load_file(
-            self.database.get_kara_uri(id.clone())?.to_string(),
-            id.local_id(),
-        )
+            .queue()
+            .write(|queue, _| queue.next().context("no next kara to play"))
+            .await?;
+        crate::c_wrapper::player_load_file(self.database.get_kara_uri(id)?.as_str(), id)
     }
 
     /// Play the previous kara in the queue.
-    pub async fn play_previous(&self) -> Result<()> {
+    pub(crate) async fn play_previous(&self) -> Result<()> {
         let id = self
-            .database
-            .previous()
-            .await
-            .ok_or_else(|| anyhow!("no previous kara to play"))?;
-        crate::c_wrapper::player_load_file(
-            self.database.get_kara_uri(id.clone())?.to_string(),
-            id.local_id(),
-        )
+            .queue()
+            .write(|queue, _| queue.previous().context("no next kara to play"))
+            .await?;
+        crate::c_wrapper::player_load_file(self.database.get_kara_uri(id)?.as_str(), id)
     }
 }
 
diff --git a/lektord/src/app/mpris.rs b/lektord/src/app/mpris.rs
index 9f57e52f..b34e9dea 100644
--- a/lektord/src/app/mpris.rs
+++ b/lektord/src/app/mpris.rs
@@ -4,7 +4,7 @@ use lektor_nkdb::Kara;
 use std::sync::Arc;
 
 /// The play state, convertible to the thing liked by MPRIS.
-pub struct PlayState(lektor_nkdb::PlayState);
+pub struct PlayState(lektor_payloads::PlayState);
 
 /// A thing used to convert a [Kara] into track metadatas for MPRIS.
 pub struct TrackMdt(Arc<Kara>);
@@ -95,15 +95,21 @@ impl AsMpris for LektorStateWeakPtr {
     }
 }
 
-impl From<PlayState> for lektor_nkdb::PlayState {
+impl From<PlayState> for lektor_payloads::PlayState {
     fn from(PlayState(value): PlayState) -> Self {
         value
     }
 }
 
+impl From<lektor_payloads::PlayState> for PlayState {
+    fn from(value: lektor_payloads::PlayState) -> Self {
+        Self(value)
+    }
+}
+
 impl From<lektor_mpris::types::PlaybackStatus> for PlayState {
     fn from(value: lektor_mpris::types::PlaybackStatus) -> Self {
-        use {lektor_mpris::types::PlaybackStatus, lektor_nkdb::PlayState};
+        use {lektor_mpris::types::PlaybackStatus, lektor_payloads::PlayState};
         match value {
             PlaybackStatus::Stopped => Self(PlayState::Stop),
             PlaybackStatus::Playing => Self(PlayState::Play),
@@ -115,9 +121,9 @@ impl From<lektor_mpris::types::PlaybackStatus> for PlayState {
 impl From<PlayState> for lektor_mpris::types::PlaybackStatus {
     fn from(PlayState(value): PlayState) -> Self {
         match value {
-            lektor_nkdb::PlayState::Stop => Self::Stopped,
-            lektor_nkdb::PlayState::Play => Self::Playing,
-            lektor_nkdb::PlayState::Pause => Self::Paused,
+            lektor_payloads::PlayState::Stop => Self::Stopped,
+            lektor_payloads::PlayState::Play => Self::Playing,
+            lektor_payloads::PlayState::Pause => Self::Paused,
         }
     }
 }
diff --git a/lektord/src/app/play_state.rs b/lektord/src/app/play_state.rs
new file mode 100644
index 00000000..89c4ed37
--- /dev/null
+++ b/lektord/src/app/play_state.rs
@@ -0,0 +1,26 @@
+//! Contains things to store the playstate for the player, in a way that can be accessed in a
+//! thread-safe maner.
+
+use lektor_payloads::PlayState;
+use std::sync::atomic::{AtomicU8, Ordering};
+
+/// The playstate, but stored in the database. We take into account the fact that it can be
+/// accessed in a parallel context.
+#[derive(Debug, Default)]
+pub struct StoredPlayState {
+    state: AtomicU8,
+}
+
+impl StoredPlayState {
+    pub fn get(&self) -> PlayState {
+        match self.state.load(Ordering::Acquire) {
+            1 => PlayState::Play,
+            2 => PlayState::Pause,
+            _ => PlayState::Stop,
+        }
+    }
+
+    pub fn set(&self, state: PlayState) {
+        self.state.store(state as u8, Ordering::Release);
+    }
+}
diff --git a/lektord/src/app/queue.rs b/lektord/src/app/queue.rs
new file mode 100644
index 00000000..a368cba3
--- /dev/null
+++ b/lektord/src/app/queue.rs
@@ -0,0 +1,307 @@
+use crate::app::play_state::StoredPlayState;
+use lektor_payloads::{KId, PlayState, Priority, PRIORITY_LENGTH};
+use lektor_utils::{filter_range, BoundedBoundRange};
+use rand::{seq::SliceRandom, thread_rng};
+use std::{
+    ops::{RangeBounds, RangeInclusive},
+    sync::atomic::{AtomicU64, Ordering},
+};
+use tokio::sync::RwLock;
+
+/// The queue contains the playing kara and the following ones. This type is just a wrapper around
+/// the [QueueContent] with a [RwLock]. For function documentation see [QueueContent].
+#[derive(Debug, Default)]
+pub(crate) struct Queue {
+    content: RwLock<QueueContent>,
+    epoch: AtomicU64,
+}
+
+pub struct QueueHandle<'a> {
+    queue: &'a Queue,
+    state: &'a StoredPlayState,
+}
+
+impl<'a> QueueHandle<'a> {
+    pub(crate) fn new(queue: &'a Queue, state: &'a StoredPlayState) -> Self {
+        Self { queue, state }
+    }
+
+    /// Read informations about the queue.
+    pub async fn read<T>(&self, cb: impl FnOnce(&QueueContent, PlayState) -> T) -> T {
+        cb(&*self.queue.content.read().await, self.state.get())
+    }
+
+    /// Modifies the queue.
+    pub async fn write<T>(&self, cb: impl FnOnce(&mut QueueContent, &StoredPlayState) -> T) -> T {
+        self.queue.epoch.fetch_add(1, Ordering::AcqRel);
+        cb(&mut *self.queue.content.write().await, self.state)
+    }
+
+    pub fn epoch(&self) -> u64 {
+        self.queue.epoch.load(Ordering::Acquire)
+    }
+}
+
+/// The content of the queue. Is protected by a RwLock from tokio.
+#[derive(Debug, Default)]
+pub struct QueueContent {
+    /// The current kara
+    current: Option<KId>,
+
+    /// The queue, stored as a priority list, stored in reverse order from the priority.
+    queue: [Vec<KId>; PRIORITY_LENGTH],
+
+    /// The history, we push back into it, so the most recent one is the one at the end of the
+    /// vector.
+    history: Vec<KId>,
+}
+
+impl QueueContent {
+    /// Get the current kara if it exists.
+    pub fn current(&self) -> Option<KId> {
+        self.current
+    }
+
+    pub fn split_range_per_level(
+        &self,
+        range: impl RangeBounds<usize>,
+    ) -> Vec<(Priority, RangeInclusive<usize>)> {
+        let (mut start, mut end) = (
+            match range.start_bound() {
+                std::ops::Bound::Included(pos) => *pos,
+                std::ops::Bound::Excluded(pos) => pos + 1,
+                std::ops::Bound::Unbounded => 0,
+            },
+            match range.end_bound() {
+                std::ops::Bound::Included(pos) => *pos,
+                std::ops::Bound::Excluded(pos) => pos - 1,
+                std::ops::Bound::Unbounded => self.count(),
+            },
+        );
+        self.queue
+            .iter()
+            .enumerate()
+            .rev()
+            .flat_map(|(idx, lvl)| {
+                let ret = (start <= lvl.len())
+                    .then(|| (Priority::from(idx + 1), start..=(lvl.len() - 1).min(end)));
+                (start, end) = (start - lvl.len(), end - lvl.len());
+                ret
+            })
+            .collect()
+    }
+
+    /// Clear the queue up to the position specified, returning it into the current kara.
+    pub fn play_from_position(&mut self, position: usize) -> Option<KId> {
+        (position < self.count())
+            .then(|| {
+                self.split_range_per_level(0..=position)
+                    .into_iter()
+                    .fold(None, |_, (prio, range)| {
+                        self.queue[prio.index()].drain(range).last()
+                    })
+            })
+            .flatten()
+    }
+
+    /// Shuffle the elemenst in the queue, the kara won't change priorities.
+    pub fn shuffle(&mut self, range: impl RangeBounds<usize> + Copy) {
+        self.split_range_per_level(range)
+            .into_iter()
+            .for_each(|(prio, range)| {
+                self.queue[prio.index()][range].shuffle(&mut thread_rng());
+            });
+    }
+
+    /// Shuffle the elemenst in the level of the queue.
+    pub fn shuffle_level(&mut self, level: Priority) {
+        self.queue[level.index()].shuffle(&mut thread_rng());
+    }
+
+    /// Get the number of element in the range. Handles the out of bound stuff.
+    pub fn range_count(&self, range: impl RangeBounds<usize> + Copy) -> usize {
+        let (start, end) = (
+            match range.start_bound() {
+                std::ops::Bound::Included(pos) => *pos,
+                std::ops::Bound::Excluded(pos) => pos + 1,
+                std::ops::Bound::Unbounded => 0,
+            },
+            match range.end_bound() {
+                std::ops::Bound::Included(pos) => *pos,
+                std::ops::Bound::Excluded(pos) => pos - 1,
+                std::ops::Bound::Unbounded => self.count(),
+            },
+        );
+        end - start
+    }
+
+    /// Move a kara after a position.
+    pub fn move_after(&mut self, range: impl RangeBounds<usize> + Copy, after: usize) {
+        if !range.contains(&after) {
+            let start = match range.start_bound() {
+                std::ops::Bound::Included(start) => *start,
+                std::ops::Bound::Excluded(start) => start - 1,
+                std::ops::Bound::Unbounded => 0,
+            };
+            let mut after = if after < start { after } else { after - start };
+            let count = self.range_count(range);
+            let ids = self.delete_get(range).into_iter().map(|(_, id)| id);
+            for lvl in self.queue.iter_mut().rev() {
+                if after > lvl.len() {
+                    after -= lvl.len();
+                    continue;
+                } else {
+                    lvl.reserve(count);
+                    ids.rev().for_each(|id| lvl.insert(after, id));
+                    break;
+                }
+            }
+        } else {
+            log::error!("can't move a range after a position included in it");
+        }
+    }
+
+    /// Remove elements from a level of the queue.
+    pub fn delete_level(&mut self, level: Priority) {
+        self.queue[level.index()].clear()
+    }
+
+    /// Remove elements from the queue by their position.
+    pub fn delete(&mut self, range: impl RangeBounds<usize> + Copy) {
+        self.split_range_per_level(range)
+            .into_iter()
+            .for_each(|(prio, range)| {
+                self.queue[prio.index()].drain(range);
+            })
+    }
+
+    /// Remove elements from the queue by their position and returns what was deleted.
+    pub fn delete_get(&mut self, range: impl RangeBounds<usize> + Copy) -> Vec<(Priority, KId)> {
+        self.split_range_per_level(range)
+            .into_iter()
+            .flat_map(|(prio, range)| {
+                self.queue[prio.index()]
+                    .drain(range)
+                    .map(move |id| (prio, id))
+                    .collect::<Vec<_>>()
+            })
+            .collect()
+    }
+
+    /// Get the content of the level of the queue.
+    pub fn get_level(&self, prio: Priority) -> Vec<KId> {
+        self.queue[prio.index()].clone()
+    }
+
+    /// Iterate over the queue.
+    pub fn get(&self, range: impl RangeBounds<usize> + Copy) -> Vec<(Priority, KId)> {
+        let iter = self
+            .queue
+            .iter()
+            .enumerate()
+            .map(|(idx, list)| list.iter().map(move |&id| (Priority::from(idx + 1), id)))
+            .rev()
+            .flatten();
+        filter_range(iter, range).collect::<Vec<_>>()
+    }
+
+    /// Returns the number of karas present in the queue.
+    pub fn count(&self) -> usize {
+        self.queue.iter().map(|lvl| lvl.len()).sum()
+    }
+
+    /// Returns the number of elements in each level of the queue
+    pub fn count_per_level(&self) -> [usize; PRIORITY_LENGTH] {
+        [
+            self.queue[Priority::Add.index()].len(),
+            self.queue[Priority::Suggest.index()].len(),
+            self.queue[Priority::Insert.index()].len(),
+            self.queue[Priority::Enforce.index()].len(),
+        ]
+    }
+
+    /// Insert something into one priority of the queue.
+    pub fn insert(&mut self, prio: Priority, kara: KId) {
+        self.queue[prio.index()].insert(0, kara);
+    }
+
+    /// Insert something into one priority of the queue.
+    pub fn insert_slice(&mut self, prio: Priority, kara: &[KId]) {
+        let level = &mut self.queue[prio.index()];
+        level.reserve(kara.len());
+        kara.iter().rev().for_each(|&id| level.insert(0, id));
+    }
+
+    /// Insert something into one priority of the queue.
+    pub fn insert_all(&mut self, prio: Priority, kara: Vec<KId>) {
+        self.insert_slice(prio, &kara)
+    }
+
+    /// Get the next kara to play from the queue and insert the old-playing one into the history.
+    /// The returned kara is the new current one.
+    pub fn next(&mut self) -> Option<KId> {
+        let mut iter = self.queue.iter_mut().rev();
+        let current = iter.find_map(|lvl| (!lvl.is_empty()).then(|| lvl.remove(0)))?;
+        if let Some(current) = self.current.replace(current) {
+            self.history.push(current);
+        }
+        Some(current)
+    }
+
+    /// Get the previous kara to play from the history and insert the old-playing one into the
+    /// queue at the same priority as the next one. The returned kara is the new current one. If
+    /// the queue is empty we add as the default priority.
+    pub fn previous(&mut self) -> Option<KId> {
+        let current = self.history.pop()?;
+        if let Some(current) = self.current.replace(current) {
+            match self.queue.iter_mut().rev().find(|lvl| !lvl.is_empty()) {
+                Some(lvl) => lvl.insert(0, current),
+                None => self.insert(Default::default(), current),
+            }
+        }
+        Some(current)
+    }
+
+    /// Delete all karas by and their id in the queue
+    pub fn delete_all(&mut self, id: KId) {
+        self.queue
+            .iter_mut()
+            .for_each(|lvl| lvl.retain(|kid| id != *kid))
+    }
+
+    /// Delete all karas by and their id from a level in the queue
+    pub fn delete_all_from_level(&mut self, prio: Priority, id: KId) {
+        self.queue[prio.index()].retain(|kid| id.ne(kid))
+    }
+
+    /// Delete all karas by and their id in the history
+    pub fn history_delete_all(&mut self, id: KId) {
+        self.history.retain(|kid| id.ne(kid))
+    }
+
+    /// Returns the number of karas present in the history.
+    pub fn history_count(&self) -> usize {
+        self.history.len()
+    }
+
+    /// Remove elements from the history by their position.
+    pub fn history_delete(&mut self, range: impl RangeBounds<usize> + Copy) -> Vec<KId> {
+        use std::ops::Bound::{self, *};
+        let count = self.history_count();
+        let inverse = |bound: Bound<&usize>| match bound {
+            Included(idx) => Included(count - idx - 1),
+            Excluded(idx) => Excluded(count - idx - 1),
+            Unbounded => Unbounded,
+        };
+        let range =
+            BoundedBoundRange::from((inverse(range.start_bound()), inverse(range.end_bound())));
+        self.history.drain(range).collect()
+    }
+
+    /// Iterate over the history. We iterate from the most recent one to the most old one.
+    pub fn history(&self, range: impl RangeBounds<usize> + Copy) -> Vec<KId> {
+        filter_range(self.history.iter().rev(), range)
+            .cloned()
+            .collect()
+    }
+}
diff --git a/lektord/src/app/routes.rs b/lektord/src/app/routes.rs
index 7f7becf7..c4f09ecd 100644
--- a/lektord/src/app/routes.rs
+++ b/lektord/src/app/routes.rs
@@ -6,32 +6,41 @@
 #![forbid(unsafe_code)]
 
 use crate::*;
-use anyhow::{anyhow, Error};
+use anyhow::anyhow;
 use axum::{
     extract::{Path, State},
-    http::{header::CONTENT_TYPE, HeaderValue, StatusCode},
+    http::{header::CONTENT_TYPE, HeaderValue},
     response::{IntoResponse, Response},
     Json,
 };
+use futures::prelude::*;
 use lektor_nkdb::*;
 use lektor_payloads::*;
 use lektor_utils::decode_base64_json;
 use rand::{seq::SliceRandom, thread_rng};
-use std::{ops::RangeBounds, str::FromStr};
+use std::ops::RangeBounds;
 use tokio::task::LocalSet;
 
 /// Get informations abount the lektord server.
+#[axum::debug_handler(state = LektorStatePtr)]
 pub(crate) async fn root(State(state): State<LektorStatePtr>) -> Json<Infos> {
     Json(Infos {
         version: crate::version().to_string(),
-        last_epoch: state.database.last_epoch_num().await,
+        last_epoch: state
+            .database
+            .last_epoch()
+            .map(|epoch| epoch.map(Epoch::num))
+            .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> {
-    let (state, current) = state.database.current().await;
+    let (state, current) = state
+        .queue()
+        .read(|content, state| (state, content.current()))
+        .await;
     let current = current.map(|id| {
         let elapsed = c_wrapper::player_get_elapsed()
             .map_err(|err| log::error!("{err}"))
@@ -64,14 +73,11 @@ pub(crate) async fn play_from_queue_pos(
     Path(position): Path<usize>,
 ) -> Result<(), LektordError> {
     let id = state
-        .database
-        .play_from_position(position)
+        .queue()
+        .write(|content, _| content.play_from_position(position))
         .await
-        .ok_or(anyhow!("position {position} doesn't exists in queue"))?;
-    crate::c_wrapper::player_load_file(
-        state.database.get_kara_uri(id.clone())?.to_string(),
-        id.local_id(),
-    )?;
+        .with_context(|| format!("position {position} doesn't exists in queue"))?;
+    crate::c_wrapper::player_load_file(state.database.get_kara_uri(id)?.to_string(), id)?;
     Ok(())
 }
 
@@ -82,17 +88,22 @@ pub(crate) async fn set_play_state(
     Json(to): Json<PlayState>,
 ) -> Result<(), LektordError> {
     if to == PlayState::Stop {
-        Ok(c_wrapper::player_stop()?)
-    } else {
-        match state.database.current().await {
-            (PlayState::Stop, _) if to == PlayState::Play => Ok(state.play_next().await?),
-            (PlayState::Stop, _) => {
-                log::trace!("playback is set to stopped, can't set it to {to:?}");
-                Ok(())
+        c_wrapper::player_stop()?
+    } else if state
+        .queue()
+        .write(|_, state| match state.get() {
+            PlayState::Stop if to == PlayState::Play => Ok(true),
+            stop @ PlayState::Stop => {
+                log::trace!("can't change playback from {stop:?} to {to:?}");
+                Ok(false)
             }
-            _ => Ok(c_wrapper::player_set_paused(to)?),
-        }
+            _ => c_wrapper::player_set_paused(to).map(|_| false),
+        })
+        .await?
+    {
+        state.play_next().await?
     }
+    Ok(())
 }
 
 /// Toggle the play state of the server, Be aware of race conditions...
@@ -101,35 +112,15 @@ pub(crate) async fn toggle_play_state() -> Result<(), LektordError> {
     Ok(crate::c_wrapper::player_toggle_pause()?)
 }
 
-/// Get all the informations about a kara by its id. Returns the kara as a json object to avoid
-/// cloning the kara structure and directly return the serialized object.
-#[axum::debug_handler(state = LektorStatePtr)]
-pub(crate) async fn get_kara_by_id(
-    State(state): State<LektorStatePtr>,
-    Path(id): Path<u64>,
-) -> Result<Response, LektordError> {
-    let mut kara = serde_json::to_string(state.database.get_kara_by_id(id).await?)
-        .map_err(|err| anyhow!("{err}"))?
-        .into_response();
-    kara.headers_mut()
-        .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
-    Ok(kara)
-}
-
 /// Get all the informations about a kara by its id. Returns the kara as a json object to avoid
 /// cloning the kara structure and directly return the serialized object.
 #[axum::debug_handler(state = LektorStatePtr)]
 pub(crate) async fn get_kara_by_kid(
     State(state): State<LektorStatePtr>,
-    Path(id): Path<String>,
+    Path(id): Path<KId>,
 ) -> Result<Response, LektordError> {
-    let id = state
-        .database
-        .get_kid_from_str(&decode_base64(&id)?)
-        .await
-        .ok_or_else(|| anyhow!("no kara found with id {id}"))?;
     let mut kara = serde_json::to_string(state.database.get_kara_by_kid(id).await?)
-        .map_err(|err| anyhow!("{err}"))?
+        .context("failed to find kara with id")?
         .into_response();
     kara.headers_mut()
         .insert(CONTENT_TYPE, HeaderValue::from_static("application/json"));
@@ -144,7 +135,8 @@ pub(crate) async fn search(
     Path(uri): Path<String>,
 ) -> Result<Json<Vec<KId>>, LektordError> {
     let SearchData { from, regex } = decode_base64_json(uri)?;
-    Ok(Json(state.database.search(from, regex).await?))
+    // Ok(Json(state.database.search(from, regex).await?))
+    todo!()
 }
 
 /// Count karas available in a search set. We take the json argument as base64 encoded
@@ -155,21 +147,39 @@ pub(crate) async fn count(
     Path(uri): Path<String>,
 ) -> Result<Json<usize>, LektordError> {
     let SearchData { from, regex } = decode_base64_json(uri)?;
-    Ok(Json(state.database.count(from, regex).await))
+    // Ok(Json(state.database.count(from, regex).await))
+    todo!()
 }
 
 /// Get all playlists from the database with their associated informations.
 #[axum::debug_handler(state = LektorStatePtr)]
-pub(crate) async fn get_playlists(
+pub(crate) async fn get_playlists(State(state): State<LektorStatePtr>) -> Json<Vec<(KId, String)>> {
+    Json(state.database.playlists().list().await)
+}
+
+/// Get the informations relative to a specific playlist.
+#[axum::debug_handler(state = LektorStatePtr)]
+pub(crate) async fn get_playlist_info(
     State(state): State<LektorStatePtr>,
-) -> Json<Vec<(lektor_payloads::PlaylistName, PlaylistInfo)>> {
-    Json(Vec::from_iter(
-        state
-            .database
-            .playlists()
-            .await
-            .into_iter()
-            .map(|(n, info)| (n.into(), info)),
+    Path(id): Path<KId>,
+) -> Result<Json<PlaylistInfo>, LektordError> {
+    Ok(Json(
+        (state.database.playlists())
+            .read(id, |plt| plt.get_infos())
+            .await?,
+    ))
+}
+
+/// Get everything about a specific playlist.
+#[axum::debug_handler(state = LektorStatePtr)]
+pub(crate) async fn get_playlist(
+    State(state): State<LektorStatePtr>,
+    Path(id): Path<KId>,
+) -> Result<Json<Playlist>, LektordError> {
+    Ok(Json(
+        (state.database.playlists())
+            .read(id, |plt| plt.clone())
+            .await?,
     ))
 }
 
@@ -177,14 +187,13 @@ pub(crate) async fn get_playlists(
 #[axum::debug_handler(state = LektorStatePtr)]
 pub(crate) async fn get_playlist_content(
     State(state): State<LektorStatePtr>,
-    name: lektor_payloads::PlaylistName,
+    Path(id): Path<KId>,
 ) -> Result<Json<Vec<KId>>, LektordError> {
-    Ok(state
-        .database
-        .playlist_get_content(&name)
-        .await
-        .ok_or_else(|| Error::msg(format!("failed to get content of playlist {name}")))
-        .map(Json)?)
+    Ok(Json(
+        (state.database.playlists())
+            .read(id, |plt| plt.iter_seq_content().collect::<Vec<_>>())
+            .await?,
+    ))
 }
 
 /// Update the content of a playlist. For that the client must be the owner of the playlist or the
@@ -193,42 +202,60 @@ pub(crate) async fn get_playlist_content(
 pub(crate) async fn update_playlist(
     State(state): State<LektorStatePtr>,
     user_infos: LektorUser,
-    name: lektor_payloads::PlaylistName,
+    Path(id): Path<KId>,
     Json(action): Json<PlaylistUpdateAction>,
 ) -> Result<(), LektordError> {
-    let user_infos = state.verify_user(user_infos);
+    let user_infos = state.verify_user(user_infos).maybe_user()?;
     let (uin, uia) = (user_infos.name(), user_infos.is_admin());
-    let db = &state.database;
-    let name = name.into();
+    let db = &state.database.playlists();
 
-    Ok(match action {
-        PlaylistUpdateAction::Delete => db.playlist_delete(name, uin, uia).await,
+    match action {
+        PlaylistUpdateAction::Delete => db.delete(id, uin, uia).await?,
         PlaylistUpdateAction::GiveTo(new_user) => {
-            db.playlist_give_to(name, new_user, uin, uia).await
+            db.write(id, uin, uia, |plt| _ = plt.add_owner(new_user))
+                .await?
         }
         PlaylistUpdateAction::Rename(new_name) => {
-            db.playlist_rename(name, new_name.into(), uin, uia).await
+            db.write(id, uin, uia, |plt| _ = plt.set_name(new_name))
+                .await?
         }
         PlaylistUpdateAction::Remove(filter) => match filter {
-            KaraFilter::KId(kid) => db.playlist_delete_id(name, kid, uin, uia).await,
-            KaraFilter::List(_, kids) => db.playlist_delete_ids(name, kids, uin, uia).await,
+            KaraFilter::KId(kid) => db.write(id, uin, uia, |plt| plt.remove(kid)).await?,
+            KaraFilter::List(_, kids) => {
+                db.write(id, uin, uia, move |plt| plt.retain(|id| !kids.contains(id)))
+                    .await?
+            }
             KaraFilter::Playlist(_, from) => {
-                db.playlist_delete_playlist(name, from, uin, uia).await
+                let kids = db
+                    .read(from, |plt| plt.iter_seq_content().collect::<Vec<_>>())
+                    .await?;
+                db.write(id, uin, uia, move |plt| plt.retain(|id| !kids.contains(id)))
+                    .await?
             }
         },
 
         PlaylistUpdateAction::Add(filter) => match filter {
-            KaraFilter::KId(kid) => db.playlist_add_id(name, kid, uin, uia).await,
+            KaraFilter::KId(kid) => db.write(id, uin, uia, |plt| plt.push(kid)).await?,
             KaraFilter::Playlist(rand, from) => {
-                db.playlist_add_playlist(name, from, rand, uin, uia).await
+                let mut kids = db
+                    .read(from, |plt| plt.iter_seq_content().collect::<Vec<_>>())
+                    .await?;
+                if rand {
+                    kids[..].shuffle(&mut thread_rng());
+                }
+                db.write(id, uin, uia, move |plt| plt.append(&mut kids))
+                    .await?
             }
-            KaraFilter::List(false, kids) => db.playlist_add_ids(name, kids, uin, uia).await,
-            KaraFilter::List(true, mut kids) => {
-                kids[..].shuffle(&mut thread_rng());
-                db.playlist_add_ids(name, kids, uin, uia).await
+            KaraFilter::List(rand, mut kids) => {
+                if rand {
+                    kids[..].shuffle(&mut thread_rng());
+                }
+                db.write(id, uin, uia, move |plt| plt.append(&mut kids))
+                    .await?
             }
         },
-    }?)
+    }
+    Ok(())
 }
 
 /// Delete a playlist. For that the client must be the owner of the playlist or the super user of
@@ -237,13 +264,13 @@ pub(crate) async fn update_playlist(
 pub(crate) async fn delete_playlist(
     State(state): State<LektorStatePtr>,
     user_infos: LektorUser,
-    name: lektor_payloads::PlaylistName,
+    Path(id): Path<KId>,
 ) -> Result<(), LektordError> {
-    let user_infos = state.verify_user(user_infos);
-    Ok(state
-        .database
-        .playlist_delete(name, user_infos.name(), user_infos.is_admin())
-        .await?)
+    let user_infos = state.verify_user(user_infos).maybe_user()?;
+    (state.database.playlists())
+        .delete(id, user_infos.name(), user_infos.is_admin())
+        .await?;
+    Ok(())
 }
 
 /// Create an empty playlist. The user must be identified or the playlist will belong to anyone.
@@ -251,13 +278,19 @@ pub(crate) async fn delete_playlist(
 pub(crate) async fn create_playlist(
     State(state): State<LektorStatePtr>,
     user_infos: LektorUser,
-    name: lektor_payloads::PlaylistName,
+    Path(name): Path<String>,
 ) -> Result<(), LektordError> {
-    let info = match state.verify_user(user_infos) {
-        LektorUser::Unverified(_, _) => PlaylistInfo::new(),
-        LektorUser::User(n, _) | LektorUser::Admin(n, _) => PlaylistInfo::new().user(n),
-    };
-    Ok(state.database.playlist_new(name, info).await?)
+    let user = state
+        .verify_user(user_infos)
+        .maybe_user()
+        .map(|user| user.into_parts().0);
+    (state.database.playlists())
+        .create(name, |plt| match user {
+            Ok(user) => plt.with_owner_sync(user),
+            _ => plt,
+        })
+        .await?;
+    Ok(())
 }
 
 /// Update the database from the remotes. The user must be the super user to do that. We can't use
@@ -268,11 +301,7 @@ pub(crate) async fn adm_update(
     State(state): State<LektorStatePtr>,
     admin: LektorUser,
 ) -> Result<(), LektordError> {
-    let admin = match state.verify_user(admin) {
-        user @ LektorUser::Admin(_, _) => user,
-        user => return Err(anyhow!("user {user:?} is not an admin").into()),
-    };
-
+    let admin = state.verify_user(admin).maybe_admin()?;
     log::info!("launching update requested by {admin:?}");
     std::thread::spawn(move || {
         let local = LocalSet::new();
@@ -281,7 +310,6 @@ pub(crate) async fn adm_update(
                 let handle = state.database.update().await;
                 let count = state.repo.update_with(&handle).await?;
                 handle.finished().await;
-                state.database.refresh_playlist_contents().await;
                 Ok::<_, anyhow::Error>(count)
             })
             .await
@@ -295,7 +323,7 @@ pub(crate) async fn adm_update(
             .max_blocking_threads(1024)
             .enable_all()
             .build()
-            .expect("failed to build tokio runtime to update database from repo")
+            .expect("can't build tokio runtime")
             .block_on(local);
     });
     Ok(())
@@ -306,29 +334,35 @@ pub(crate) async fn adm_update(
 pub(crate) async fn adm_shutdown(
     State(state): State<LektorStatePtr>,
     admin: LektorUser,
-) -> StatusCode {
-    match state.verify_user(admin) {
-        admin @ LektorUser::Admin(..) => {
-            log::info!("shutdown requested by {admin:?}");
-            if let Some(shutdown) = state.shutdown.write().await.take() {
-                let _ = shutdown.send(());
-            }
-            StatusCode::OK
-        }
-        _ => StatusCode::FORBIDDEN,
-    }
+) -> Result<(), LektordError> {
+    let admin = state.verify_user(admin).maybe_admin()?;
+    log::info!("shutdown requested by {admin:?}");
+    (state.shutdown.write().await.take())
+        .map(|shutdown| _ = shutdown.send(()))
+        .context("failed to send shutdown signal")?;
+    Ok(())
 }
 
 #[axum::debug_handler(state = LektorStatePtr)]
 pub(crate) async fn get_queue_count(
     State(state): State<LektorStatePtr>,
 ) -> Json<[usize; PRIORITY_LENGTH]> {
-    Json(state.database.queue_count().await)
+    Json(
+        state
+            .queue()
+            .read(|content, _| content.count_per_level())
+            .await,
+    )
 }
 
 #[axum::debug_handler(state = LektorStatePtr)]
 pub(crate) async fn get_history_count(State(state): State<LektorStatePtr>) -> Json<usize> {
-    Json(state.database.history_count().await)
+    Json(
+        state
+            .queue()
+            .read(|content, _| content.history_count())
+            .await,
+    )
 }
 
 /// Returns the karas in the indicated range of the queue.
@@ -339,8 +373,8 @@ pub(crate) async fn get_queue_range(
 ) -> Json<Vec<(Priority, KId)>> {
     Json(
         state
-            .database
-            .queue((range.start_bound(), range.end_bound()))
+            .queue()
+            .read(|content, _| content.get((range.start_bound(), range.end_bound())))
             .await,
     )
 }
@@ -350,26 +384,41 @@ pub(crate) async fn get_queue_range(
 pub(crate) async fn add_to_queue(
     State(state): State<LektorStatePtr>,
     Json(QueueAddAction { priority, action }): Json<QueueAddAction>,
-) {
+) -> Result<(), LektordError> {
     match action {
-        KaraFilter::KId(id) => state.database.queue_insert(priority, id).await,
-        KaraFilter::List(false, ids) => state.database.queue_insert_all(priority, ids).await,
-        KaraFilter::List(true, mut ids) => {
-            ids[..].shuffle(&mut thread_rng());
-            state.database.queue_insert_all(priority, ids).await
+        KaraFilter::KId(id) => {
+            state
+                .queue()
+                .write(|content, _| content.insert(priority, id))
+                .await
         }
-        KaraFilter::Playlist(true, plt) => {
-            if let Some(mut plt) = state.database.playlist_get_content(plt).await {
-                plt[..].shuffle(&mut thread_rng());
-                state.database.queue_insert_all(priority, plt).await
+        KaraFilter::List(shuffle, mut ids) => {
+            if shuffle {
+                ids[..].shuffle(&mut thread_rng());
             }
+            state
+                .queue()
+                .write(|content, _| content.insert_all(priority, ids))
+                .await
         }
-        KaraFilter::Playlist(false, plt) => {
-            if let Some(plt) = state.database.playlist_get_content(plt).await {
-                state.database.queue_insert_all(priority, plt).await
-            }
+        KaraFilter::Playlist(shuffle, plt) => {
+            let karas = (state.database.playlists())
+                .read(plt, |plt| match shuffle {
+                    true => {
+                        let mut content: Vec<_> = plt.iter_uniq_content().collect();
+                        content[..].shuffle(&mut thread_rng());
+                        content
+                    }
+                    false => plt.iter_seq_content().collect(),
+                })
+                .await?;
+            state
+                .queue()
+                .write(|content, _| content.insert_all(priority, karas))
+                .await
         }
     }
+    Ok(())
 }
 
 /// Enqueue a kara in the queue, at a specific priority.
@@ -378,7 +427,10 @@ pub(crate) async fn add_kid_to_queue(
     State(state): State<LektorStatePtr>,
     Path((prio, kid)): Path<(Priority, KId)>,
 ) {
-    state.database.queue_insert(prio, kid).await
+    state
+        .queue()
+        .write(|content, _| content.insert(prio, kid))
+        .await
 }
 
 /// Remove a kara from the queue, only a specific priority.
@@ -387,7 +439,36 @@ pub(crate) async fn delete_kid_from_queue(
     State(state): State<LektorStatePtr>,
     Path((prio, kid)): Path<(Priority, KId)>,
 ) {
-    state.database.queue_delete_all_from_level(prio, kid).await
+    state
+        .queue()
+        .write(|content, _| content.delete_all_from_level(prio, kid))
+        .await
+}
+
+/// Get the queue level
+#[axum::debug_handler(state = LektorStatePtr)]
+pub(crate) async fn get_queue_level(
+    State(state): State<LektorStatePtr>,
+    Path(prio): Path<Priority>,
+) -> Json<Vec<KId>> {
+    Json(
+        state
+            .queue()
+            .read(|content, _| content.get_level(prio))
+            .await,
+    )
+}
+
+/// Delete the queue level
+#[axum::debug_handler(state = LektorStatePtr)]
+pub(crate) async fn delete_queue_level(
+    State(state): State<LektorStatePtr>,
+    Path(prio): Path<Priority>,
+) {
+    state
+        .queue()
+        .write(|content, _| content.delete_level(prio))
+        .await
 }
 
 /// Shuffle a level of the queue.
@@ -396,7 +477,10 @@ pub(crate) async fn shuffle_queue_level(
     State(state): State<LektorStatePtr>,
     Path(prio): Path<Priority>,
 ) {
-    state.database.queue_shuffle_level(prio).await
+    state
+        .queue()
+        .write(|content, _| content.shuffle_level(prio))
+        .await
 }
 
 /// Get informations from the queue.
@@ -405,20 +489,21 @@ pub(crate) async fn get_from_queue(
     State(state): State<LektorStatePtr>,
     Path(level): Path<Priority>,
 ) -> Json<Vec<KId>> {
-    Json(state.database.queue_get_level(level).await)
+    Json(
+        state
+            .queue()
+            .read(|content, _| content.get_level(level))
+            .await,
+    )
 }
 
 /// Remove something from the queue, can be a level, a kara, etc.
 #[axum::debug_handler(state = LektorStatePtr)]
-pub(crate) async fn delete_from_queue(
-    State(state): State<LektorStatePtr>,
-    Path(what): Path<String>,
-) {
-    match Priority::from_str(&what) {
-        Ok(prio) => state.database.queue_delete_level(prio).await,
-        // Not a priority, try to parse a KId and delete it.
-        _ => state.database.queue_delete_all_maybe_id(what).await,
-    }
+pub(crate) async fn delete_from_queue(State(state): State<LektorStatePtr>, Path(kid): Path<KId>) {
+    state
+        .queue()
+        .write(|content, _| content.delete_all(kid))
+        .await
 }
 
 /// Remove a kara from the history, each occurances.
@@ -427,7 +512,10 @@ pub(crate) async fn delete_kid_from_history(
     State(state): State<LektorStatePtr>,
     Path(id): Path<KId>,
 ) {
-    state.database.history_delete_all(id).await
+    state
+        .queue()
+        .write(|content, _| content.history_delete_all(id))
+        .await
 }
 
 /// Updates the karas in the indicated range of the queue.
@@ -437,18 +525,35 @@ pub(crate) async fn update_queue_range(
     range: Range,
     Json(action): Json<QueueUpdateAction>,
 ) {
-    use QueueUpdateAction::*;
     match action {
-        Shuffle => state.database.queue_shuffle(range).await,
-        Delete => state.database.queue_delete(range).await,
-        MoveAfter(after) => state.database.queue_move_after(range, after).await,
+        QueueUpdateAction::Shuffle => {
+            state
+                .queue()
+                .write(|content, _| content.shuffle(range))
+                .await
+        }
+        QueueUpdateAction::Delete => {
+            state
+                .queue()
+                .write(|content, _| content.delete(range))
+                .await
+        }
+        QueueUpdateAction::MoveAfter(after) => {
+            state
+                .queue()
+                .write(|content, _| content.move_after(range, after))
+                .await
+        }
     }
 }
 
 /// Removes the karas in the indicated range of the queue.
 #[axum::debug_handler(state = LektorStatePtr)]
 pub(crate) async fn delete_queue_range(State(state): State<LektorStatePtr>, range: Range) {
-    state.database.queue_delete(range).await;
+    state
+        .queue()
+        .write(|content, _| content.delete(range))
+        .await
 }
 
 /// Returns the karas in the indicated range of the history.
@@ -457,7 +562,12 @@ pub(crate) async fn get_history_range(
     State(state): State<LektorStatePtr>,
     range: Range,
 ) -> Json<Vec<KId>> {
-    Json(state.database.history(range).await)
+    Json(
+        state
+            .queue()
+            .write(|content, _| content.history(range))
+            .await,
+    )
 }
 
 /// Removes the karas in the indicated range of the history.
@@ -466,5 +576,10 @@ pub(crate) async fn delete_history_range(
     State(state): State<LektorStatePtr>,
     range: Range,
 ) -> Json<Vec<KId>> {
-    Json(state.database.history_delete(range).await)
+    Json(
+        state
+            .queue()
+            .write(|content, _| content.history_delete(range))
+            .await,
+    )
 }
diff --git a/lektord/src/c_wrapper/commands.rs b/lektord/src/c_wrapper/commands.rs
index 6132438b..c12fe75a 100644
--- a/lektord/src/c_wrapper/commands.rs
+++ b/lektord/src/c_wrapper/commands.rs
@@ -1,6 +1,6 @@
 use super::{PlayerEvent, STATE};
 use anyhow::{bail, Context, Error, Result};
-use lektor_nkdb::PlayState;
+use lektor_payloads::{KId, PlayState};
 use std::{ffi::*, path::Path, ptr::NonNull};
 
 /// Safe wrapper around `mod_stop_playback`. Stops the playback. Same as [player_toggle_pause], don't update
@@ -77,7 +77,7 @@ pub(crate) fn player_get_elapsed() -> Result<i64> {
 }
 
 /// Safe wrapper around `mod_load_file` Tell the player to load a file by its path.
-pub(crate) fn player_load_file(path: impl AsRef<Path>, id: u64) -> Result<()> {
+pub(crate) fn player_load_file(path: impl AsRef<Path>, id: KId) -> Result<()> {
     extern "C" {
         fn mod_load_file(_: NonNull<c_char>, _: u64) -> c_int;
     }
@@ -86,7 +86,7 @@ pub(crate) fn player_load_file(path: impl AsRef<Path>, id: u64) -> Result<()> {
         .to_str()
         .ok_or_else(|| Error::msg("path contained non-utf8 characters"))?;
     let cstr = CString::new(path)?;
-    (0 == unsafe { mod_load_file(NonNull::new_unchecked(cstr.as_ptr() as *mut _), id) })
+    (0 == unsafe { mod_load_file(NonNull::new_unchecked(cstr.as_ptr() as *mut _), id.into()) })
         .then_some(())
         .with_context(|| format!("failed load file {path}"))
 }
diff --git a/lektord/src/c_wrapper/mod.rs b/lektord/src/c_wrapper/mod.rs
index 23c595e5..97b4d3c7 100644
--- a/lektord/src/c_wrapper/mod.rs
+++ b/lektord/src/c_wrapper/mod.rs
@@ -2,7 +2,6 @@
 
 use crate::LektorStatePtr;
 use anyhow::{bail, ensure, Context, Result};
-use lektor_nkdb::PlayState;
 use lektor_utils::config::LektorPlayerConfig;
 use std::{
     ffi::*,
@@ -69,36 +68,51 @@ pub(crate) fn init_player_module(ptr: LektorStatePtr, config: LektorPlayerConfig
         force_x11,
     } = config;
 
-    unsafe {
-        // We must use non-async stuff to propagate things from the player module back to the tokio
-        // runtime... The code called form C/C++ will use `sender.blocking_send`.
-        let (sender, mut receiver) = tokio::sync::mpsc::channel::<PlayerEvent>(10);
-        let weak_ptr = Arc::downgrade(&ptr);
-        let handle = tokio::task::spawn(async move {
-            while let Some(event) = receiver.recv().await {
-                use {LktPlayState as LPS, PlayState as PS};
-                match event {
-                    PlayerEvent::SetPlayState(state) => match LktPlayState::try_from(state) {
-                        Ok(LPS::Stop) => ptr.database.set_playstate(PS::Stop).await,
-                        Ok(LPS::Play) => ptr.database.set_playstate(PS::Play).await,
-                        Ok(LPS::Pause) => ptr.database.set_playstate(PS::Pause).await,
-                        Ok(LPS::Toggle) => ptr.database.toggle_playstate().await,
-                        Err(e) => log::error!("invalid playstate: {e}"),
-                    },
-                    PlayerEvent::PlayNext => {
-                        if let Some(ptr) = weak_ptr.upgrade() {
-                            let _ = ptr.play_next().await.map_err(|e| log::error!("{e}"));
-                        }
+    // We must use non-async stuff to propagate things from the player module back to the tokio
+    // runtime... The code called form C/C++ will use `sender.blocking_send`.
+    let (sender, mut receiver) = tokio::sync::mpsc::channel::<PlayerEvent>(10);
+    let weak_ptr = Arc::downgrade(&ptr);
+    let handle = tokio::task::spawn(async move {
+        while let Some(event) = receiver.recv().await {
+            use {lektor_payloads::PlayState as PS, LktPlayState as LPS};
+            match event {
+                PlayerEvent::SetPlayState(state) => match LktPlayState::try_from(state) {
+                    Ok(state @ (LPS::Stop | LPS::Play | LPS::Pause)) => {
+                        (ptr.queue())
+                            .write(|_, stored| {
+                                stored.set(match state {
+                                    LPS::Pause => PS::Pause,
+                                    LPS::Play => PS::Play,
+                                    _ => PS::Stop,
+                                })
+                            })
+                            .await
+                    }
+                    Ok(LPS::Toggle) => {
+                        (ptr.queue())
+                            .write(|_, stored| match stored.get() {
+                                PS::Stop | PS::Pause => stored.set(PS::Play),
+                                PS::Play => stored.set(PS::Pause),
+                            })
+                            .await
+                    }
+                    Err(e) => log::error!("invalid playstate: {e}"),
+                },
+                PlayerEvent::PlayNext => {
+                    if let Some(ptr) = weak_ptr.upgrade() {
+                        let _ = ptr.play_next().await.map_err(|e| log::error!("{e}"));
                     }
-                    PlayerEvent::PlayPrev => {
-                        if let Some(ptr) = weak_ptr.upgrade() {
-                            let _ = ptr.play_previous().await.map_err(|e| log::error!("{e}"));
-                        }
+                }
+                PlayerEvent::PlayPrev => {
+                    if let Some(ptr) = weak_ptr.upgrade() {
+                        let _ = ptr.play_previous().await.map_err(|e| log::error!("{e}"));
                     }
                 }
             }
-        });
+        }
+    });
 
+    unsafe {
         // Set the state for the C/C++ code
         ensure!(
             STATE.set((sender, handle)).is_ok(),
diff --git a/lektord/src/c_wrapper/playstate.rs b/lektord/src/c_wrapper/playstate.rs
index d77d0fc5..9221061f 100644
--- a/lektord/src/c_wrapper/playstate.rs
+++ b/lektord/src/c_wrapper/playstate.rs
@@ -1,4 +1,4 @@
-use lektor_nkdb::PlayState;
+use lektor_payloads::PlayState;
 use std::ffi::c_int;
 
 /// The play state that can be send from/to the player module. They don't directly map to what is
diff --git a/lektord/src/lib.rs b/lektord/src/lib.rs
new file mode 100644
index 00000000..fa39c033
--- /dev/null
+++ b/lektord/src/lib.rs
@@ -0,0 +1,106 @@
+mod app;
+mod c_wrapper;
+pub mod cmd;
+pub mod config;
+mod error;
+
+include!(concat!(env!("OUT_DIR"), "/lektord_build_infos.rs"));
+
+use crate::{app::*, config::*, error::*};
+use anyhow::{Context, Result};
+use futures::{stream::FuturesUnordered, TryStreamExt as _};
+use hyper::service::service_fn;
+use hyper_util::{
+    rt::{TokioExecutor, TokioIo},
+    server::conn::auto::Builder as ServerBuilder,
+};
+use std::net::SocketAddr;
+use tokio::{net::TcpListener, signal, sync::oneshot::Receiver};
+use tower::Service;
+
+/// Launches the server, for each socket to listen to we have one task. We do that to be able to
+/// handle each sockets in a concurrent way.
+pub async fn launch_server(config: LektorConfig) -> Result<()> {
+    // Write to apply any changes...
+    log::info!("starting the lektord daemon");
+    lektor_utils::config::write_config_async("lektord", config.clone()).await?;
+
+    // Init the application.
+    let addrs = config.listen.clone();
+    let (app, shutdown) = app(config).await.context("failed to build service")?;
+
+    // Launch an instance for each socket to listen to.
+    FuturesUnordered::from_iter(addrs.into_iter().map(|listen| async move {
+        TcpListener::bind(listen)
+            .await
+            .map(|socket| (listen, socket))
+            .with_context(|| format!("failed to bind to {listen}"))
+    }))
+    .try_collect::<Vec<_>>()
+    .await?
+    .into_iter()
+    .for_each(|(addr, socket)| {
+        tokio::spawn(server_instance(addr, socket, app.clone()));
+    });
+
+    // Wait for terminazon...
+    shutdown_signal(shutdown).await;
+    Ok(())
+}
+
+/// Have the instance of the server for one socket that we listen to. For each client we will
+/// create a new task to be able to handle clients concurrently for one socket.
+async fn server_instance(addr: SocketAddr, socket: TcpListener, app: axum::Router) {
+    loop {
+        let Ok((stream, client)) = socket.accept().await else {
+            return log::error!("failed to accept socket at {addr}");
+        };
+        let app = app.clone(); // One thread per client, they all share the same state!
+        tokio::spawn(async move {
+            ServerBuilder::new(TokioExecutor::new())
+                .serve_connection(TokioIo::new(stream), service_fn(|r| app.clone().call(r)))
+                .await
+                .inspect_err(|e| log::error!("failed to serve {client} from {addr}: {e}"))
+        });
+    }
+}
+
+/// Gracefull ctrl+c handling.
+async fn shutdown_signal(shutdown: Receiver<()>) {
+    let shutdown = async {
+        let _ = shutdown.await;
+        log::info!("shutdown signal!")
+    };
+
+    let ctrl_c = async {
+        signal::ctrl_c()
+            .await
+            .expect("failed to install Ctrl+C handler");
+        log::info!("ctrl+c signal!")
+    };
+
+    #[cfg(unix)]
+    let terminate = async {
+        signal::unix::signal(signal::unix::SignalKind::terminate())
+            .expect("failed to install signal handler")
+            .recv()
+            .await;
+        log::info!("terminate signal!")
+    };
+
+    #[cfg(not(unix))]
+    let terminate = std::future::pending::<()>();
+
+    tokio::select! {
+        _ = ctrl_c => {},
+        _ = terminate => {},
+        _ = shutdown => {},
+    }
+
+    log::info!("received termination signal shutting down, try to unregister the state from the player module");
+    let _ = c_wrapper::close_player_module()
+        .await
+        .map_err(|e| log::error!("{e}"));
+    log::info!("will now call exit(EXIT_SUCCESS)");
+    std::process::exit(0);
+}
diff --git a/lektord/src/main.rs b/lektord/src/main.rs
index 42035fdc..7d532034 100644
--- a/lektord/src/main.rs
+++ b/lektord/src/main.rs
@@ -1,28 +1,10 @@
-mod app;
-pub mod c_wrapper;
-mod cmd;
-mod config;
-mod error;
-
-include!(concat!(env!("OUT_DIR"), "/lektord_build_infos.rs"));
-
-use crate::{app::*, config::*, error::*};
 use anyhow::{Context, Result};
-use cmd::SubCommand;
-use futures::{stream::FuturesUnordered, TryStreamExt as _};
 use lektor_utils::*;
-
-// Server things
-
-use hyper::service::service_fn;
-use hyper_util::{
-    rt::{TokioExecutor, TokioIo},
-    server::conn::auto::Builder as ServerBuilder,
+use lektord::{
+    cmd::{self, SubCommand},
+    config::*,
 };
-use std::net::SocketAddr;
 use std::sync::atomic::{AtomicU64, Ordering};
-use tokio::{net::TcpListener, signal, sync::oneshot::Receiver};
-use tower::Service;
 
 fn main() -> Result<()> {
     logger::Builder::default()
@@ -62,95 +44,8 @@ fn main() -> Result<()> {
             .enable_all()
             .thread_stack_size(3 * 1024 * 1024) // 3Mio for each thread, should be enaugh
             .build()?
-            .block_on(launch_server(config)),
+            .block_on(lektord::launch_server(config)),
 
         args => unreachable!("{args:?}"),
     }
 }
-
-/// Launches the server, for each socket to listen to we have one task. We do that to be able to
-/// handle each sockets in a concurrent way.
-async fn launch_server(config: LektorConfig) -> Result<()> {
-    // Write to apply any changes...
-    log::info!("starting the lektord daemon");
-    lektor_utils::config::write_config_async("lektord", config.clone()).await?;
-
-    // Init the application.
-    let addrs = config.listen.clone();
-    let (app, shutdown) = app(config).await.context("failed to build service")?;
-
-    // Launch an instance for each socket to listen to.
-    FuturesUnordered::from_iter(addrs.into_iter().map(|listen| async move {
-        TcpListener::bind(listen)
-            .await
-            .map(|socket| (listen, socket))
-            .with_context(|| format!("failed to bind to {listen}"))
-    }))
-    .try_collect::<Vec<_>>()
-    .await?
-    .into_iter()
-    .for_each(|(addr, socket)| {
-        tokio::spawn(server_instance(addr, socket, app.clone()));
-    });
-
-    // Wait for terminazon...
-    shutdown_signal(shutdown).await;
-    Ok(())
-}
-
-/// Have the instance of the server for one socket that we listen to. For each client we will
-/// create a new task to be able to handle clients concurrently for one socket.
-async fn server_instance(addr: SocketAddr, socket: TcpListener, app: axum::Router) {
-    loop {
-        let Ok((stream, client)) = socket.accept().await else {
-            return log::error!("failed to accept socket at {addr}");
-        };
-        let app = app.clone(); // One thread per client, they all share the same state!
-        tokio::spawn(async move {
-            ServerBuilder::new(TokioExecutor::new())
-                .serve_connection(TokioIo::new(stream), service_fn(|r| app.clone().call(r)))
-                .await
-                .inspect_err(|e| log::error!("failed to serve {client} from {addr}: {e}"))
-        });
-    }
-}
-
-/// Gracefull ctrl+c handling.
-async fn shutdown_signal(shutdown: Receiver<()>) {
-    let shutdown = async {
-        let _ = shutdown.await;
-        log::info!("shutdown signal!")
-    };
-
-    let ctrl_c = async {
-        signal::ctrl_c()
-            .await
-            .expect("failed to install Ctrl+C handler");
-        log::info!("ctrl+c signal!")
-    };
-
-    #[cfg(unix)]
-    let terminate = async {
-        signal::unix::signal(signal::unix::SignalKind::terminate())
-            .expect("failed to install signal handler")
-            .recv()
-            .await;
-        log::info!("terminate signal!")
-    };
-
-    #[cfg(not(unix))]
-    let terminate = std::future::pending::<()>();
-
-    tokio::select! {
-        _ = ctrl_c => {},
-        _ = terminate => {},
-        _ = shutdown => {},
-    }
-
-    log::info!("received termination signal shutting down, try to unregister the state from the player module");
-    let _ = c_wrapper::close_player_module()
-        .await
-        .map_err(|e| log::error!("{e}"));
-    log::info!("will now call exit(EXIT_SUCCESS)");
-    std::process::exit(0);
-}
diff --git a/lkt/Cargo.toml b/lkt/Cargo.toml
index c97c8633..6924ce8f 100644
--- a/lkt/Cargo.toml
+++ b/lkt/Cargo.toml
@@ -11,7 +11,6 @@ description = "Simple command line utility to interact with the lektord daemon"
 tokio.workspace = true
 reqwest.workspace = true
 futures.workspace = true
-async-trait.workspace = true
 
 serde.workspace = true
 serde_json.workspace = true
diff --git a/lkt/src/args/mod.rs b/lkt/src/args/mod.rs
index e4831946..a9814e26 100644
--- a/lkt/src/args/mod.rs
+++ b/lkt/src/args/mod.rs
@@ -2,6 +2,7 @@ mod parsers;
 
 use clap::{Parser, Subcommand};
 use clap_complete::Shell;
+use lektor_payloads::KId;
 
 #[derive(Parser, Debug)]
 #[command( author
@@ -206,7 +207,7 @@ pub enum SubCommand {
              , long       = "info"
              , value_name = "ID"
         )]
-        get: Option<u64>,
+        get: Option<KId>,
 
         /// Search kara in a playlist with a query. The first element is the name of the playlist.
         #[arg( action     = clap::ArgAction::Append
diff --git a/lkt/src/lib.rs b/lkt/src/lib.rs
new file mode 100644
index 00000000..db3d322c
--- /dev/null
+++ b/lkt/src/lib.rs
@@ -0,0 +1,346 @@
+#![forbid(unsafe_code)]
+
+include!(concat!(env!("OUT_DIR"), "/lkt_build_infos.rs"));
+
+pub mod args;
+pub mod config;
+pub mod manpage;
+
+use crate::{args::*, config::*};
+use anyhow::{anyhow, Context as _, Result};
+use futures::{
+    stream::{self, FuturesUnordered},
+    StreamExt,
+};
+use lektor_lib::*;
+use lektor_payloads::*;
+use std::{borrow::Cow, ops::RangeBounds};
+
+pub async fn collect_karas(config: &ConnectConfig, ids: Vec<KId>) -> Result<Vec<Kara>> {
+    let count = ids.len();
+    let res: Vec<_> = stream::iter(ids.into_iter())
+        .then(|id| requests::get_kara_by_kid(config, id))
+        .filter_map(|res| async { res.map_err(|err| log::error!("{err}")).ok() })
+        .collect::<_>()
+        .await;
+    res.len()
+        .eq(&count)
+        .then_some(res)
+        .ok_or(anyhow!("got errors, didn't received all the karas' data"))
+}
+
+pub async fn simple_karas_print(config: &ConnectConfig, ids: Vec<KId>) -> Result<()> {
+    let res = collect_karas(config, ids).await?;
+    let count = res.len().to_string().len();
+    res.into_iter()
+        .enumerate()
+        .for_each(|(i, k)| println!("[{i:0>count$}] {k}"));
+    Ok(())
+}
+
+pub async fn exec_lkt(config: LktConfig, cmd: SubCommand) -> Result<()> {
+    // Write to apply changes...
+    lektor_utils::config::write_config_async("lkt", config.clone()).await?;
+    let config = ConnectConfig::from(config);
+    let config = &config;
+
+    use crate::args::SubCommand::*;
+    match cmd {
+        // ============= //
+        // Queue options //
+        // ============= //
+
+        // Display current kara in one line (for status lines...)
+        Playback { current: true, .. } => {
+            let PlayStateWithCurrent { state, current } = requests::get_status(config).await?;
+            match current {
+                None => println!("[{state:?}]"),
+                Some((kid, elapsed, duration)) => {
+                    let current = requests::get_kara_by_kid(config, kid).await?.to_string();
+                    println!("[{state:?}] {elapsed}/{duration} {current}");
+                }
+            }
+            Ok(())
+        }
+
+        // Display the status of lektord
+        Playback { status: true, .. } => {
+            let Infos {
+                version,
+                last_epoch,
+            } = requests::get_infos(config).await?;
+            let PlayStateWithCurrent { state, current } = requests::get_status(config).await?;
+            let current = match current {
+                None => None,
+                Some((kid, elapsed, duration)) => Some((
+                    requests::get_kara_by_kid(config, kid).await?,
+                    elapsed,
+                    duration,
+                )),
+            };
+            let queue_counts = requests::get_queue_count(config).await?;
+            let history_count = requests::get_history_count(config).await?;
+            let last_epoch = match last_epoch {
+                Some(num) => num.to_string().into(),
+                None => Cow::Borrowed("None"),
+            };
+
+            println!("Version:          {version}");
+            println!("Playback State:   {state:?}");
+            if let Some((current, elapsed, duration)) = current {
+                println!("Current Kara:     {current}");
+                println!("Kara elapsed:     {elapsed}s");
+                println!("Kara duration:    {duration}s");
+            }
+            println!("Karas in Queue:   {queue_counts:?}");
+            println!("Karas in Hustory: {history_count}");
+            println!("Database epoch:   {last_epoch}");
+
+            Ok(())
+        }
+
+        // Play from a position in the queue
+        Playback {
+            play: Some(play), ..
+        } => requests::play_from_position(config, play.unwrap_or_default()).await,
+
+        // Toggle the play state
+        Playback {
+            pause: Some(None), ..
+        } => requests::toggle_playback_state(config).await,
+
+        // Pause the playback
+        Playback {
+            pause: Some(Some(true)),
+            ..
+        } => requests::set_playback_state(config, PlayState::Pause).await,
+
+        // Force to resume the playback
+        Playback {
+            pause: Some(Some(false)),
+            ..
+        } => requests::set_playback_state(config, PlayState::Play).await,
+
+        Playback { next: true, .. } => requests::play_next(config).await,
+        Playback { prev: true, .. } => requests::play_previous(config).await,
+        Playback { stop: true, .. } => requests::set_playback_state(config, PlayState::Stop).await,
+
+        // ============= //
+        // Queue options //
+        // ============= //
+
+        // List kara in the queue
+        Queue { pos: Some(pos), .. } => {
+            let pos = pos.unwrap_or_default();
+            let ids = requests::get_queue_range(config, pos).await?;
+            let prios: Vec<_> = ids.iter().map(|(prio, _)| *prio).collect();
+            let karas: Vec<_> =
+                collect_karas(config, ids.into_iter().map(|(_, id)| id).collect()).await?;
+            let count = match pos {
+                Range::From(_) | Range::Full => prios.len().max(karas.len()),
+                Range::To(max) | Range::Bound(_, max) => max - 1,
+                Range::ToInclusive(max) | Range::BoundInclusive(_, max) => max,
+            };
+            let start = match pos.start_bound() {
+                std::ops::Bound::Included(start) => *start,
+                std::ops::Bound::Excluded(start) => start - 1,
+                std::ops::Bound::Unbounded => 0,
+            };
+            let count = count.to_string().len();
+            let space = PRIORITY_VALUES
+                .iter()
+                .map(|prio| prio.as_str().len())
+                .max()
+                .expect("oupsy daisy");
+            std::iter::zip(prios, karas)
+                .enumerate()
+                .for_each(|(i, (l, k))| println!("[{:0>count$}] {l:>space$}: {k}", start + i));
+            Ok(())
+        }
+
+        // Add karas to the queue
+        Queue { add: Some(add), .. } => {
+            let AddArguments { level: lvl, query } = add.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::Database, [query]).await?;
+            let add = QueueAddAction {
+                priority: lvl,
+                action: KaraFilter::List(true, ids),
+            };
+            requests::add_to_queue(config, add).await
+        }
+
+        // Remove kara from the queue by their position
+        Queue {
+            remove: Some(r), ..
+        } => requests::remove_range_from_queue(config, r.unwrap_or_default()).await,
+
+        // Shuffle the queue.
+        Queue {
+            shuffle: Some(shuffle),
+            ..
+        } => requests::shuffle_queue_range(config, shuffle.unwrap_or_default()).await,
+
+        // =============== //
+        // History options //
+        // =============== //
+        History { clear: true, .. } => requests::remove_range_from_history(config, ..).await,
+        History { pos: Some(pos), .. } => {
+            let ids = requests::get_history_range(config, pos.unwrap_or_default()).await?;
+            simple_karas_print(config, ids).await
+        }
+
+        // ============== //
+        // Search options //
+        // ============== //
+        Search {
+            database: Some(regex),
+            ..
+        } => {
+            let regex = regex.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::Database, [regex]).await?;
+            simple_karas_print(config, ids).await
+        }
+
+        Search {
+            count: Some(regex), ..
+        } => {
+            let regex = regex.join(" ");
+            let count = requests::count_karas(config, SearchFrom::Database, regex.parse()?).await?;
+            println!("Search in Database: {regex}");
+            println!("Matched Count:      {count}");
+            Ok(())
+        }
+
+        Search { get: Some(kid), .. } => {
+            let kara = requests::get_kara_by_kid(config, kid).await?;
+            dbg!(kara);
+            Ok(())
+        }
+
+        Search {
+            plt: Some(mut args),
+            ..
+        } => {
+            let name = args.remove(0).parse()?;
+            let regex = args.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::Playlist(name), [regex]).await?;
+            simple_karas_print(config, ids).await
+        }
+
+        Search {
+            queue: Some(args), ..
+        } => {
+            let regex = args.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::Queue, [regex]).await?;
+            simple_karas_print(config, ids).await
+        }
+
+        Search {
+            history: Some(args),
+            ..
+        } => {
+            let regex = args.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::History, [regex]).await?;
+            simple_karas_print(config, ids).await
+        }
+
+        // ================ //
+        // Playlist options //
+        // ================ //
+
+        // Create a playlist
+        Playlist {
+            create: Some(n), ..
+        } => requests::create_playlist(config, n.into()).await,
+
+        // Delete a playlist and all its content.
+        Playlist {
+            destroy: Some(n), ..
+        } => requests::delete_playlist(config, n.into()).await,
+
+        // List all the playlists
+        Playlist {
+            list: Some(None), ..
+        } => {
+            let plts = stream::iter(requests::get_playlists(config).await?)
+                .then(|(id, _)| async move { requests::get_playlist_info(config, id).await })
+                .collect::<FuturesUnordered<_>>()
+                .await
+                .into_iter()
+                .collect::<Result<Vec<_>, _>>()?;
+
+            let count = plts.len().to_string().len();
+            let padding: String = " ".repeat(count);
+            for (idx, playlist) in plts.into_iter().enumerate() {
+                if idx != 0 {
+                    println!();
+                }
+                println!("{idx:0>count$}: Playlist {}", playlist.name());
+                if playlist.owners().count() != 0 {
+                    println!(
+                        "{padding}  Owned by {}",
+                        playlist.owners().collect::<Vec<&str>>().join(",")
+                    );
+                }
+                println!(
+                    "{padding}  Created at {}",
+                    playlist.created_at().format("%Y-%m-%d %H:%M:%S")
+                );
+                println!(
+                    "{padding}  Last updated at {}",
+                    playlist.updated_at().format("%Y-%m-%d %H:%M:%S")
+                );
+            }
+            Ok(())
+        }
+
+        // List the content of a playlist
+        Playlist {
+            list: Some(Some(n)),
+            ..
+        } => {
+            let id = requests::get_playlists(config)
+                .await?
+                .into_iter()
+                .find_map(|(id, name)| (n == name).then_some(id))
+                .with_context(|| format!("failed to find a playlist with the name {n}"))?;
+            let ids = requests::get_playlist_content(config, id).await?;
+            simple_karas_print(config, ids).await
+        }
+
+        // Add something to a playlist.
+        Playlist {
+            add: Some(mut args),
+            ..
+        } => {
+            let name = args.remove(0).into();
+            let rgx = args.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::Database, [rgx]).await?;
+            let add = KaraFilter::List(true, ids);
+            requests::add_to_playlist(config, name, add).await
+        }
+
+        // Remove something from a playlist
+        Playlist {
+            remove: Some(mut args),
+            ..
+        } => {
+            let name = args.remove(0).into();
+            let rgx = args.join(" ").parse()?;
+            let ids = requests::search_karas(config, SearchFrom::Database, [rgx]).await?;
+            let remove = KaraFilter::List(true, ids);
+            requests::remove_from_playlist(config, name, remove).await
+        }
+
+        // ============= //
+        // Admin options //
+        // ============= //
+        Admin { kill: true, .. } => requests::shutdown_lektord(config).await,
+        Admin { update: true, .. } => requests::update_from_repo(config).await,
+
+        // ============================================== //
+        // Can't be there... unless no flag was passed... //
+        // ============================================== //
+        _ => unreachable!("{cmd:#?}"),
+    }
+}
diff --git a/lkt/src/main.rs b/lkt/src/main.rs
index 9afa21b2..8bc0685e 100644
--- a/lkt/src/main.rs
+++ b/lkt/src/main.rs
@@ -1,19 +1,12 @@
 #![forbid(unsafe_code)]
 
-include!(concat!(env!("OUT_DIR"), "/lkt_build_infos.rs"));
-
-mod args;
-mod config;
-mod manpage;
-
-use crate::{args::*, config::*};
-use anyhow::{anyhow, Context as _, Result};
-use chrono::TimeZone;
-use futures::{stream, StreamExt};
-use lektor_lib::*;
-use lektor_payloads::*;
+use anyhow::{Context as _, Result};
 use lektor_utils::*;
-use std::{borrow::Cow, ops::RangeBounds};
+use lkt::{
+    args::{self, *},
+    config::*,
+    manpage,
+};
 
 fn main() -> Result<()> {
     logger::Builder::default()
@@ -46,326 +39,6 @@ fn main() -> Result<()> {
     tokio::runtime::Builder::new_current_thread()
         .enable_all()
         .build()?
-        .block_on(exec_lkt(config, args.action))?;
+        .block_on(lkt::exec_lkt(config, args.action))?;
     Ok(())
 }
-
-async fn collect_karas(config: &ConnectConfig, ids: Vec<KId>) -> Result<Vec<Kara>> {
-    let count = ids.len();
-    let res: Vec<_> = stream::iter(ids.into_iter())
-        .then(|id| requests::get_kara_by_kid(config, id))
-        .filter_map(|res| async { res.map_err(|err| log::error!("{err}")).ok() })
-        .collect::<_>()
-        .await;
-    res.len()
-        .eq(&count)
-        .then_some(res)
-        .ok_or(anyhow!("got errors, didn't received all the karas' data"))
-}
-
-async fn simple_karas_print(config: &ConnectConfig, ids: Vec<KId>) -> Result<()> {
-    let res = collect_karas(config, ids).await?;
-    let count = res.len().to_string().len();
-    res.into_iter()
-        .enumerate()
-        .for_each(|(i, k)| println!("[{i:0>count$}] {k}"));
-    Ok(())
-}
-
-async fn exec_lkt(config: LktConfig, cmd: SubCommand) -> Result<()> {
-    // Write to apply changes...
-    lektor_utils::config::write_config_async("lkt", config.clone()).await?;
-    let config = ConnectConfig::from(config);
-    let config = &config;
-
-    use crate::args::SubCommand::*;
-    match cmd {
-        // ============= //
-        // Queue options //
-        // ============= //
-
-        // Display current kara in one line (for status lines...)
-        Playback { current: true, .. } => {
-            let PlayStateWithCurrent { state, current } = requests::get_status(config).await?;
-            match current {
-                None => println!("[{state:?}]"),
-                Some((kid, elapsed, duration)) => {
-                    let current = requests::get_kara_by_kid(config, kid).await?.to_string();
-                    println!("[{state:?}] {elapsed}/{duration} {current}");
-                }
-            }
-            Ok(())
-        }
-
-        // Display the status of lektord
-        Playback { status: true, .. } => {
-            let Infos {
-                version,
-                last_epoch,
-            } = requests::get_infos(config).await?;
-            let PlayStateWithCurrent { state, current } = requests::get_status(config).await?;
-            let current = match current {
-                None => None,
-                Some((kid, elapsed, duration)) => Some((
-                    requests::get_kara_by_kid(config, kid).await?,
-                    elapsed,
-                    duration,
-                )),
-            };
-            let queue_counts = requests::get_queue_count(config).await?;
-            let history_count = requests::get_history_count(config).await?;
-            let last_epoch = match last_epoch {
-                Some(num) => num.to_string().into(),
-                None => Cow::Borrowed("None"),
-            };
-
-            println!("Version:          {version}");
-            println!("Playback State:   {state:?}");
-            if let Some((current, elapsed, duration)) = current {
-                println!("Current Kara:     {current}");
-                println!("Kara elapsed:     {elapsed}s");
-                println!("Kara duration:    {duration}s");
-            }
-            println!("Karas in Queue:   {queue_counts:?}");
-            println!("Karas in Hustory: {history_count}");
-            println!("Database epoch:   {last_epoch}");
-
-            Ok(())
-        }
-
-        // Play from a position in the queue
-        Playback {
-            play: Some(play), ..
-        } => requests::play_from_position(config, play.unwrap_or_default()).await,
-
-        // Toggle the play state
-        Playback {
-            pause: Some(None), ..
-        } => requests::toggle_playback_state(config).await,
-
-        // Pause the playback
-        Playback {
-            pause: Some(Some(true)),
-            ..
-        } => requests::set_playback_state(config, PlayState::Pause).await,
-
-        // Force to resume the playback
-        Playback {
-            pause: Some(Some(false)),
-            ..
-        } => requests::set_playback_state(config, PlayState::Play).await,
-
-        Playback { next: true, .. } => requests::play_next(config).await,
-        Playback { prev: true, .. } => requests::play_previous(config).await,
-        Playback { stop: true, .. } => requests::set_playback_state(config, PlayState::Stop).await,
-
-        // ============= //
-        // Queue options //
-        // ============= //
-
-        // List kara in the queue
-        Queue { pos: Some(pos), .. } => {
-            let pos = pos.unwrap_or_default();
-            let ids = requests::get_queue_range(config, pos).await?;
-            let prios: Vec<_> = ids.iter().map(|(prio, _)| *prio).collect();
-            let karas: Vec<_> =
-                collect_karas(config, ids.into_iter().map(|(_, id)| id).collect()).await?;
-            let count = match pos {
-                Range::From(_) | Range::Full => prios.len().max(karas.len()),
-                Range::To(max) | Range::Bound(_, max) => max - 1,
-                Range::ToInclusive(max) | Range::BoundInclusive(_, max) => max,
-            };
-            let start = match pos.start_bound() {
-                std::ops::Bound::Included(start) => *start,
-                std::ops::Bound::Excluded(start) => start - 1,
-                std::ops::Bound::Unbounded => 0,
-            };
-            let count = count.to_string().len();
-            let space = PRIORITY_VALUES
-                .iter()
-                .map(|prio| prio.as_str().len())
-                .max()
-                .expect("oupsy daisy");
-            std::iter::zip(prios, karas)
-                .enumerate()
-                .for_each(|(i, (l, k))| println!("[{:0>count$}] {l:>space$}: {k}", start + i));
-            Ok(())
-        }
-
-        // Add karas to the queue
-        Queue { add: Some(add), .. } => {
-            let AddArguments { level: lvl, query } = add.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::Database, query).await?;
-            let add = QueueAddAction {
-                priority: lvl,
-                action: KaraFilter::List(true, ids),
-            };
-            requests::add_to_queue(config, add).await
-        }
-
-        // Remove kara from the queue by their position
-        Queue {
-            remove: Some(r), ..
-        } => requests::remove_range_from_queue(config, r.unwrap_or_default()).await,
-
-        // Shuffle the queue.
-        Queue {
-            shuffle: Some(shuffle),
-            ..
-        } => requests::shuffle_queue_range(config, shuffle.unwrap_or_default()).await,
-
-        // =============== //
-        // History options //
-        // =============== //
-        History { clear: true, .. } => requests::remove_range_from_history(config, ..).await,
-        History { pos: Some(pos), .. } => {
-            let ids = requests::get_history_range(config, pos.unwrap_or_default()).await?;
-            simple_karas_print(config, ids).await
-        }
-
-        // ============== //
-        // Search options //
-        // ============== //
-        Search {
-            database: Some(regex),
-            ..
-        } => {
-            let regex = regex.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::Database, regex).await?;
-            simple_karas_print(config, ids).await
-        }
-
-        Search {
-            count: Some(regex), ..
-        } => {
-            let regex = regex.join(" ");
-            let count = requests::count_karas(config, SearchFrom::Database, regex.parse()?).await?;
-            println!("Search in Database: {regex}");
-            println!("Matched Count:      {count}");
-            Ok(())
-        }
-
-        Search { get: Some(id), .. } => {
-            let kara = requests::get_kara_by_id(config, id).await?;
-            dbg!(kara);
-            Ok(())
-        }
-
-        Search {
-            plt: Some(mut args),
-            ..
-        } => {
-            let name = args.remove(0).parse()?;
-            let regex = args.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::Playlist(name), regex).await?;
-            simple_karas_print(config, ids).await
-        }
-
-        Search {
-            queue: Some(args), ..
-        } => {
-            let regex = args.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::Queue, regex).await?;
-            simple_karas_print(config, ids).await
-        }
-
-        Search {
-            history: Some(args),
-            ..
-        } => {
-            let regex = args.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::History, regex).await?;
-            simple_karas_print(config, ids).await
-        }
-
-        // ================ //
-        // Playlist options //
-        // ================ //
-
-        // Create a playlist
-        Playlist {
-            create: Some(n), ..
-        } => requests::create_playlist(config, n.into()).await,
-
-        // Delete a playlist and all its content.
-        Playlist {
-            destroy: Some(n), ..
-        } => requests::delete_playlist(config, n.into()).await,
-
-        // List all the playlists
-        Playlist {
-            list: Some(None), ..
-        } => {
-            let plts = requests::get_playlists(config).await?;
-            let count = plts.len().to_string().len();
-            let padding: String = " ".repeat(count);
-            for (idx, (name, infos)) in plts.into_iter().enumerate() {
-                let PlaylistInfo {
-                    user,
-                    created_at,
-                    updated_at,
-                } = infos;
-                if idx != 0 {
-                    println!();
-                }
-                println!("{idx:0>count$}: Playlist {name}");
-                if let Some(user) = user {
-                    println!("{padding}  Created by {user}");
-                }
-                if let Some(time) = chrono::Local.timestamp_opt(created_at, 0).latest() {
-                    let time = time.format("%Y-%m-%d %H:%M:%S");
-                    println!("{padding}  Created at {time}");
-                };
-                if let Some(time) = chrono::Local.timestamp_opt(updated_at, 0).latest() {
-                    let time = time.format("%Y-%m-%d %H:%M:%S");
-                    println!("{padding}  Last updated at {time}");
-                };
-            }
-            Ok(())
-        }
-
-        // List the content of a playlist
-        Playlist {
-            list: Some(Some(n)),
-            ..
-        } => {
-            let ids = requests::get_playlist_content(config, n.into()).await?;
-            simple_karas_print(config, ids).await
-        }
-
-        // Add something to a playlist.
-        Playlist {
-            add: Some(mut args),
-            ..
-        } => {
-            let name = args.remove(0).into();
-            let rgx = args.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::Database, rgx).await?;
-            let add = KaraFilter::List(true, ids);
-            requests::add_to_playlist(config, name, add).await
-        }
-
-        // Remove something from a playlist
-        Playlist {
-            remove: Some(mut args),
-            ..
-        } => {
-            let name = args.remove(0).into();
-            let rgx = args.join(" ").parse()?;
-            let ids = requests::search_karas(config, SearchFrom::Database, rgx).await?;
-            let remove = KaraFilter::List(true, ids);
-            requests::remove_from_playlist(config, name, remove).await
-        }
-
-        // ============= //
-        // Admin options //
-        // ============= //
-        Admin { kill: true, .. } => requests::shutdown_lektord(config).await,
-        Admin { update: true, .. } => requests::update_from_repo(config).await,
-
-        // ============================================== //
-        // Can't be there... unless no flag was passed... //
-        // ============================================== //
-        _ => unreachable!("{cmd:#?}"),
-    }
-}
-- 
GitLab