about summary refs log tree commit diff stats
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--.cargo/config.toml3
-rw-r--r--.envrc5
-rw-r--r--Cargo.lock1614
-rw-r--r--Cargo.toml23
-rw-r--r--LICENSES/Apache-2.0.txt73
-rw-r--r--NEWS.md295
-rw-r--r--cog.toml4
-rwxr-xr-x[-rw-r--r--]contrib/external_commands_script.sh (renamed from crates/bytes/.gitignore)12
-rw-r--r--crates/bytes/Cargo.lock65
-rw-r--r--crates/bytes/src/serde.rs19
-rw-r--r--crates/colors/Cargo.toml (renamed from crates/bytes/Cargo.toml)21
-rw-r--r--crates/colors/src/custom.rs75
-rw-r--r--crates/colors/src/lib.rs97
-rw-r--r--crates/colors/src/list.rs233
-rw-r--r--crates/colors/src/support.rs126
-rw-r--r--crates/fmt/Cargo.toml2
-rw-r--r--crates/libmpv2/CHANGELOG.md8
-rw-r--r--crates/libmpv2/Cargo.toml2
-rw-r--r--crates/libmpv2/examples/opengl.rs17
-rw-r--r--crates/libmpv2/libmpv2-sys/Cargo.toml2
-rw-r--r--crates/libmpv2/libmpv2-sys/build.rs4
-rw-r--r--crates/libmpv2/src/lib.rs2
-rw-r--r--crates/libmpv2/src/mpv.rs28
-rw-r--r--crates/libmpv2/src/mpv/events.rs50
-rw-r--r--crates/libmpv2/src/mpv/protocol.rs127
-rw-r--r--crates/libmpv2/src/mpv/render.rs48
-rw-r--r--crates/libmpv2/src/tests.rs24
-rwxr-xr-xcrates/libmpv2/update.sh4
-rw-r--r--crates/yt/Cargo.toml (renamed from yt/Cargo.toml)40
-rw-r--r--crates/yt/src/ansi_escape_codes.rs29
-rw-r--r--crates/yt/src/app.rs (renamed from yt/src/app.rs)8
-rw-r--r--crates/yt/src/cli.rs67
-rw-r--r--crates/yt/src/commands/cache/implm.rs40
-rw-r--r--crates/yt/src/commands/cache/mod.rs19
-rw-r--r--crates/yt/src/commands/config/implm.rs23
-rw-r--r--crates/yt/src/commands/config/mod.rs (renamed from yt/src/constants.rs)8
-rw-r--r--crates/yt/src/commands/database/implm.rs45
-rw-r--r--crates/yt/src/commands/database/mod.rs41
-rw-r--r--crates/yt/src/commands/download/implm/download/download_options.rs121
-rw-r--r--crates/yt/src/commands/download/implm/download/mod.rs290
-rw-r--r--crates/yt/src/commands/download/implm/download/progress_hook.rs175
-rw-r--r--crates/yt/src/commands/download/implm/mod.rs55
-rw-r--r--crates/yt/src/commands/download/mod.rs34
-rw-r--r--crates/yt/src/commands/implm.rs38
-rw-r--r--crates/yt/src/commands/mod.rs164
-rw-r--r--crates/yt/src/commands/playlist/implm.rs110
-rw-r--r--crates/yt/src/commands/playlist/mod.rs20
-rw-r--r--crates/yt/src/commands/select/implm/fs_generators/help.str (renamed from yt/src/select/selection_file/help.str)0
-rw-r--r--crates/yt/src/commands/select/implm/fs_generators/help.str.license (renamed from yt/src/select/selection_file/help.str.license)0
-rw-r--r--crates/yt/src/commands/select/implm/fs_generators/mod.rs355
-rw-r--r--crates/yt/src/commands/select/implm/mod.rs52
-rw-r--r--crates/yt/src/commands/select/implm/standalone/add.rs (renamed from yt/src/select/cmds/add.rs)131
-rw-r--r--crates/yt/src/commands/select/implm/standalone/mod.rs132
-rw-r--r--crates/yt/src/commands/select/mod.rs230
-rw-r--r--crates/yt/src/commands/show/implm/mod.rs110
-rw-r--r--crates/yt/src/commands/show/mod.rs30
-rw-r--r--crates/yt/src/commands/status/implm.rs157
-rw-r--r--crates/yt/src/commands/status/mod.rs20
-rw-r--r--crates/yt/src/commands/subscriptions/implm.rs253
-rw-r--r--crates/yt/src/commands/subscriptions/mod.rs62
-rw-r--r--crates/yt/src/commands/update/implm/mod.rs62
-rw-r--r--crates/yt/src/commands/update/implm/updater.rs205
-rw-r--r--crates/yt/src/commands/update/mod.rs27
-rw-r--r--crates/yt/src/commands/videos/implm.rs73
-rw-r--r--crates/yt/src/commands/videos/mod.rs46
-rw-r--r--crates/yt/src/commands/watch/implm/mod.rs244
-rw-r--r--crates/yt/src/commands/watch/implm/playlist_handler/client_messages.rs (renamed from yt/src/watch/playlist_handler/client_messages/mod.rs)45
-rw-r--r--crates/yt/src/commands/watch/implm/playlist_handler/mod.rs225
-rw-r--r--crates/yt/src/commands/watch/mod.rs24
-rw-r--r--crates/yt/src/config/mod.rs138
-rw-r--r--crates/yt/src/config/non_empty_vec.rs83
-rw-r--r--crates/yt/src/config/paths.rs58
-rw-r--r--crates/yt/src/config/support.rs161
-rw-r--r--crates/yt/src/main.rs89
-rw-r--r--crates/yt/src/output/mod.rs (renamed from yt/src/comments/output.rs)17
-rw-r--r--crates/yt/src/select/duration.rs240
-rw-r--r--crates/yt/src/select/mod.rs35
-rw-r--r--crates/yt/src/shared/bytes/error.rs (renamed from crates/bytes/src/error.rs)0
-rw-r--r--crates/yt/src/shared/bytes/mod.rs (renamed from crates/bytes/src/lib.rs)24
-rw-r--r--crates/yt/src/shared/mod.rs (renamed from crates/yt_dlp/src/wrapper/mod.rs)5
-rw-r--r--crates/yt/src/storage/db/extractor_hash.rs220
-rw-r--r--crates/yt/src/storage/db/get/extractor_hash.rs68
-rw-r--r--crates/yt/src/storage/db/get/mod.rs15
-rw-r--r--crates/yt/src/storage/db/get/playlist.rs68
-rw-r--r--crates/yt/src/storage/db/get/subscription.rs49
-rw-r--r--crates/yt/src/storage/db/get/txn_log.rs43
-rw-r--r--crates/yt/src/storage/db/get/video/mod.rs261
-rw-r--r--crates/yt/src/storage/db/insert/maintenance.rs38
-rw-r--r--crates/yt/src/storage/db/insert/mod.rs115
-rw-r--r--crates/yt/src/storage/db/insert/playlist.rs222
-rw-r--r--crates/yt/src/storage/db/insert/subscription.rs95
-rw-r--r--crates/yt/src/storage/db/insert/video/mod.rs610
-rw-r--r--crates/yt/src/storage/db/mod.rs18
-rw-r--r--crates/yt/src/storage/db/playlist/mod.rs59
-rw-r--r--crates/yt/src/storage/db/subscription.rs52
-rw-r--r--crates/yt/src/storage/db/txn_log.rs24
-rw-r--r--crates/yt/src/storage/db/video/comments/display.rs (renamed from yt/src/comments/display.rs)73
-rw-r--r--crates/yt/src/storage/db/video/comments/mod.rs202
-rw-r--r--crates/yt/src/storage/db/video/comments/raw.rs87
-rw-r--r--crates/yt/src/storage/db/video/comments/tests.rs249
-rw-r--r--crates/yt/src/storage/db/video/mod.rs (renamed from yt/src/storage/video_database/mod.rs)205
-rw-r--r--crates/yt/src/storage/migrate/mod.rs (renamed from yt/src/storage/migrate/mod.rs)229
-rw-r--r--crates/yt/src/storage/migrate/sql/0_Empty_to_Zero.sql (renamed from yt/src/storage/migrate/sql/00_empty_to_zero.sql)0
-rw-r--r--crates/yt/src/storage/migrate/sql/1_Zero_to_One.sql (renamed from yt/src/storage/migrate/sql/01_zero_to_one.sql)0
-rw-r--r--crates/yt/src/storage/migrate/sql/2_One_to_Two.sql (renamed from yt/src/storage/migrate/sql/02_one_to_two.sql)0
-rw-r--r--crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql85
-rw-r--r--crates/yt/src/storage/migrate/sql/4_Three_to_Four.sql24
-rw-r--r--crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql15
-rw-r--r--crates/yt/src/storage/mod.rs (renamed from yt/src/storage/mod.rs)6
-rw-r--r--crates/yt/src/storage/notify.rs (renamed from yt/src/storage/video_database/notify.rs)4
-rw-r--r--crates/yt/src/version/mod.rs (renamed from yt/src/version/mod.rs)31
-rw-r--r--crates/yt/src/videos/format_video.rs133
-rw-r--r--crates/yt/src/videos/mod.rs213
-rw-r--r--crates/yt/src/yt_dlp/mod.rs253
-rw-r--r--crates/yt/tests/_testenv/init.rs136
-rw-r--r--crates/yt/tests/_testenv/mod.rs35
-rw-r--r--crates/yt/tests/_testenv/run.rs183
-rw-r--r--crates/yt/tests/_testenv/util.rs371
-rw-r--r--crates/yt/tests/select/base.rs50
-rw-r--r--crates/yt/tests/select/file.rs31
-rw-r--r--crates/yt/tests/select/mod.rs25
-rw-r--r--crates/yt/tests/select/options.rs51
-rw-r--r--crates/yt/tests/subscriptions/import_export/golden.txt2
-rw-r--r--crates/yt/tests/subscriptions/import_export/golden.txt.license (renamed from crates/yt_dlp/src/python_json_decode_failed.error_msg.license)0
-rw-r--r--crates/yt/tests/subscriptions/import_export/mod.rs35
-rw-r--r--crates/yt/tests/subscriptions/mod.rs12
-rw-r--r--crates/yt/tests/subscriptions/naming_subscriptions/golden.txt2
-rw-r--r--crates/yt/tests/subscriptions/naming_subscriptions/golden.txt.license (renamed from crates/bytes/Cargo.lock.license)2
-rw-r--r--crates/yt/tests/subscriptions/naming_subscriptions/mod.rs33
-rw-r--r--crates/yt/tests/tests.rs22
-rw-r--r--crates/yt/tests/videos/downloading.rs52
-rw-r--r--crates/yt/tests/videos/mod.rs11
-rw-r--r--crates/yt/tests/watch/focus_switch.rs53
-rw-r--r--crates/yt/tests/watch/mod.rs135
-rw-r--r--crates/yt_dlp/Cargo.toml15
-rw-r--r--crates/yt_dlp/README.md2
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/.gitignore (renamed from crates/yt_dlp/.cargo/config.toml)9
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/Cargo.toml31
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/LICENSE201
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/README.md160
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/src/kv.rs127
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/src/level.rs43
-rw-r--r--crates/yt_dlp/crates/pyo3-pylogger/src/lib.rs211
-rwxr-xr-xcrates/yt_dlp/crates/pyo3-pylogger/update.sh (renamed from crates/bytes/update.sh)8
-rw-r--r--crates/yt_dlp/examples/main.rs15
-rw-r--r--crates/yt_dlp/src/duration.rs78
-rw-r--r--crates/yt_dlp/src/error.rs68
-rw-r--r--crates/yt_dlp/src/info_json.rs56
-rw-r--r--crates/yt_dlp/src/lib.rs795
-rw-r--r--crates/yt_dlp/src/logging.rs133
-rw-r--r--crates/yt_dlp/src/options.rs207
-rw-r--r--crates/yt_dlp/src/post_processors/dearrow.rs247
-rw-r--r--crates/yt_dlp/src/post_processors/mod.rs48
-rw-r--r--crates/yt_dlp/src/progress_hook.rs67
-rw-r--r--crates/yt_dlp/src/python_error.rs55
-rw-r--r--crates/yt_dlp/src/python_json_decode_failed.error_msg5
-rw-r--r--crates/yt_dlp/src/tests.rs89
-rw-r--r--crates/yt_dlp/src/wrapper/info_json.rs824
-rw-r--r--crates/yt_dlp/src/wrapper/yt_dlp_options.rs62
-rwxr-xr-xcrates/yt_dlp/update.sh4
-rw-r--r--flake.lock48
-rw-r--r--flake.nix34
-rw-r--r--nix/package.nix138
-rw-r--r--package/package.nix75
-rwxr-xr-xscripts/mkdb.sh11
-rwxr-xr-xupdate.sh2
-rw-r--r--yt/src/cache/mod.rs105
-rw-r--r--yt/src/cli.rs383
-rw-r--r--yt/src/comments/comment.rs65
-rw-r--r--yt/src/comments/description.rs44
-rw-r--r--yt/src/comments/mod.rs165
-rw-r--r--yt/src/config/default.rs110
-rw-r--r--yt/src/config/definitions.rs67
-rw-r--r--yt/src/config/file_system.rs120
-rw-r--r--yt/src/config/mod.rs76
-rw-r--r--yt/src/download/download_options.rs113
-rw-r--r--yt/src/download/mod.rs363
-rw-r--r--yt/src/main.rs256
-rw-r--r--yt/src/select/cmds/mod.rs106
-rw-r--r--yt/src/select/mod.rs173
-rw-r--r--yt/src/select/selection_file/duration.rs185
-rw-r--r--yt/src/select/selection_file/mod.rs32
-rw-r--r--yt/src/status/mod.rs120
-rw-r--r--yt/src/storage/subscriptions.rs144
-rw-r--r--yt/src/storage/video_database/downloader.rs130
-rw-r--r--yt/src/storage/video_database/extractor_hash.rs163
-rw-r--r--yt/src/storage/video_database/get/mod.rs303
-rw-r--r--yt/src/storage/video_database/get/playlist/iterator.rs101
-rw-r--r--yt/src/storage/video_database/get/playlist/mod.rs167
-rw-r--r--yt/src/storage/video_database/set/mod.rs341
-rw-r--r--yt/src/storage/video_database/set/playlist.rs81
-rw-r--r--yt/src/subscribe/mod.rs187
-rw-r--r--yt/src/unreachable.rs50
-rw-r--r--yt/src/update/mod.rs196
-rw-r--r--yt/src/update/updater.rs171
-rw-r--r--yt/src/videos/display/format_video.rs94
-rw-r--r--yt/src/videos/display/mod.rs229
-rw-r--r--yt/src/videos/mod.rs67
-rw-r--r--yt/src/watch/mod.rs172
-rw-r--r--yt/src/watch/playlist.rs109
-rw-r--r--yt/src/watch/playlist_handler/mod.rs339
201 files changed, 13473 insertions, 8572 deletions
diff --git a/.cargo/config.toml b/.cargo/config.toml
index 338862f..94d197f 100644
--- a/.cargo/config.toml
+++ b/.cargo/config.toml
@@ -8,8 +8,5 @@
 # You should have received a copy of the License along with this program.
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-[env]
-PYO3_PYTHON = "/nix/store/7xzk119acyws2c4ysygdv66l0grxkr39-python3-3.11.9-env/bin/python3"
-
 [build]
 rustflags = ["-Clink-arg=-fuse-ld=mold", "-Ctarget-cpu=native"]
diff --git a/.envrc b/.envrc
index 5537ab5..adb00cc 100644
--- a/.envrc
+++ b/.envrc
@@ -20,5 +20,8 @@ PATH_add ./target/release
 PATH_add ./target/profiling
 
 export DATABASE_URL="sqlite://$root/target/database.sqlx"
+
+# Plugins are not supported.
+export YTDLP_NO_PLUGINS=1
+
 export PYO3_PYTHON=python3
-export YT_STORE_INFO_JSON=yes
diff --git a/Cargo.lock b/Cargo.lock
index f12d722..5958efa 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,6 +1,6 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
-version = 3
+version = 4
 
 [[package]]
 name = "addr2line"
@@ -13,9 +13,9 @@ dependencies = [
 
 [[package]]
 name = "adler2"
-version = "2.0.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
 [[package]]
 name = "aho-corasick"
@@ -49,9 +49,9 @@ dependencies = [
 
 [[package]]
 name = "anstream"
-version = "0.6.18"
+version = "0.6.19"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8acc5369981196006228e28809f761875c0327210a891e941f4c683b3a99529b"
+checksum = "301af1932e46185686725e0fad2f8f2aa7da69dd70bf6ecc44d6b703844a3933"
 dependencies = [
  "anstyle",
  "anstyle-parse",
@@ -64,44 +64,44 @@ dependencies = [
 
 [[package]]
 name = "anstyle"
-version = "1.0.10"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "55cc3b69f167a1ef2e161439aa98aed94e6028e5f9a59be9a6ffb47aef1651f9"
+checksum = "862ed96ca487e809f1c8e5a8447f6ee2cf102f846893800b20cebdf541fc6bbd"
 
 [[package]]
 name = "anstyle-parse"
-version = "0.2.6"
+version = "0.2.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b2d16507662817a6a20a9ea92df6652ee4f94f914589377d69f3b21bc5798a9"
+checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2"
 dependencies = [
  "utf8parse",
 ]
 
 [[package]]
 name = "anstyle-query"
-version = "1.1.2"
+version = "1.1.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "79947af37f4177cfead1110013d678905c37501914fba0efea834c3fe9a8d60c"
+checksum = "6c8bdeb6047d8983be085bab0ba1472e6dc604e7041dbf6fcd5e71523014fae9"
 dependencies = [
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anstyle-wincon"
-version = "3.0.7"
+version = "3.0.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ca3534e77181a9cc07539ad51f2141fe32f6c3ffd4df76db8ad92346b003ae4e"
+checksum = "403f75924867bb1033c59fbf0797484329750cfbe3c4325cd33127941fabc882"
 dependencies = [
  "anstyle",
- "once_cell",
+ "once_cell_polyfill",
  "windows-sys 0.59.0",
 ]
 
 [[package]]
 name = "anyhow"
-version = "1.0.96"
+version = "1.0.98"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b964d184e89d9b6b67dd2715bc8e74cf3107fb2b529990c90cf517326150bf4"
+checksum = "e16d2d3311acee920a9eb8d33b8cbc1787ce4a264e85f964c2404b969bdcd487"
 
 [[package]]
 name = "arrayref"
@@ -125,16 +125,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
 name = "autocfg"
-version = "1.4.0"
+version = "1.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
+checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
 name = "backtrace"
-version = "0.3.74"
+version = "0.3.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
+checksum = "6806a6321ec58106fea15becdad98371e28d92ccbc7c8f1b3b6dd724fe8f1002"
 dependencies = [
  "addr2line",
  "cfg-if",
@@ -153,17 +159,17 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
 
 [[package]]
 name = "base64ct"
-version = "1.6.0"
+version = "1.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+checksum = "55248b47b0caf0546f7988906588779981c43bb1bc9d0c44087278f80cdb44ba"
 
 [[package]]
 name = "bindgen"
-version = "0.71.1"
+version = "0.72.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3"
+checksum = "4f72209734318d0b619a5e0f5129918b848c416e122a3c4ce054e03cb87b726f"
 dependencies = [
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
  "cexpr",
  "clang-sys",
  "itertools",
@@ -185,25 +191,25 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
 
 [[package]]
 name = "bitflags"
-version = "2.8.0"
+version = "2.9.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f68f53c83ab957f72c32642f3868eec03eb974d1fb82e453128456482613d36"
+checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "blake3"
-version = "1.6.0"
+version = "1.8.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1230237285e3e10cde447185e8975408ae24deaa67205ce684805c25bc0c7937"
+checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
 dependencies = [
  "arrayref",
  "arrayvec",
  "cc",
  "cfg-if",
  "constant_time_eq",
- "memmap2",
+ "serde",
 ]
 
 [[package]]
@@ -217,9 +223,9 @@ dependencies = [
 
 [[package]]
 name = "bumpalo"
-version = "3.17.0"
+version = "3.19.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1628fb46dfa0b37568d12e5edd512553eccf6a22a78e8bde00bb4aed84d5bdbf"
+checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
 
 [[package]]
 name = "byteorder"
@@ -229,22 +235,15 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
 
 [[package]]
 name = "bytes"
-version = "1.4.1"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "bytes"
-version = "1.10.0"
+version = "1.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
+checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
 
 [[package]]
 name = "cc"
-version = "1.2.15"
+version = "1.2.29"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c736e259eea577f443d5c86c304f9f4ae0295c43f3ba05c21f1d66b5f06001af"
+checksum = "5c1599538de2394445747c8cf7935946e3cc27e9625f889d979bfb2aaf569362"
 dependencies = [
  "shlex",
 ]
@@ -260,22 +259,22 @@ dependencies = [
 
 [[package]]
 name = "cfg-if"
-version = "1.0.0"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268"
 
 [[package]]
 name = "chrono"
-version = "0.4.39"
+version = "0.4.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825"
+checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d"
 dependencies = [
  "android-tzdata",
  "iana-time-zone",
  "js-sys",
  "num-traits",
  "wasm-bindgen",
- "windows-targets 0.52.6",
+ "windows-link",
 ]
 
 [[package]]
@@ -300,9 +299,9 @@ dependencies = [
 
 [[package]]
 name = "clap"
-version = "4.5.30"
+version = "4.5.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92b7b18d71fad5313a1e320fa9897994228ce274b60faa4d694fe0ea89cd9e6d"
+checksum = "be92d32e80243a54711e5d7ce823c35c41c9d929dc4ab58e1276f625841aadf9"
 dependencies = [
  "clap_builder",
  "clap_derive",
@@ -310,9 +309,9 @@ dependencies = [
 
 [[package]]
 name = "clap_builder"
-version = "4.5.30"
+version = "4.5.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a35db2071778a7344791a4fb4f95308b5673d219dee3ae348b86642574ecc90c"
+checksum = "707eab41e9622f9139419d573eca0900137718000c517d47da73045f54331c3d"
 dependencies = [
  "anstream",
  "anstyle",
@@ -321,10 +320,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "clap_complete"
+version = "4.5.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5abde44486daf70c5be8b8f8f1b66c49f86236edf6fa2abadb4d961c4c6229a"
+dependencies = [
+ "clap",
+ "clap_lex",
+ "is_executable",
+ "shlex",
+]
+
+[[package]]
 name = "clap_derive"
-version = "4.5.28"
+version = "4.5.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf4ced95c6f4a675af3da73304b9ac4ed991640c36374e4b46795c49e17cf1ed"
+checksum = "ef4f52386a59ca4c860f7393bcf8abd8dfd91ecccc0f774635ff68e92eeef491"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -334,15 +345,19 @@ dependencies = [
 
 [[package]]
 name = "clap_lex"
-version = "0.7.4"
+version = "0.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f46ad14479a25103f283c0f10005961cf086d8dc42205bb44c46ac563475dca6"
+checksum = "b94f61472cee1439c0b966b47e3aca9ae07e45d070759512cd390ea2bebc6675"
 
 [[package]]
 name = "colorchoice"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5b63caa9aa9397e2d9480a9b13673856c78d8ac123288526c37d7839f2a86990"
+checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75"
+
+[[package]]
+name = "colors"
+version = "1.8.0"
 
 [[package]]
 name = "concurrent-queue"
@@ -366,6 +381,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
 
 [[package]]
+name = "core-foundation"
+version = "0.9.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "core-foundation-sys"
 version = "0.8.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -382,9 +407,9 @@ dependencies = [
 
 [[package]]
 name = "crc"
-version = "3.2.1"
+version = "3.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69e6e4d7b33a94f0991c26729976b10ebde1d34c3ee82408fb536164fa10d636"
+checksum = "9710d3b3739c2e349eb44fe848ad0b7c8cb1e42bd87ee49371df2f7acaf3e675"
 dependencies = [
  "crc-catalog",
 ]
@@ -410,9 +435,9 @@ dependencies = [
 
 [[package]]
 name = "crossbeam-channel"
-version = "0.5.14"
+version = "0.5.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "06ba6d68e24814cb8de6bb986db8222d3a027d15872cabc0d18817bc3c0e4471"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
 dependencies = [
  "crossbeam-utils",
 ]
@@ -462,10 +487,40 @@ dependencies = [
 ]
 
 [[package]]
+name = "curl"
+version = "0.4.48"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e2d5c8f48d9c0c23250e52b55e82a6ab4fdba6650c931f5a0a57a43abda812b"
+dependencies = [
+ "curl-sys",
+ "libc",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "socket2",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "curl-sys"
+version = "0.4.82+curl-8.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4d63638b5ec65f1a4ae945287b3fd035be4554bbaf211901159c9a2a74fb5be"
+dependencies = [
+ "cc",
+ "libc",
+ "libz-sys",
+ "openssl-sys",
+ "pkg-config",
+ "vcpkg",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "der"
-version = "0.7.9"
+version = "0.7.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0"
+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
 dependencies = [
  "const-oid",
  "pem-rfc7468",
@@ -473,6 +528,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "diff"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8"
+
+[[package]]
 name = "digest"
 version = "0.10.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -503,14 +564,23 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
 
 [[package]]
 name = "either"
-version = "1.13.0"
+version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60b1af1c220855b6ceac025d3f6ecdd2b7c4894bfe9cd9bda4fbb4bc7c0d4cf0"
+checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 dependencies = [
  "serde",
 ]
 
 [[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
 name = "equivalent"
 version = "1.0.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -518,12 +588,12 @@ checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
 name = "errno"
-version = "0.3.10"
+version = "0.3.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
+checksum = "778e2ac28f6c47af28e4907f13ffd1e1ddbd400980a9abd7c8df189bf578a5ad"
 dependencies = [
  "libc",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -555,18 +625,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
 
 [[package]]
-name = "filetime"
-version = "0.2.25"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "35c0522e981e68cbfa8c3f978441a5f34b30b96e146b33cd3359176b50fe8586"
-dependencies = [
- "cfg-if",
- "libc",
- "libredox",
- "windows-sys 0.59.0",
-]
-
-[[package]]
 name = "flume"
 version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -578,10 +636,31 @@ dependencies = [
 ]
 
 [[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
 name = "foldhash"
-version = "0.1.4"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a0d2fde1f7b3d48b8395d5f2de76c18a528bd6a9cdde438df747bfcba3e05d6f"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared",
+]
+
+[[package]]
+name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
 
 [[package]]
 name = "form_urlencoded"
@@ -704,25 +783,25 @@ dependencies = [
 
 [[package]]
 name = "getrandom"
-version = "0.2.15"
+version = "0.2.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
+checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi 0.11.0+wasi-snapshot-preview1",
+ "wasi 0.11.1+wasi-snapshot-preview1",
 ]
 
 [[package]]
 name = "getrandom"
-version = "0.3.1"
+version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
+checksum = "26145e563e54f2cadc477553f1ec5ee650b00862f0a58bcd12cbdc5f0ea2d2f4"
 dependencies = [
  "cfg-if",
  "libc",
- "wasi 0.13.3+wasi-0.2.2",
- "windows-targets 0.52.6",
+ "r-efi",
+ "wasi 0.14.2+wasi-0.2.4",
 ]
 
 [[package]]
@@ -738,10 +817,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2"
 
 [[package]]
+name = "h2"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17da50a276f1e01e0ba6c029e47b7100754904ee8a278f886546e98575380785"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
 name = "hashbrown"
-version = "0.15.2"
+version = "0.15.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289"
+checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5"
 dependencies = [
  "allocator-api2",
  "equivalent",
@@ -765,9 +863,9 @@ checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
 
 [[package]]
 name = "hermit-abi"
-version = "0.4.0"
+version = "0.5.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fbf6a919d6cf397374f7dfeeea91d974c7c0a7221d0d0f4f20d859d329e53fcc"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 
 [[package]]
 name = "hex"
@@ -803,15 +901,134 @@ dependencies = [
 ]
 
 [[package]]
+name = "http"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
+dependencies = [
+ "bytes",
+ "fnv",
+ "itoa",
+]
+
+[[package]]
+name = "http-body"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184"
+dependencies = [
+ "bytes",
+ "http",
+]
+
+[[package]]
+name = "http-body-util"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "http",
+ "http-body",
+ "pin-project-lite",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "hyper"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc2b571658e38e0c01b1fdca3bbbe93c00d3d71693ff2770043f8c29bc7d6f80"
+dependencies = [
+ "bytes",
+ "futures-channel",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "smallvec",
+ "tokio",
+ "want",
+]
+
+[[package]]
+name = "hyper-rustls"
+version = "0.27.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58"
+dependencies = [
+ "http",
+ "hyper",
+ "hyper-util",
+ "rustls",
+ "rustls-pki-types",
+ "tokio",
+ "tokio-rustls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-tls"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0"
+dependencies = [
+ "bytes",
+ "http-body-util",
+ "hyper",
+ "hyper-util",
+ "native-tls",
+ "tokio",
+ "tokio-native-tls",
+ "tower-service",
+]
+
+[[package]]
+name = "hyper-util"
+version = "0.1.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f66d5bd4c6f02bf0542fad85d626775bab9258cf795a4256dcaf3161114d1df"
+dependencies = [
+ "base64",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "futures-util",
+ "http",
+ "http-body",
+ "hyper",
+ "ipnet",
+ "libc",
+ "percent-encoding",
+ "pin-project-lite",
+ "socket2",
+ "system-configuration",
+ "tokio",
+ "tower-service",
+ "tracing",
+ "windows-registry",
+]
+
+[[package]]
 name = "iana-time-zone"
-version = "0.1.61"
+version = "0.1.63"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220"
+checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8"
 dependencies = [
  "android_system_properties",
  "core-foundation-sys",
  "iana-time-zone-haiku",
  "js-sys",
+ "log",
  "wasm-bindgen",
  "windows-core",
 ]
@@ -827,21 +1044,22 @@ dependencies = [
 
 [[package]]
 name = "icu_collections"
-version = "1.5.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+checksum = "200072f5d0e3614556f94a9930d5dc3e0662a652823904c3a75dc3b0af7fee47"
 dependencies = [
  "displaydoc",
+ "potential_utf",
  "yoke",
  "zerofrom",
  "zerovec",
 ]
 
 [[package]]
-name = "icu_locid"
-version = "1.5.0"
+name = "icu_locale_core"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+checksum = "0cde2700ccaed3872079a65fb1a78f6c0a36c91570f28755dda67bc8f7d9f00a"
 dependencies = [
  "displaydoc",
  "litemap",
@@ -851,30 +1069,10 @@ dependencies = [
 ]
 
 [[package]]
-name = "icu_locid_transform"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e"
-dependencies = [
- "displaydoc",
- "icu_locid",
- "icu_locid_transform_data",
- "icu_provider",
- "tinystr",
- "zerovec",
-]
-
-[[package]]
-name = "icu_locid_transform_data"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e"
-
-[[package]]
 name = "icu_normalizer"
-version = "1.5.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+checksum = "436880e8e18df4d7bbc06d58432329d6458cc84531f7ac5f024e93deadb37979"
 dependencies = [
  "displaydoc",
  "icu_collections",
@@ -882,68 +1080,55 @@ dependencies = [
  "icu_properties",
  "icu_provider",
  "smallvec",
- "utf16_iter",
- "utf8_iter",
- "write16",
  "zerovec",
 ]
 
 [[package]]
 name = "icu_normalizer_data"
-version = "1.5.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+checksum = "00210d6893afc98edb752b664b8890f0ef174c8adbb8d0be9710fa66fbbf72d3"
 
 [[package]]
 name = "icu_properties"
-version = "1.5.1"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5"
+checksum = "016c619c1eeb94efb86809b015c58f479963de65bdb6253345c1a1276f22e32b"
 dependencies = [
  "displaydoc",
  "icu_collections",
- "icu_locid_transform",
+ "icu_locale_core",
  "icu_properties_data",
  "icu_provider",
- "tinystr",
+ "potential_utf",
+ "zerotrie",
  "zerovec",
 ]
 
 [[package]]
 name = "icu_properties_data"
-version = "1.5.0"
+version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+checksum = "298459143998310acd25ffe6810ed544932242d3f07083eee1084d83a71bd632"
 
 [[package]]
 name = "icu_provider"
-version = "1.5.0"
+version = "2.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+checksum = "03c80da27b5f4187909049ee2d72f276f0d9f99a42c306bd0131ecfe04d8e5af"
 dependencies = [
  "displaydoc",
- "icu_locid",
- "icu_provider_macros",
+ "icu_locale_core",
  "stable_deref_trait",
  "tinystr",
  "writeable",
  "yoke",
  "zerofrom",
+ "zerotrie",
  "zerovec",
 ]
 
 [[package]]
-name = "icu_provider_macros"
-version = "1.5.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
 name = "idna"
 version = "1.0.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -956,9 +1141,9 @@ dependencies = [
 
 [[package]]
 name = "idna_adapter"
-version = "1.2.0"
+version = "1.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
 dependencies = [
  "icu_normalizer",
  "icu_properties",
@@ -966,9 +1151,9 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.7.1"
+version = "2.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652"
+checksum = "fe4cd85333e22411419a0bcae1297d25e58c9443848b11dc6a86fefe8c78a661"
 dependencies = [
  "equivalent",
  "hashbrown",
@@ -976,9 +1161,9 @@ dependencies = [
 
 [[package]]
 name = "indoc"
-version = "2.0.5"
+version = "2.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
+checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
 
 [[package]]
 name = "inotify"
@@ -986,7 +1171,7 @@ version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f37dccff2791ab604f9babef0ba14fbe0be30bd368dc541e2b08d07c8aa908f3"
 dependencies = [
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
  "inotify-sys",
  "libc",
 ]
@@ -1001,10 +1186,37 @@ dependencies = [
 ]
 
 [[package]]
+name = "io-uring"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b86e202f00093dcba4275d4636b93ef9dd75d025ae560d2521b45ea28ab49013"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "ipnet"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130"
+
+[[package]]
+name = "iri-string"
+version = "0.7.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc5ebe9c3a1a7a5127f920a418f7585e9e758e911d0466ed004f393b0e380b2"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
 name = "is-terminal"
-version = "0.4.15"
+version = "0.4.16"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e19b23d53f35ce9f56aebc7d1bb4e6ac1e9c0db7ac85c8d1760c04379edced37"
+checksum = "e04d7f318608d35d4b61ddd75cbdaee86b023ebe2bd5a66ee0915f0bf93095a9"
 dependencies = [
  "hermit-abi",
  "libc",
@@ -1012,6 +1224,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "is_executable"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4a1b5bad6f9072935961dfbf1cced2f3d129963d091b6f69f007fe04e758ae2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
 name = "is_terminal_polyfill"
 version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1028,9 +1249,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.14"
+version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
+checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
 
 [[package]]
 name = "js-sys"
@@ -1044,9 +1265,9 @@ dependencies = [
 
 [[package]]
 name = "kqueue"
-version = "1.0.8"
+version = "1.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7447f1ca1b7b563588a205fe93dea8df60fd981423a768bc1c0ded35ed147d0c"
+checksum = "eac30106d7dce88daf4a3fcb4879ea939476d5074a9b7ddd0fb97fa4bed5596a"
 dependencies = [
  "kqueue-sys",
  "libc",
@@ -1073,29 +1294,29 @@ dependencies = [
 
 [[package]]
 name = "libc"
-version = "0.2.169"
+version = "0.2.174"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a"
+checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776"
 
 [[package]]
 name = "libloading"
-version = "0.8.6"
+version = "0.8.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
+checksum = "07033963ba89ebaf1584d767badaa2e8fcec21aedea6b8c0346d487d49c28667"
 dependencies = [
  "cfg-if",
- "windows-targets 0.52.6",
+ "windows-targets 0.53.2",
 ]
 
 [[package]]
 name = "libm"
-version = "0.2.11"
+version = "0.2.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
+checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de"
 
 [[package]]
 name = "libmpv2"
-version = "1.4.1"
+version = "1.8.0"
 dependencies = [
  "crossbeam",
  "libmpv2-sys",
@@ -1105,50 +1326,51 @@ dependencies = [
 
 [[package]]
 name = "libmpv2-sys"
-version = "1.4.1"
+version = "1.8.0"
 dependencies = [
  "bindgen",
 ]
 
 [[package]]
-name = "libredox"
-version = "0.1.3"
+name = "libsqlite3-sys"
+version = "0.30.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d"
+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
 dependencies = [
- "bitflags 2.8.0",
- "libc",
- "redox_syscall",
+ "cc",
+ "pkg-config",
+ "vcpkg",
 ]
 
 [[package]]
-name = "libsqlite3-sys"
-version = "0.30.1"
+name = "libz-sys"
+version = "1.1.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
+checksum = "8b70e7a7df205e92a1a4cd9aaae7898dac0aa555503cc0a649494d0d60e7651d"
 dependencies = [
  "cc",
+ "libc",
  "pkg-config",
  "vcpkg",
 ]
 
 [[package]]
 name = "linux-raw-sys"
-version = "0.4.15"
+version = "0.9.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab"
+checksum = "cd945864f07fe9f5371a27ad7b52a172b4b499999f1d97574c9fa68373937e12"
 
 [[package]]
 name = "litemap"
-version = "0.7.4"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104"
+checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956"
 
 [[package]]
 name = "lock_api"
-version = "0.4.12"
+version = "0.4.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+checksum = "96936507f153605bddfcda068dd804796c84324ed2510809e5b2a624c81da765"
 dependencies = [
  "autocfg",
  "scopeguard",
@@ -1156,9 +1378,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.26"
+version = "0.4.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30bde2b3dc3671ae49d8e2e9f044c7c005836e7a023ee57cffa25ab82764bb9e"
+checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
 
 [[package]]
 name = "md-5"
@@ -1172,18 +1394,9 @@ dependencies = [
 
 [[package]]
 name = "memchr"
-version = "2.7.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
-
-[[package]]
-name = "memmap2"
-version = "0.9.5"
+version = "2.7.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fd3f7eed9d3848f8b98834af67102b720745c4ec028fcd0aa0239277e7de374f"
-dependencies = [
- "libc",
-]
+checksum = "32a282da65faaf38286cf3be983213fcf1d2e2a58700e808f83f4ea9a4804bc0"
 
 [[package]]
 name = "memoffset"
@@ -1195,6 +1408,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
 name = "minimal-lexical"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1202,23 +1421,40 @@ checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
 
 [[package]]
 name = "miniz_oxide"
-version = "0.8.5"
+version = "0.8.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e3e04debbb59698c15bacbb6d93584a8c0ca9cc3213cb423d31f760d8843ce5"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
 dependencies = [
  "adler2",
 ]
 
 [[package]]
 name = "mio"
-version = "1.0.3"
+version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
+checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c"
 dependencies = [
  "libc",
  "log",
- "wasi 0.11.0+wasi-snapshot-preview1",
- "windows-sys 0.52.0",
+ "wasi 0.11.1+wasi-snapshot-preview1",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "native-tls"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e"
+dependencies = [
+ "libc",
+ "log",
+ "openssl",
+ "openssl-probe",
+ "openssl-sys",
+ "schannel",
+ "security-framework",
+ "security-framework-sys",
+ "tempfile",
 ]
 
 [[package]]
@@ -1233,12 +1469,11 @@ dependencies = [
 
 [[package]]
 name = "notify"
-version = "8.0.0"
+version = "8.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2fee8403b3d66ac7b26aee6e40a897d85dc5ce26f44da36b8b73e987cc52e943"
+checksum = "3163f59cd3fa0e9ef8c32f242966a7b9994fd7378366099593e0e73077cd8c97"
 dependencies = [
- "bitflags 2.8.0",
- "filetime",
+ "bitflags 2.9.1",
  "inotify",
  "kqueue",
  "libc",
@@ -1246,7 +1481,7 @@ dependencies = [
  "mio",
  "notify-types",
  "walkdir",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
 ]
 
 [[package]]
@@ -1256,16 +1491,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
 
 [[package]]
-name = "nucleo-matcher"
-version = "0.3.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
-dependencies = [
- "memchr",
- "unicode-segmentation",
-]
-
-[[package]]
 name = "num-bigint-dig"
 version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1323,15 +1548,59 @@ dependencies = [
 
 [[package]]
 name = "once_cell"
-version = "1.20.3"
+version = "1.21.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
+checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
 
 [[package]]
-name = "owo-colors"
-version = "4.1.0"
+name = "once_cell_polyfill"
+version = "1.70.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fb37767f6569cd834a413442455e0f066d0d522de8630436e2a1761d9726ba56"
+checksum = "a4895175b425cb1f87721b59f0f286c2092bd4af812243672510e1ac53e2e0ad"
+
+[[package]]
+name = "openssl"
+version = "0.10.73"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8505734d46c8ab1e19a1dce3aef597ad87dcb4c37e7188231769bd6bd51cebf8"
+dependencies = [
+ "bitflags 2.9.1",
+ "cfg-if",
+ "foreign-types",
+ "libc",
+ "once_cell",
+ "openssl-macros",
+ "openssl-sys",
+]
+
+[[package]]
+name = "openssl-macros"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "openssl-probe"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e"
+
+[[package]]
+name = "openssl-sys"
+version = "0.9.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90096e2e47630d78b7d1c20952dc621f957103f8bc2c8359ec81290d75238571"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
 
 [[package]]
 name = "parking"
@@ -1341,9 +1610,9 @@ checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
 
 [[package]]
 name = "parking_lot"
-version = "0.12.3"
+version = "0.12.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+checksum = "70d58bf43669b5795d1576d0641cfb6fbb2057bf629506267a92807158584a13"
 dependencies = [
  "lock_api",
  "parking_lot_core",
@@ -1351,9 +1620,9 @@ dependencies = [
 
 [[package]]
 name = "parking_lot_core"
-version = "0.9.10"
+version = "0.9.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+checksum = "bc838d2a56b5b1a6c25f55575dfc605fabb63bb2365f6c2353ef9159aa69e4a5"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1378,48 +1647,46 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 
 [[package]]
-name = "pest"
-version = "2.7.15"
+name = "phf"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b7cafe60d6cf8e62e1b9b2ea516a089c008945bb5a275416789e7db0bc199dc"
+checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
 dependencies = [
- "memchr",
- "thiserror",
- "ucd-trie",
+ "phf_macros",
+ "phf_shared",
+ "serde",
 ]
 
 [[package]]
-name = "pest_derive"
-version = "2.7.15"
+name = "phf_generator"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "816518421cfc6887a0d62bf441b6ffb4536fcc926395a69e1a85852d4363f57e"
+checksum = "2cbb1126afed61dd6368748dae63b1ee7dc480191c6262a3b4ff1e29d86a6c5b"
 dependencies = [
- "pest",
- "pest_generator",
+ "fastrand",
+ "phf_shared",
 ]
 
 [[package]]
-name = "pest_generator"
-version = "2.7.15"
+name = "phf_macros"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7d1396fd3a870fc7838768d171b4616d5c91f6cc25e377b673d714567d99377b"
+checksum = "d713258393a82f091ead52047ca779d37e5766226d009de21696c4e667044368"
 dependencies = [
- "pest",
- "pest_meta",
+ "phf_generator",
+ "phf_shared",
  "proc-macro2",
  "quote",
  "syn",
 ]
 
 [[package]]
-name = "pest_meta"
-version = "2.7.15"
+name = "phf_shared"
+version = "0.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e1e58089ea25d717bfd31fb534e4f3afcc2cc569c70de3e239778991ea3b7dea"
+checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
 dependencies = [
- "once_cell",
- "pest",
- "sha2",
+ "siphasher",
 ]
 
 [[package]]
@@ -1457,30 +1724,49 @@ dependencies = [
 
 [[package]]
 name = "pkg-config"
-version = "0.3.31"
+version = "0.3.32"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
+checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
 
 [[package]]
 name = "portable-atomic"
-version = "1.10.0"
+version = "1.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6"
+checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e5a7c30837279ca13e7c867e9e40053bc68740f988cb07f7ca6df43cc734b585"
+dependencies = [
+ "zerovec",
+]
 
 [[package]]
 name = "ppv-lite86"
-version = "0.2.20"
+version = "0.2.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04"
+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
 dependencies = [
  "zerocopy",
 ]
 
 [[package]]
+name = "pretty_assertions"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d"
+dependencies = [
+ "diff",
+ "yansi",
+]
+
+[[package]]
 name = "prettyplease"
-version = "0.2.29"
+version = "0.2.35"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6924ced06e1f7dfe3fa48d57b9f74f55d8915f5036121bef647ef4b204895fac"
+checksum = "061c1221631e079b26479d25bbf2275bfe5917ae8419cd7e34f13bfc2aa7539a"
 dependencies = [
  "proc-macro2",
  "syn",
@@ -1488,20 +1774,19 @@ dependencies = [
 
 [[package]]
 name = "proc-macro2"
-version = "1.0.93"
+version = "1.0.95"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99"
+checksum = "02b3e5e68a3a1a02aad3ec490a98007cbc13c37cbe84a3cd7b8e406d76e7f778"
 dependencies = [
  "unicode-ident",
 ]
 
 [[package]]
 name = "pyo3"
-version = "0.23.4"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "57fe09249128b3173d092de9523eaa75136bf7ba85e0d69eca241c7939c933cc"
+checksum = "8970a78afe0628a3e3430376fc5fd76b6b45c4d43360ffd6cdd40bdde72b682a"
 dependencies = [
- "cfg-if",
  "indoc",
  "libc",
  "memoffset",
@@ -1515,9 +1800,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-build-config"
-version = "0.23.4"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1cd3927b5a78757a0d71aa9dff669f903b1eb64b54142a9bd9f757f8fde65fd7"
+checksum = "458eb0c55e7ece017adeba38f2248ff3ac615e53660d7c71a238d7d2a01c7598"
 dependencies = [
  "once_cell",
  "target-lexicon",
@@ -1525,9 +1810,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-ffi"
-version = "0.23.4"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dab6bb2102bd8f991e7749f130a70d05dd557613e39ed2deeee8e9ca0c4d548d"
+checksum = "7114fe5457c61b276ab77c5055f206295b812608083644a5c5b2640c3102565c"
 dependencies = [
  "libc",
  "pyo3-build-config",
@@ -1535,9 +1820,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-macros"
-version = "0.23.4"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "91871864b353fd5ffcb3f91f2f703a22a9797c91b9ab497b1acac7b07ae509c7"
+checksum = "a8725c0a622b374d6cb051d11a0983786448f7785336139c3c94f5aa6bef7e50"
 dependencies = [
  "proc-macro2",
  "pyo3-macros-backend",
@@ -1547,9 +1832,9 @@ dependencies = [
 
 [[package]]
 name = "pyo3-macros-backend"
-version = "0.23.4"
+version = "0.25.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "43abc3b80bc20f3facd86cd3c60beed58c3e2aa26213f3cda368de39c60a27e4"
+checksum = "4109984c22491085343c05b0dbc54ddc405c3cf7b4374fc533f5c3313a572ccc"
 dependencies = [
  "heck",
  "proc-macro2",
@@ -1559,15 +1844,30 @@ dependencies = [
 ]
 
 [[package]]
+name = "pyo3-pylogger"
+version = "1.8.0"
+dependencies = [
+ "log",
+ "phf",
+ "pyo3",
+]
+
+[[package]]
 name = "quote"
-version = "1.0.38"
+version = "1.0.40"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc"
+checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
 dependencies = [
  "proc-macro2",
 ]
 
 [[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
 name = "rand"
 version = "0.8.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1594,16 +1894,16 @@ version = "0.6.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
 dependencies = [
- "getrandom 0.2.15",
+ "getrandom 0.2.16",
 ]
 
 [[package]]
 name = "redox_syscall"
-version = "0.5.9"
+version = "0.5.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "82b568323e98e49e2a0899dcee453dd679fae22d69adf9b11dd508d1549b7e2f"
+checksum = "0d04b7d0ee6b4a0207a0a7adb104d23ecb0b47d6beae7152d0fa34b692b29fd6"
 dependencies = [
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
@@ -1636,10 +1936,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c"
 
 [[package]]
+name = "reqwest"
+version = "0.12.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cbc931937e6ca3a06e3b6c0aa7841849b160a90351d6ab467a8b9b9959767531"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.16",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
 name = "rsa"
-version = "0.9.7"
+version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "47c75d7c5c6b673e58bf54d8544a9f432e3a925b0e80f7cd3602ab5c50c55519"
+checksum = "78928ac1ed176a5ca1d17e578a1825f3d81ca54cf41053a592584b020cfd691b"
 dependencies = [
  "const-oid",
  "digest",
@@ -1657,9 +2011,9 @@ dependencies = [
 
 [[package]]
 name = "rustc-demangle"
-version = "0.1.24"
+version = "0.1.25"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
+checksum = "989e6739f80c4ad5b13e0fd7fe89531180375b18520cc8c82080e4dc4035b84f"
 
 [[package]]
 name = "rustc-hash"
@@ -1669,28 +2023,61 @@ checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
 
 [[package]]
 name = "rustix"
-version = "0.38.44"
+version = "1.0.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154"
+checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8"
 dependencies = [
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
  "errno",
  "libc",
  "linux-raw-sys",
- "windows-sys 0.59.0",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2491382039b29b9b11ff08b76ff6c97cf287671dbb74f0be44bda389fffe9bd1"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229a4a4c221013e7e1f1a043678c5cc39fe5171437c88fb47151a21e6f5b5c79"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a17884ae0c1b773f1ccd2bd4a8c72f16da897310a98b0e84bf349ad5ead92fc"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
 ]
 
 [[package]]
 name = "rustversion"
-version = "1.0.19"
+version = "1.0.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
+checksum = "8a0d197bd2c9dc6e53b84da9556a69ba4cdfab8619eb41a8bd1cc2027a0f6b1d"
 
 [[package]]
 name = "ryu"
-version = "1.0.19"
+version = "1.0.20"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6ea1a2d0a644769cc99faa24c3ad26b379b786fe7c36fd3c546254801650e6dd"
+checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
 
 [[package]]
 name = "same-file"
@@ -1702,6 +2089,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "schannel"
+version = "0.1.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d"
+dependencies = [
+ "windows-sys 0.59.0",
+]
+
+[[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1709,9 +2105,9 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
 name = "sdl2"
-version = "0.37.0"
+version = "0.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3b498da7d14d1ad6c839729bd4ad6fc11d90a57583605f3b4df2cd709a9cd380"
+checksum = "2d42407afc6a8ab67e36f92e80b8ba34cbdc55aaeed05249efe9a2e8d0e9feef"
 dependencies = [
  "bitflags 1.3.2",
  "lazy_static",
@@ -1721,9 +2117,9 @@ dependencies = [
 
 [[package]]
 name = "sdl2-sys"
-version = "0.37.0"
+version = "0.38.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "951deab27af08ed9c6068b7b0d05a93c91f0a8eb16b6b816a5e73452a43521d3"
+checksum = "3ff61407fc75d4b0bbc93dc7e4d6c196439965fbef8e4a4f003a36095823eac0"
 dependencies = [
  "cfg-if",
  "libc",
@@ -1731,19 +2127,42 @@ dependencies = [
 ]
 
 [[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "serde"
-version = "1.0.218"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
+checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.218"
+version = "1.0.219"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
+checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1752,9 +2171,9 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.139"
+version = "1.0.140"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44f86c3acccc9c65b153fe1b85a3be07fe5515274ec9f0653b4a0875731c72a6"
+checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
 dependencies = [
  "itoa",
  "memchr",
@@ -1764,9 +2183,9 @@ dependencies = [
 
 [[package]]
 name = "serde_spanned"
-version = "0.6.8"
+version = "1.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "87607cb1398ed59d48732e575a4c28a7a8ebf2454b964fe3f224f2afc07909e1"
+checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83"
 dependencies = [
  "serde",
 ]
@@ -1796,9 +2215,9 @@ dependencies = [
 
 [[package]]
 name = "sha2"
-version = "0.10.8"
+version = "0.10.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
 dependencies = [
  "cfg-if",
  "cpufeatures",
@@ -1813,9 +2232,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
 
 [[package]]
 name = "signal-hook-registry"
-version = "1.4.2"
+version = "1.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
+checksum = "9203b8055f63a2a00e2f593bb0510367fe707d7ff1e5c872de2f537b339e5410"
 dependencies = [
  "libc",
 ]
@@ -1831,28 +2250,31 @@ dependencies = [
 ]
 
 [[package]]
+name = "siphasher"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
+
+[[package]]
 name = "slab"
-version = "0.4.9"
+version = "0.4.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
-dependencies = [
- "autocfg",
-]
+checksum = "04dc19736151f35336d325007ac991178d504a119863a2fcb3758cdb5e52c50d"
 
 [[package]]
 name = "smallvec"
-version = "1.14.0"
+version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 dependencies = [
  "serde",
 ]
 
 [[package]]
 name = "socket2"
-version = "0.5.8"
+version = "0.5.10"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c970269d99b64e60ec3bd6ad27270092a5394c4e309314b18ae3fe575695fbe8"
+checksum = "e22376abed350d73dd1cd119b57ffccad95b4e585a7cda43e286245ce23c0678"
 dependencies = [
  "libc",
  "windows-sys 0.52.0",
@@ -1879,9 +2301,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4410e73b3c0d8442c5f99b425d7a435b5ee0ae4167b3196771dd3f7a01be745f"
+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
 dependencies = [
  "sqlx-core",
  "sqlx-macros",
@@ -1892,11 +2314,12 @@ dependencies = [
 
 [[package]]
 name = "sqlx-core"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6a007b6936676aa9ab40207cde35daab0a04b823be8ae004368c0793b96a61e0"
+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
 dependencies = [
- "bytes 1.10.0",
+ "base64",
+ "bytes",
  "crc",
  "crossbeam-queue",
  "either",
@@ -1925,9 +2348,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-macros"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3112e2ad78643fef903618d78cf0aec1cb3134b019730edb039b69eaf531f310"
+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -1938,9 +2361,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-macros-core"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4e9f90acc5ab146a99bf5061a7eb4976b573f560bc898ef3bf8435448dd5e7ad"
+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
 dependencies = [
  "dotenvy",
  "either",
@@ -1957,22 +2380,21 @@ dependencies = [
  "sqlx-postgres",
  "sqlx-sqlite",
  "syn",
- "tempfile",
  "tokio",
  "url",
 ]
 
 [[package]]
 name = "sqlx-mysql"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4560278f0e00ce64938540546f59f590d60beee33fffbd3b9cd47851e5fff233"
+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
 dependencies = [
  "atoi",
  "base64",
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
  "byteorder",
- "bytes 1.10.0",
+ "bytes",
  "crc",
  "digest",
  "dotenvy",
@@ -2006,13 +2428,13 @@ dependencies = [
 
 [[package]]
 name = "sqlx-postgres"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c5b98a57f363ed6764d5b3a12bfedf62f07aa16e1856a7ddc2a0bb190a959613"
+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
 dependencies = [
  "atoi",
  "base64",
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
  "byteorder",
  "crc",
  "dotenvy",
@@ -2043,9 +2465,9 @@ dependencies = [
 
 [[package]]
 name = "sqlx-sqlite"
-version = "0.8.3"
+version = "0.8.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f85ca71d3a5b24e64e1d08dd8fe36c6c95c339a896cc33068148906784620540"
+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
 dependencies = [
  "atoi",
  "flume",
@@ -2060,6 +2482,7 @@ dependencies = [
  "serde",
  "serde_urlencoded",
  "sqlx-core",
+ "thiserror",
  "tracing",
  "url",
 ]
@@ -2108,9 +2531,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
 
 [[package]]
 name = "syn"
-version = "2.0.98"
+version = "2.0.104"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
+checksum = "17b6f705963418cdb9927482fa304bc562ece2fdd4f616084c50b7023b435a40"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2118,10 +2541,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "sync_wrapper"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
+dependencies = [
+ "futures-core",
+]
+
+[[package]]
 name = "synstructure"
-version = "0.13.1"
+version = "0.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2129,20 +2561,40 @@ dependencies = [
 ]
 
 [[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags 2.9.1",
+ "core-foundation",
+ "system-configuration-sys",
+]
+
+[[package]]
+name = "system-configuration-sys"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "target-lexicon"
-version = "0.12.16"
+version = "0.13.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
+checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
 
 [[package]]
 name = "tempfile"
-version = "3.17.1"
+version = "3.20.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "22e5a0acb1f3f55f65cc4a866c361b2fb2a0ff6366785ae6fbb5f85df07ba230"
+checksum = "e8a64e3985349f2441a1a9ef0b853f869006c3855f2cda6862a94d26ebb9d6a1"
 dependencies = [
- "cfg-if",
  "fastrand",
- "getrandom 0.3.1",
+ "getrandom 0.3.3",
  "once_cell",
  "rustix",
  "windows-sys 0.59.0",
@@ -2159,7 +2611,7 @@ dependencies = [
 
 [[package]]
 name = "termsize"
-version = "1.4.1"
+version = "1.8.0"
 dependencies = [
  "libc",
  "winapi",
@@ -2167,18 +2619,18 @@ dependencies = [
 
 [[package]]
 name = "thiserror"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc"
+checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 dependencies = [
  "thiserror-impl",
 ]
 
 [[package]]
 name = "thiserror-impl"
-version = "2.0.11"
+version = "2.0.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
+checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2187,19 +2639,18 @@ dependencies = [
 
 [[package]]
 name = "thread_local"
-version = "1.1.8"
+version = "1.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
 dependencies = [
  "cfg-if",
- "once_cell",
 ]
 
 [[package]]
 name = "tinystr"
-version = "0.7.6"
+version = "0.8.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+checksum = "5d4f6d1145dcb577acf783d4e601bc1d76a13337bb54e6233add580b07344c8b"
 dependencies = [
  "displaydoc",
  "zerovec",
@@ -2207,9 +2658,9 @@ dependencies = [
 
 [[package]]
 name = "tinyvec"
-version = "1.8.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "022db8904dfa342efe721985167e9fcd16c29b226db4397ed752a761cfce81e8"
+checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71"
 dependencies = [
  "tinyvec_macros",
 ]
@@ -2222,16 +2673,18 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
 
 [[package]]
 name = "tokio"
-version = "1.43.0"
+version = "1.46.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e"
+checksum = "0cc3a2344dafbe23a245241fe8b09735b521110d30fcefbbd5feb1797ca35d17"
 dependencies = [
  "backtrace",
- "bytes 1.10.0",
+ "bytes",
+ "io-uring",
  "libc",
  "mio",
  "pin-project-lite",
  "signal-hook-registry",
+ "slab",
  "socket2",
  "tokio-macros",
  "windows-sys 0.52.0",
@@ -2249,6 +2702,26 @@ dependencies = [
 ]
 
 [[package]]
+name = "tokio-native-tls"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2"
+dependencies = [
+ "native-tls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-rustls"
+version = "0.26.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e727b36a1a0e8b74c376ac2211e40c2c8af09fb4013c60d910495810f008e9b"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
 name = "tokio-stream"
 version = "0.1.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2260,40 +2733,105 @@ dependencies = [
 ]
 
 [[package]]
+name = "tokio-util"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "66a539a9ad6d5d281510d5bd368c973d636c02dbf8a67300bfb6b950696ad7df"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "futures-util",
+ "hashbrown",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
 name = "toml"
-version = "0.8.20"
+version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cd87a5cdd6ffab733b2f74bc4fd7ee5fff6634124999ac278c35fc78c6120148"
+checksum = "ed0aee96c12fa71097902e0bb061a5e1ebd766a6636bb605ba401c45c1650eac"
 dependencies = [
+ "indexmap",
  "serde",
  "serde_spanned",
  "toml_datetime",
- "toml_edit",
+ "toml_parser",
+ "toml_writer",
+ "winnow",
 ]
 
 [[package]]
 name = "toml_datetime"
-version = "0.6.8"
+version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
+checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3"
 dependencies = [
  "serde",
 ]
 
 [[package]]
-name = "toml_edit"
-version = "0.22.24"
+name = "toml_parser"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474"
+checksum = "97200572db069e74c512a14117b296ba0a80a30123fbbb5aa1f4a348f639ca30"
 dependencies = [
- "indexmap",
- "serde",
- "serde_spanned",
- "toml_datetime",
  "winnow",
 ]
 
 [[package]]
+name = "toml_writer"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fcc842091f2def52017664b53082ecbbeb5c7731092bad69d2c63050401dfd64"
+
+[[package]]
+name = "tower"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
+dependencies = [
+ "futures-core",
+ "futures-util",
+ "pin-project-lite",
+ "sync_wrapper",
+ "tokio",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-http"
+version = "0.6.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2"
+dependencies = [
+ "bitflags 2.9.1",
+ "bytes",
+ "futures-util",
+ "http",
+ "http-body",
+ "iri-string",
+ "pin-project-lite",
+ "tower",
+ "tower-layer",
+ "tower-service",
+]
+
+[[package]]
+name = "tower-layer"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
+
+[[package]]
+name = "tower-service"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
+
+[[package]]
 name = "tracing"
 version = "0.1.41"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2307,9 +2845,9 @@ dependencies = [
 
 [[package]]
 name = "tracing-attributes"
-version = "0.1.28"
+version = "0.1.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
+checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2318,22 +2856,18 @@ dependencies = [
 
 [[package]]
 name = "tracing-core"
-version = "0.1.33"
+version = "0.1.34"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c"
+checksum = "b9d12581f227e93f094d3af2ae690a574abb8a2b9b7a96e7cfe9647b2b617678"
 dependencies = [
  "once_cell",
 ]
 
 [[package]]
-name = "trinitry"
-version = "0.2.2"
+name = "try-lock"
+version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f814008587cd653ef1f92f9caf321e86a6f53899ec118fd50eaed55974863a40"
-dependencies = [
- "pest",
- "pest_derive",
-]
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
 
 [[package]]
 name = "typenum"
@@ -2342,12 +2876,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
 
 [[package]]
-name = "ucd-trie"
-version = "0.1.7"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
-
-[[package]]
 name = "unicode-bidi"
 version = "0.3.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2355,9 +2883,9 @@ checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
 
 [[package]]
 name = "unicode-ident"
-version = "1.0.17"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "00e2473a93778eb0bad35909dff6a10d28e63f792f16ed15e404fca9d5eeedbe"
+checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
 
 [[package]]
 name = "unicode-normalization"
@@ -2375,22 +2903,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e70f2a8b45122e719eb623c01822704c4e0907e7e426a05927e1a1cfff5b75d0"
 
 [[package]]
-name = "unicode-segmentation"
-version = "1.12.0"
+name = "unicode-width"
+version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
+checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c"
 
 [[package]]
-name = "unicode-width"
-version = "0.2.0"
+name = "unindent"
+version = "0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd"
+checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
 
 [[package]]
-name = "unindent"
-version = "0.2.3"
+name = "untrusted"
+version = "0.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
 
 [[package]]
 name = "url"
@@ -2405,12 +2933,6 @@ dependencies = [
 ]
 
 [[package]]
-name = "utf16_iter"
-version = "1.0.5"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246"
-
-[[package]]
 name = "utf8_iter"
 version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2424,7 +2946,7 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
 
 [[package]]
 name = "uu_fmt"
-version = "1.4.1"
+version = "1.8.0"
 dependencies = [
  "unicode-width",
 ]
@@ -2458,16 +2980,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "want"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e"
+dependencies = [
+ "try-lock",
+]
+
+[[package]]
 name = "wasi"
-version = "0.11.0+wasi-snapshot-preview1"
+version = "0.11.1+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
 
 [[package]]
 name = "wasi"
-version = "0.13.3+wasi-0.2.2"
+version = "0.14.2+wasi-0.2.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
 dependencies = [
  "wit-bindgen-rt",
 ]
@@ -2505,6 +3036,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.50"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.100"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2537,10 +3081,20 @@ dependencies = [
 ]
 
 [[package]]
+name = "web-sys"
+version = "0.3.77"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
 name = "whoami"
-version = "1.5.2"
+version = "1.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d"
+checksum = "6994d13118ab492c3c80c1f81928718159254c53c472bf9ce36f8dae4add02a7"
 dependencies = [
  "redox_syscall",
  "wasite",
@@ -2579,11 +3133,72 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
 [[package]]
 name = "windows-core"
-version = "0.52.0"
+version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
+checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3"
 dependencies = [
- "windows-targets 0.52.6",
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a47fddd13af08290e67f4acabf4b459f647552718f683a7b415d290ac744a836"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bd9211b69f8dcdfa817bfd14bf1c97c9188afa36f4750130fcdf3f400eca9fa8"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a"
+
+[[package]]
+name = "windows-registry"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57"
+dependencies = [
+ "windows-link",
 ]
 
 [[package]]
@@ -2614,6 +3229,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "windows-sys"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
+dependencies = [
+ "windows-targets 0.53.2",
+]
+
+[[package]]
 name = "windows-targets"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2637,7 +3261,7 @@ dependencies = [
  "windows_aarch64_gnullvm 0.52.6",
  "windows_aarch64_msvc 0.52.6",
  "windows_i686_gnu 0.52.6",
- "windows_i686_gnullvm",
+ "windows_i686_gnullvm 0.52.6",
  "windows_i686_msvc 0.52.6",
  "windows_x86_64_gnu 0.52.6",
  "windows_x86_64_gnullvm 0.52.6",
@@ -2645,6 +3269,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "windows-targets"
+version = "0.53.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c66f69fcc9ce11da9966ddb31a40968cad001c5bedeb5c2b82ede4253ab48aef"
+dependencies = [
+ "windows_aarch64_gnullvm 0.53.0",
+ "windows_aarch64_msvc 0.53.0",
+ "windows_i686_gnu 0.53.0",
+ "windows_i686_gnullvm 0.53.0",
+ "windows_i686_msvc 0.53.0",
+ "windows_x86_64_gnu 0.53.0",
+ "windows_x86_64_gnullvm 0.53.0",
+ "windows_x86_64_msvc 0.53.0",
+]
+
+[[package]]
 name = "windows_aarch64_gnullvm"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2657,6 +3297,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
 
 [[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764"
+
+[[package]]
 name = "windows_aarch64_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2669,6 +3315,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
 
 [[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c"
+
+[[package]]
 name = "windows_i686_gnu"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2681,12 +3333,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
 
 [[package]]
+name = "windows_i686_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1dc67659d35f387f5f6c479dc4e28f1d4bb90ddd1a5d3da2e5d97b42d6272c3"
+
+[[package]]
 name = "windows_i686_gnullvm"
 version = "0.52.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
 
 [[package]]
+name = "windows_i686_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11"
+
+[[package]]
 name = "windows_i686_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2699,6 +3363,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
 
 [[package]]
+name = "windows_i686_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d"
+
+[[package]]
 name = "windows_x86_64_gnu"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2711,6 +3381,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
 
 [[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba"
+
+[[package]]
 name = "windows_x86_64_gnullvm"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2723,6 +3399,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
 
 [[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57"
+
+[[package]]
 name = "windows_x86_64_msvc"
 version = "0.48.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2735,46 +3417,49 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
 
 [[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271414315aff87387382ec3d271b52d7ae78726f5d44ac98b4f4030c91880486"
+
+[[package]]
 name = "winnow"
-version = "0.7.3"
+version = "0.7.12"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0e7f4ea97f6f78012141bcdb6a216b2609f0979ada50b20ca5b52dde2eac2bb1"
-dependencies = [
- "memchr",
-]
+checksum = "f3edebf492c8125044983378ecb5766203ad3b4c2f7a922bd7dd207f6d443e95"
 
 [[package]]
 name = "wit-bindgen-rt"
-version = "0.33.0"
+version = "0.39.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
 dependencies = [
- "bitflags 2.8.0",
+ "bitflags 2.9.1",
 ]
 
 [[package]]
-name = "write16"
-version = "1.0.0"
+name = "writeable"
+version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+checksum = "ea2f10b9bb0928dfb1b42b65e1f9e36f7f54dbdf08457afefb38afcdec4fa2bb"
 
 [[package]]
-name = "writeable"
-version = "0.5.5"
+name = "xdg"
+version = "3.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+checksum = "2fb433233f2df9344722454bc7e96465c9d03bff9d77c248f9e7523fe79585b5"
 
 [[package]]
-name = "xdg"
-version = "2.5.2"
+name = "yansi"
+version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "213b7324336b53d2414b2db8537e56544d981803139155afa84f76eeebb7a546"
+checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049"
 
 [[package]]
 name = "yoke"
-version = "0.7.5"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40"
+checksum = "5f41bb01b8226ef4bfd589436a297c53d118f65921786300e427be8d487695cc"
 dependencies = [
  "serde",
  "stable_deref_trait",
@@ -2784,9 +3469,9 @@ dependencies = [
 
 [[package]]
 name = "yoke-derive"
-version = "0.7.5"
+version = "0.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
+checksum = "38da3c9736e16c5d3c8c597a9aaa5d1fa565d0532ae05e27c24aa62fb32c0ab6"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2796,30 +3481,32 @@ dependencies = [
 
 [[package]]
 name = "yt"
-version = "1.4.1"
+version = "1.8.0"
 dependencies = [
  "anyhow",
  "blake3",
- "bytes 1.4.1",
  "chrono",
  "chrono-humanize",
  "clap",
+ "clap_complete",
+ "colors",
  "futures",
  "libmpv2",
  "log",
  "notify",
- "nucleo-matcher",
- "owo-colors",
+ "pretty_assertions",
  "regex",
+ "reqwest",
  "serde",
  "serde_json",
+ "shlex",
  "sqlx",
  "stderrlog",
  "tempfile",
  "termsize",
  "tokio",
+ "tokio-util",
  "toml",
- "trinitry",
  "url",
  "uu_fmt",
  "xdg",
@@ -2828,32 +3515,32 @@ dependencies = [
 
 [[package]]
 name = "yt_dlp"
-version = "1.4.1"
+version = "1.8.0"
 dependencies = [
- "bytes 1.4.1",
+ "curl",
  "log",
  "pyo3",
+ "pyo3-pylogger",
  "serde",
  "serde_json",
- "tokio",
+ "thiserror",
  "url",
 ]
 
 [[package]]
 name = "zerocopy"
-version = "0.7.35"
+version = "0.8.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0"
+checksum = "1039dd0d3c310cf05de012d8a39ff557cb0d23087fd44cad61df08fc31907a2f"
 dependencies = [
- "byteorder",
  "zerocopy-derive",
 ]
 
 [[package]]
 name = "zerocopy-derive"
-version = "0.7.35"
+version = "0.8.26"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
+checksum = "9ecf5b4cc5364572d7f4c329661bcc82724222973f2cab6f050a4e5c22f75181"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2862,18 +3549,18 @@ dependencies = [
 
 [[package]]
 name = "zerofrom"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
 dependencies = [
  "zerofrom-derive",
 ]
 
 [[package]]
 name = "zerofrom-derive"
-version = "0.1.5"
+version = "0.1.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -2888,10 +3575,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde"
 
 [[package]]
+name = "zerotrie"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f0bbd478583f79edad978b407914f61b2972f5af6fa089686016be8f9af595"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
 name = "zerovec"
-version = "0.10.4"
+version = "0.11.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079"
+checksum = "4a05eb080e015ba39cc9e23bbe5e7fb04d5fb040350f99f34e338d5fdd294428"
 dependencies = [
  "yoke",
  "zerofrom",
@@ -2900,9 +3598,9 @@ dependencies = [
 
 [[package]]
 name = "zerovec-derive"
-version = "0.10.3"
+version = "0.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
+checksum = "5b96237efa0c878c64bd89c436f661be4e46b2f3eff1ebb976f7ef2321d2f58f"
 dependencies = [
  "proc-macro2",
  "quote",
diff --git a/Cargo.toml b/Cargo.toml
index eb3f735..7128bec 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -11,18 +11,18 @@
 [workspace]
 resolver = "2"
 members = [
-  "crates/bytes",
-  "crates/yt_dlp",
+  "crates/colors",
   "crates/libmpv2",
   "crates/libmpv2/libmpv2-sys",
   "crates/termsize",
-  "yt",
+  "crates/yt",
+  "crates/yt_dlp",
 ]
 
 [workspace.package]
-edition = "2021"
-version = "1.4.1"
-rust-version = "1.80.0"
+edition = "2024"
+version = "1.8.0"
+rust-version = "1.85.0"
 authors = ["Benedikt Peetz <benedikt.peetz@b-peetz.de>"]
 repository = "https://git.vhack.eu/soispha/clients/yt"
 license = "GPL-3.0-or-later"
@@ -31,17 +31,18 @@ description = "A fully featured command line YouTube client"
 [workspace.dependencies]
 # Own Crates
 yt_dlp = { path = "./crates/yt_dlp" }
-bytes = { path = "./crates/bytes" }
 libmpv2 = { path = "./crates/libmpv2" }
 termsize = { path = "./crates/termsize" }
 uu_fmt = { path = "./crates/fmt" }
+colors = { path = "./crates/colors" }
 
 # Shared
-log = "0.4.26"
-serde = { version = "1.0.218", features = ["derive"] }
-serde_json = "1.0.139"
+pyo3 = { version = "0.25.1", features = ["macros"], default-features = false }
+log = { version = "0.4.27", features = ["kv"] }
+serde = { version = "1.0.219", features = ["derive"] }
+serde_json = "1.0.140"
 url = { version = "2.5.4", features = ["serde"] }
-tokio = { version = "1.43.0", features = [
+tokio = { version = "1.46.1", features = [
   "rt-multi-thread",
   "macros",
   "process",
diff --git a/LICENSES/Apache-2.0.txt b/LICENSES/Apache-2.0.txt
new file mode 100644
index 0000000..137069b
--- /dev/null
+++ b/LICENSES/Apache-2.0.txt
@@ -0,0 +1,73 @@
+Apache License
+Version 2.0, January 2004
+http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+"License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document.
+
+"Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License.
+
+"Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity.
+
+"You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License.
+
+"Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files.
+
+"Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types.
+
+"Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below).
+
+"Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof.
+
+"Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution."
+
+"Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions:
+
+     (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and
+
+     (b) You must cause any modified files to carry prominent notices stating that You changed the files; and
+
+     (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and
+
+     (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License.
+
+     You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!)  The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/NEWS.md b/NEWS.md
index 95ce885..9788514 100644
--- a/NEWS.md
+++ b/NEWS.md
@@ -14,6 +14,301 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 All notable changes to this project will be documented in this file. See [conventional commits](https://www.conventionalcommits.org/) for commit guidelines.
 
 - - -
+## [v1.8.0](https://git.foss-syndicate.org/soispha/clients/yt/compare/4276f312926c0b166967066ca06887d42e362561..v1.8.0) - 2025-07-24
+#### Bug Fixes
+- **(crates/yt)** Correct imports - ([ac99460](https://git.foss-syndicate.org/soispha/clients/yt/commit/ac99460ed65126d32160ac12641f211d9162db91)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** **Always** honor the `config.global.display_colors` config setting - ([e2e88fd](https://git.foss-syndicate.org/soispha/clients/yt/commit/e2e88fdabe9bfb3ed236983e6e737b9790d50cd2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** Add stuff that was missed - ([f09ad6c](https://git.foss-syndicate.org/soispha/clients/yt/commit/f09ad6c85f8d4f3de73de895f370b5773b63177a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/cli)** Use the correct `--plackback-speed` option name - ([bda2fd6](https://git.foss-syndicate.org/soispha/clients/yt/commit/bda2fd6e886ae9f699708fd6d6848d9a11bba423)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands)** Add the actual `implm` wrapper - ([1fdae07](https://git.foss-syndicate.org/soispha/clients/yt/commit/1fdae07aa66b298db23cb5f85b41da4a4f0c539b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands/database)** Correctly format the default for `--kind` - ([51bec16](https://git.foss-syndicate.org/soispha/clients/yt/commit/51bec161d856735bb24545055b9414a0fb8ef9b6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands/select)** Allow configuring the `yt select url` opener - ([994971a](https://git.foss-syndicate.org/soispha/clients/yt/commit/994971a56d50e8bb6d01583ac71fee90463e855d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/config)** Avoid module name re-use in `watch` config - ([dc09734](https://git.foss-syndicate.org/soispha/clients/yt/commit/dc097347be5077b56a70f79c6f06b56a919232ff)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/config)** Ensure that the download_dir is created - ([59608b3](https://git.foss-syndicate.org/soispha/clients/yt/commit/59608b3ee2473cc2d2b35b286168c932f0c78f2d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/db/insert/maintenance)** Re-init - ([ee48fa7](https://git.foss-syndicate.org/soispha/clients/yt/commit/ee48fa727afd1927f20b5d0491344f4afb03bd2e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/db/insert/playlist)** Account for playlist_len == 0 - ([2c0e68f](https://git.foss-syndicate.org/soispha/clients/yt/commit/2c0e68f39296e5ab180e28d90c3076cf71dfc080)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/downloader)** Correctly treat the download as blocking - ([507c961](https://git.foss-syndicate.org/soispha/clients/yt/commit/507c9611232e7b820789ec776159c703acd499ab)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/select)** Correctly open the persistent file in `select split` - ([e209cce](https://git.foss-syndicate.org/soispha/clients/yt/commit/e209cceacac0d6ee4051d8bb3dce0ad97f2f474d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/storage/db/videos/comments)** Don't always associate a reply with its base - ([98c3984](https://git.foss-syndicate.org/soispha/clients/yt/commit/98c3984e1e15a3b98d1eeb191809d1b1ae7be119)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/storage/migrate)** Merge the `videos` and `video_options` tables - ([e76c029](https://git.foss-syndicate.org/soispha/clients/yt/commit/e76c029e3392283fe0c230bba01a236b71089bbe)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/update)** Remove the `--grouped` update support - ([32cb3cd](https://git.foss-syndicate.org/soispha/clients/yt/commit/32cb3cd0ac414490e9bd614f8faa59a5bb9ca4e2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/{commands/playlist,videos/format_video})** Correctly calculate watch percent - ([ca62bbb](https://git.foss-syndicate.org/soispha/clients/yt/commit/ca62bbb3e2455d4d832b4b359e7247deebf7f5c1)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(nix/package)** Update to include the newest changes - ([a201b66](https://git.foss-syndicate.org/soispha/clients/yt/commit/a201b665bb05ec90622d9850863367e27881e036)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Use `json_try_get!` instead of `json.get(..).map(|| ..)` - ([a9fddbe](https://git.foss-syndicate.org/soispha/clients/yt/commit/a9fddbeebf428eb57c60afab96fbbd38629a636e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Build system
+- **(.cargo/cargo.toml)** Remove pointless `PYO3_PATH` - ([4276f31](https://git.foss-syndicate.org/soispha/clients/yt/commit/4276f312926c0b166967066ca06887d42e362561)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(.envrc)** Set the `PYO3_PYTHON` variable again - ([2e4261a](https://git.foss-syndicate.org/soispha/clients/yt/commit/2e4261a0d28f11abc0bf4df55a9775ad045cd028)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(Cargo.lock)** Update - ([cf8ca00](https://git.foss-syndicate.org/soispha/clients/yt/commit/cf8ca007dadf71af9163d40f3ca741992ad80a52)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(Cargo.toml)** Update to include new changes - ([72445e1](https://git.foss-syndicate.org/soispha/clients/yt/commit/72445e192409d6d628f9af91fa08f3a69c02e459)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/Cargo.toml)** Remove now pointless `owo-colors` dep - ([211e0bd](https://git.foss-syndicate.org/soispha/clients/yt/commit/211e0bd88d3902e5226022974e3263f34e3a1aa3)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Update - ([58dfb43](https://git.foss-syndicate.org/soispha/clients/yt/commit/58dfb436de0ae70540574195dc113f07e97483b4)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({update.sh,crates/{libmpv2,yt_dlp}/update.sh})** Remove pointless update instructions - ([ab8605a](https://git.foss-syndicate.org/soispha/clients/yt/commit/ab8605a62fc88255dbcc079a16102f17dbe5e704)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Documentation
+- **(contrib/external_commands_script.sh)** Init - ([ebc1fff](https://git.foss-syndicate.org/soispha/clients/yt/commit/ebc1fff767e4a94de1f9f4db27dc955d0748a4dd)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/libmpv2)** Correctly format doc-test - ([d6e9378](https://git.foss-syndicate.org/soispha/clients/yt/commit/d6e937812c26368c594b1e0be036f167be2e7e5d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Features
+- **(crates/yt)** Separate all commands from their implementation code - ([c4524db](https://git.foss-syndicate.org/soispha/clients/yt/commit/c4524db090d2d31af8bc3e7ec64c1ea9f5ec72aa)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** Support a `--format` argument for most commands with output - ([d6e1711](https://git.foss-syndicate.org/soispha/clients/yt/commit/d6e17110dae3f1afe35415065e9a08d0f90f2592)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands/cache)** Init - ([a902e8e](https://git.foss-syndicate.org/soispha/clients/yt/commit/a902e8e273262b08a7dbbd3c611d7153d4fa9b4e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands/database)** Init, to show the txn_log - ([761560f](https://git.foss-syndicate.org/soispha/clients/yt/commit/761560fe7b3d2e5cbc1fd942ea7bb84d440459fe)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands/show)** Also provide thumbnail and info screen - ([5ccf617](https://git.foss-syndicate.org/soispha/clients/yt/commit/5ccf61730945e98f36a3e8621b22cfd3be4ab4eb)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands/watch/mpv_commands)** Hook-up the new show commands - ([99d4f68](https://git.foss-syndicate.org/soispha/clients/yt/commit/99d4f688868ee664470b13a0d61ac65832263bab)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/select)** Print the currently processed line as progress - ([d8000c8](https://git.foss-syndicate.org/soispha/clients/yt/commit/d8000c8591a4886023aaf52b9298147c67449932)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/storage)** Migrate inserts to operations and use methods - ([e4d6fc0](https://git.foss-syndicate.org/soispha/clients/yt/commit/e4d6fc04f60cf7b8173df7f261428b25d009ba39)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/storage/db/insert)** Track all inserted operations - ([f6eb32a](https://git.foss-syndicate.org/soispha/clients/yt/commit/f6eb32ae50a21d0d3b0ed0e992f3871d59966743)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/subscribe)** Support a `--no-check` argument - ([45a4507](https://git.foss-syndicate.org/soispha/clients/yt/commit/45a45074e1afe0254d7e732d03f5ba29e6b53030)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/update)** Make the concurrency configurable - ([6095c67](https://git.foss-syndicate.org/soispha/clients/yt/commit/6095c678c42c20810eac0dd6f4fa371199f3ad7a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/watch)** Make the time between watch progress saves configurable - ([7dc1f2d](https://git.foss-syndicate.org/soispha/clients/yt/commit/7dc1f2d302b176dc11d2680d7a0fd8d710da6e23)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/watch)** Support `--headless` and `--provide-ipc-socket` flags - ([8f6d3d0](https://git.foss-syndicate.org/soispha/clients/yt/commit/8f6d3d07f192c3be5348493358c82b697ee26392)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Miscellaneous Chores
+- **(crates/yt/Cargo.toml)** Add `pretty-assertions` for tests - ([9b018e2](https://git.foss-syndicate.org/soispha/clients/yt/commit/9b018e276a216e64ae1ebb446ab49a67b7960bff)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Add missing license headers - ([71351b8](https://git.foss-syndicate.org/soispha/clients/yt/commit/71351b83af2cfb142ad536936f613a66059244f6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Performance Improvements
+- **(crates/yt/db/extractor_hash/realize)** Allow passing in a `all_hashes` - ([c254ed0](https://git.foss-syndicate.org/soispha/clients/yt/commit/c254ed0c7d098cf3224e004a4b13a11632e432a3)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Refactoring
+- **(crates/bytes)** Move into yt - ([66d56d7](https://git.foss-syndicate.org/soispha/clients/yt/commit/66d56d7a2b64c5ed13860d809d9bf35d86292df2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/colors)** Don't expose the custom colours module - ([57520fd](https://git.foss-syndicate.org/soispha/clients/yt/commit/57520fd2c02ec4e09d0714b870de6968ac7f6378)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** Allow `missing_panic_docs` and use expect - ([e60cf47](https://git.foss-syndicate.org/soispha/clients/yt/commit/e60cf473b3ba1b5c0295d69e93e7d266f62ed60a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** Use the new storage layer - ([95ccd01](https://git.foss-syndicate.org/soispha/clients/yt/commit/95ccd01d26c6664c9917332d4f19c949dfb905cd)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** Make every `pub` item `pub(crate)` - ([c3abafd](https://git.foss-syndicate.org/soispha/clients/yt/commit/c3abafd4878df886dc8765a048cb0b70f282f1f3)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/commands)** Restrict visibility to itself - ([ed9956b](https://git.foss-syndicate.org/soispha/clients/yt/commit/ed9956b784b087f1610f472954339990f79eec49)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/comments)** Remove dead code - ([860798a](https://git.foss-syndicate.org/soispha/clients/yt/commit/860798a28518b8dccd28433b34102961a0e09045)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/config)** Always use imported paths in config dec - ([4632928](https://git.foss-syndicate.org/soispha/clients/yt/commit/46329283ef91d023c07aecd856889d496dc69471)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/config)** Use a macro to generate the config parsing code - ([9753705](https://git.foss-syndicate.org/soispha/clients/yt/commit/97537059b44f5ed336a915a1ba805be215cf6566)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/constants)** Remove empty module - ([2b61c8b](https://git.foss-syndicate.org/soispha/clients/yt/commit/2b61c8bbb32524ac63ff8e7e814091707a03ef4e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/db/insert::Commitable)** Make `Debug` a dependency - ([7ec9b54](https://git.foss-syndicate.org/soispha/clients/yt/commit/7ec9b549510db0e96d5d0b9019ee0e6689e3952d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/download/progress_hook)** Use `json_{get,cast}` and owu-colors - ([3c11d06](https://git.foss-syndicate.org/soispha/clients/yt/commit/3c11d068e438da22375a9d0af9abb81c738ebcbb)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/src/ansi_escape_codes)** Use better name for `erase_in_display_from_cursor` - ([3737b65](https://git.foss-syndicate.org/soispha/clients/yt/commit/3737b653725c2e6ad5323cf6caafcd6e59a938f6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt_dlp)** Port to `pyo3` again - ([82277ca](https://git.foss-syndicate.org/soispha/clients/yt/commit/82277ca7513eff82365ed54fe9836aae5bd45fe1)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Style
+- **(treewide)** Format - ([19d3c02](https://git.foss-syndicate.org/soispha/clients/yt/commit/19d3c02396ac1c00742e50e60a0a840fbfade1ac)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Tests
+- **(crates/libmpv2)** Avoid compiling a doc-test - ([8f2dc82](https://git.foss-syndicate.org/soispha/clients/yt/commit/8f2dc825461ad3006c3f8bdfcb9f3b5048fdb861)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt)** Add basic integration tests - ([234b910](https://git.foss-syndicate.org/soispha/clients/yt/commit/234b9105e097fb63f636bc05ac2f471c001c3aac)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/testenv/run/run_piped)** Finalize the second command after the first one - ([1ffbeef](https://git.foss-syndicate.org/soispha/clients/yt/commit/1ffbeefff7f3817e2dec72ec06c2f139dbaca7ac)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/tests/_testenv)** Store `Child`s instead of PIDs - ([7cc99ec](https://git.foss-syndicate.org/soispha/clients/yt/commit/7cc99ec385857dc6e33072f9e7865ee2a93a8d69)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/tests/_testenv::init)** Use appropriate atomic u64 type - ([4e09daf](https://git.foss-syndicate.org/soispha/clients/yt/commit/4e09dafa7a213ce2fcafe7d810cf3ae1f5f9bdb0)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(crates/yt/tests/watch/focus_switch.rs)** This test simply lacks its purpose - ([889e0b1](https://git.foss-syndicate.org/soispha/clients/yt/commit/889e0b10424542246de14b6a3dcadf56cd46424b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+
+- - -
+
+## [v1.7.1](https://git.foss-syndicate.org/soispha/clients/yt/compare/69d1f92c9ff5e76c0c2b91641962f9e21afe2ded..v1.7.1) - 2025-06-28
+#### Bug Fixes
+- **(yt/download/progress_hook)** Remove superfluous apostrophes - ([92e3367](https://git.foss-syndicate.org/soispha/clients/yt/commit/92e3367fbc93b67b2db9d7296630d299294e4c13)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select/cmds/add)** Use the correct names for the download type - ([cd03c0b](https://git.foss-syndicate.org/soispha/clients/yt/commit/cd03c0b9501c596c3abcd41e07105a3def20e2dd)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select/cmds/add)** Don't print the title as value, cast it first - ([247dabc](https://git.foss-syndicate.org/soispha/clients/yt/commit/247dabc7905d9deecc86ac11404b5665042c60f1)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select/split)** Also use persist the selection file - ([72d33c1](https://git.foss-syndicate.org/soispha/clients/yt/commit/72d33c13a8a715a5a12d804464d887c2376701ad)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/subscribe)** Don't hard-error on failed subscribe, if it was not specified - ([c5ad75c](https://git.foss-syndicate.org/soispha/clients/yt/commit/c5ad75c9176990da906c9ef3086e8efe25037fd9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/{subscribe,storage/subscriptions})** Fix more instances of the capitalize Playlist type - ([2cee354](https://git.foss-syndicate.org/soispha/clients/yt/commit/2cee35477e4e4e2b3b6aeb094217e0419bdcaed4)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Polyfill missing rustpython features used in urllib3 - ([8c65652](https://git.foss-syndicate.org/soispha/clients/yt/commit/8c6565295986b704f36a9174d05deacc6925b7e4)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Build system
+- **(nix/package)** Update the git hashes after the update - ([f74251b](https://git.foss-syndicate.org/soispha/clients/yt/commit/f74251b5191963b979a23fa16555712aa83817ba)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(nix/package)** Add all required files to the src allow list - ([09d5c9c](https://git.foss-syndicate.org/soispha/clients/yt/commit/09d5c9c93c786a309328564d74c58e8be1dcfa5b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({flake,Cargo}.lock)** Update - ([033b0d3](https://git.foss-syndicate.org/soispha/clients/yt/commit/033b0d3ce9eef96827a3f33e4aa5f108e98e4878)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({nix,flake})** Add missing buildInputs - ([bc1f78f](https://git.foss-syndicate.org/soispha/clients/yt/commit/bc1f78fde9aa45a3d53a36bbfab11178f6f8f684)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Miscellaneous Chores
+- **(yt/storage/video_database/set)** Apply some of clippy's suggestions - ([d451984](https://git.foss-syndicate.org/soispha/clients/yt/commit/d451984d34c74190340cc82d203565c7e4747908)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/package_hacks)** Add missing license headers - ([c3a8c10](https://git.foss-syndicate.org/soispha/clients/yt/commit/c3a8c104515b47597f8b72eeabc7dcd266ec0316)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Revert
+- "build(treewide): Update" - ([69d1f92](https://git.foss-syndicate.org/soispha/clients/yt/commit/69d1f92c9ff5e76c0c2b91641962f9e21afe2ded)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+
+- - -
+
+## [v1.7.0](https://git.foss-syndicate.org/soispha/clients/yt/compare/382eae56dc3ecaed91b9fd8db1c830d5dec49e44..v1.7.0) - 2025-06-24
+#### Bug Fixes
+- **(yt/update/grouped)** Don't drop the verbosity level - ([28d4c61](https://git.foss-syndicate.org/soispha/clients/yt/commit/28d4c61bb0b3b6b20d57a0dd970af83265bb0ad2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/post_processors/dearrow)** Don't try to access the drained vec - ([9b4f09c](https://git.foss-syndicate.org/soispha/clients/yt/commit/9b4f09cf736e68bdbd246dca17d7a3c6b8eba3ea)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Build system
+- **(treewide)** Update - ([e6aa91c](https://git.foss-syndicate.org/soispha/clients/yt/commit/e6aa91c56ca51a8593b9a58ec5746741888db7f9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/Cargo.toml)** Pin git dependencies - ([772f169](https://git.foss-syndicate.org/soispha/clients/yt/commit/772f16902d75e3d6ae211b9ef3977316708698c4)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Features
+- **(yt/cli)** Also add completion for subscription names - ([382eae5](https://git.foss-syndicate.org/soispha/clients/yt/commit/382eae56dc3ecaed91b9fd8db1c830d5dec49e44)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/version)** Add the (rust)python version again - ([84175a0](https://git.foss-syndicate.org/soispha/clients/yt/commit/84175a03a71918497aa0c8ee3444736d771cccff)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+
+- - -
+
+## [v1.6.1](https://git.foss-syndicate.org/soispha/clients/yt/compare/3f6ef87fc31581215cb00d56462b35e07b7a1f28..v1.6.1) - 2025-06-17
+#### Bug Fixes
+- **(package)** Set the PYTHONPATH ourselves - ([ea77b89](https://git.foss-syndicate.org/soispha/clients/yt/commit/ea77b898e5dfb2a7900a87a1bb73167a6e1a140c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Typos in strings - ([987cff2](https://git.foss-syndicate.org/soispha/clients/yt/commit/987cff2b5996cc86069dc1d9cbb0f465c32d391c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/)** Include the frozen python stdlib - ([3f6ef87](https://git.foss-syndicate.org/soispha/clients/yt/commit/3f6ef87fc31581215cb00d56462b35e07b7a1f28)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/post_processing/dearrow)** Correctly type the `CasualVote` field - ([528c2d4](https://git.foss-syndicate.org/soispha/clients/yt/commit/528c2d4a4842647da3a91a034c810c44ebf9b949)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/post_processors)** Register in python - ([1a6d363](https://git.foss-syndicate.org/soispha/clients/yt/commit/1a6d3639e6fddb731735554d407d1eea77f053c6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/post_processors/dearrow)** Migrate to curl for api requests - ([0a17001](https://git.foss-syndicate.org/soispha/clients/yt/commit/0a1700131341c5dac55a395ce5ccdac4f8ec0c9e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Build system
+- **(flake)** Teach the flake about the new package.nix location - ([c4bc9fd](https://git.foss-syndicate.org/soispha/clients/yt/commit/c4bc9fdfde2852cc0f5efbb9bed327f16a6fe275)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({Cargo,flake}.lock)** Update - ([2aaa919](https://git.foss-syndicate.org/soispha/clients/yt/commit/2aaa919101be7a4fa42ac76a5f2f491689319e39)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Miscellaneous Chores
+- **(treewide)** Assure that `nix fmt` and `reuse lint` are happy - ([d847968](https://git.foss-syndicate.org/soispha/clients/yt/commit/d847968fab7dc55b30f8a137dbce2bae07112c82)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Refactoring
+- **(nix/package)** Avoid the duplicated `package` name - ([9fbbd3e](https://git.foss-syndicate.org/soispha/clients/yt/commit/9fbbd3e71f2d7286e9ef1cbdbdea4020bd511308)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Split the big `lib.rs` file up - ([8d6eb78](https://git.foss-syndicate.org/soispha/clients/yt/commit/8d6eb786ee99e7b0c36736152e30a5f61cd34167)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/logging)** Avoid adding to the `__all__` list - ([e0120c0](https://git.foss-syndicate.org/soispha/clients/yt/commit/e0120c08672009f8d4445eebef8efb22ddae5fb3)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/progress_hook)** Use public api via `__priv` module - ([74ecf0e](https://git.foss-syndicate.org/soispha/clients/yt/commit/74ecf0ea1564343905a96dbd14826700762ec825)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+
+- - -
+
+## [v1.6.0](https://git.foss-syndicate.org/soispha/clients/yt/compare/07db485f9c5206fbcfe2a5f9db28a9587edc6d2b..v1.6.0) - 2025-06-16
+#### Bug Fixes
+- **(libmpv2-sys)** Avoid generating comments, that confuse rustdoc - ([0c0e00d](https://git.foss-syndicate.org/soispha/clients/yt/commit/0c0e00da2c21c4b8325fa6145c808e9df0df0834)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(scripts/mkdb.sh)** Also use the `$DATABASE_URL` variable as source source - ([45e5500](https://git.foss-syndicate.org/soispha/clients/yt/commit/45e55007aa13b1ec24af4c543bc3b8699710301c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/cli)** Remove duplicated short flag key (help also uses 'h') - ([c1122d6](https://git.foss-syndicate.org/soispha/clients/yt/commit/c1122d6ab31548aff9bf8aaa4a855a771355c8e9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/download/get_file_size)** Correct deal with `filesize_approx` = Null - ([680f811](https://git.foss-syndicate.org/soispha/clients/yt/commit/680f811adc83554cfbaff56d8b50501786a949e2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/downloader/progress_hook)** Silence clippy warnings - ([65ba5d7](https://git.foss-syndicate.org/soispha/clients/yt/commit/65ba5d738dcfeaecb398e246e0db5d7c4bf04b99)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select/selection_file/duration)** Improve the duration parser - ([9e1c1ae](https://git.foss-syndicate.org/soispha/clients/yt/commit/9e1c1aec0548a6482e23ceac4e1265ef8baf8023)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/migrate)** Correct the two to three migration script - ([7694496](https://git.foss-syndicate.org/soispha/clients/yt/commit/7694496efa621466e327b9c00fe1c5cc092ccc1f)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/migrate)** Correctly state the upgrade to the topmost version - ([449c4c2](https://git.foss-syndicate.org/soispha/clients/yt/commit/449c4c26c91400e56e0e685958b825b3f02f4e40)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/migrate)** Improve error messages - ([3a16edd](https://git.foss-syndicate.org/soispha/clients/yt/commit/3a16edde524f881c8955026350243a1b4d54d89b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/migrate)** Account for the fact that DbVersions::Empty means no Version - ([d1f004c](https://git.foss-syndicate.org/soispha/clients/yt/commit/d1f004ce48caf90ab4f3ec1d0bbb588c9cbf0fe9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/video_database/set)** Reset the `is_focused` flag - ([07db485](https://git.foss-syndicate.org/soispha/clients/yt/commit/07db485f9c5206fbcfe2a5f9db28a9587edc6d2b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/subscribe)** Deal with moved url value - ([fb00ecf](https://git.foss-syndicate.org/soispha/clients/yt/commit/fb00ecf745c1bd12e026faabf235a75c2c775a3a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update)** Also handle the newly introduced error conditions - ([a7e1a2d](https://git.foss-syndicate.org/soispha/clients/yt/commit/a7e1a2d7475fc1304ef7b33aa2f170f8232bd1d8)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update)** Correct the progress display in `--grouped` mode - ([35f400c](https://git.foss-syndicate.org/soispha/clients/yt/commit/35f400cebca70325e7e999f15dcaa562dbc78f25)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update)** Avoid printing all the subscriptions that are not updated - ([810c0d3](https://git.foss-syndicate.org/soispha/clients/yt/commit/810c0d3e75287c15e8baf210f89c807a21d3acee)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update/video_entry_to_video)** Cast the json objects - ([b6a57c5](https://git.foss-syndicate.org/soispha/clients/yt/commit/b6a57c5cad1ee7df56dad5ccb2317f936e682bbe)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/version)** Use yt_dlp's native python version imply - ([22f74fc](https://git.foss-syndicate.org/soispha/clients/yt/commit/22f74fc43b004045d13b0184ae075dea0ebc8eda)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/watch/playlist)** Workaround terminals, that treat 0 as 1 - ([b3be18a](https://git.foss-syndicate.org/soispha/clients/yt/commit/b3be18a0bfb55135135c9769ac531c098ca4d26c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/{se,}dowa)** Don't exit completely, if the downloader fails - ([b70dd45](https://git.foss-syndicate.org/soispha/clients/yt/commit/b70dd458615bbad99cf05dbde3dc831a9922ba21)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Avoid writing the json output to disk - ([c8601d6](https://git.foss-syndicate.org/soispha/clients/yt/commit/c8601d67c2dd67ed3ae4465fbf3906fa2cf15a98)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/json_{cast,get})** Improve error reporting - ([c4f8c14](https://git.foss-syndicate.org/soispha/clients/yt/commit/c4f8c14b5636055a2973afe0d5ef6494d97a1a76)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Build system
+- **(.envrc)** Also disable ytdlp plugins by default - ([1b8113a](https://git.foss-syndicate.org/soispha/clients/yt/commit/1b8113a72161e5d5f1f7a8328265f8075fc3491a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(.envrc)** Remove outdated env variables - ([e51139d](https://git.foss-syndicate.org/soispha/clients/yt/commit/e51139da51bbb8725614356bd173d1d66af7f74f)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(cog.toml)** Use the correct remote url - ([4e2aeec](https://git.foss-syndicate.org/soispha/clients/yt/commit/4e2aeec877ec9083de5116bcca8da039389b9f09)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(cog.toml)** Use correct username - ([b9957a2](https://git.foss-syndicate.org/soispha/clients/yt/commit/b9957a2dc50b02f1df8bcb2dc3ddcc3c081b94d3)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(flake)** Document for what the `CLANG_*` env vars are needed - ([d03e537](https://git.foss-syndicate.org/soispha/clients/yt/commit/d03e5374a238dc2b701c259bbd5ade91c6b4a9ff)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(flake)** Remove `flake-utils` - ([d21e1ac](https://git.foss-syndicate.org/soispha/clients/yt/commit/d21e1ac26c5e57f7e5f9cb2fea937b807118187b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(flake)** Switch to `nixpkgs-unstable-small` - ([1a807d2](https://git.foss-syndicate.org/soispha/clients/yt/commit/1a807d25bd1a47fb81b538a1638514cedb928148)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(flake)** Adapt the dev env to yt_dlp's new dependencies - ([b8682b4](https://git.foss-syndicate.org/soispha/clients/yt/commit/b8682b478a3a2322a370cc8eabf46d20d00e8c37)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(flake)** Add `git-bug` to the devshell - ([ebcd3e1](https://git.foss-syndicate.org/soispha/clients/yt/commit/ebcd3e153e01bd1432b583b2a09569ba2017b8ed)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(package/package.nix)** Update to the new build requirements - ([f590cef](https://git.foss-syndicate.org/soispha/clients/yt/commit/f590cef92da3931fae1607c6964b8125ab2f6307)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Update - ([2380d7d](https://git.foss-syndicate.org/soispha/clients/yt/commit/2380d7d7fdfdda91c26e8027f41aa6788f3590e0)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Update - ([0791777](https://git.foss-syndicate.org/soispha/clients/yt/commit/0791777665fe99d02b5e4aaaa43ca3483712dac9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Update - ([fa79bd7](https://git.foss-syndicate.org/soispha/clients/yt/commit/fa79bd7eef3824ad208984df9cc7784bdab5ba2b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Update - ([47754f5](https://git.foss-syndicate.org/soispha/clients/yt/commit/47754f54b978e7ed66ccd29c866fabe28607997e)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({flake,Cargo}.lock)** Update - ([9c7dfa7](https://git.foss-syndicate.org/soispha/clients/yt/commit/9c7dfa7a8ca71bd5067741917a6f96061290976b)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Documentation
+- **(yt/update)** Add comment about the `unsmuggle_url` invocation - ([6c47d93](https://git.foss-syndicate.org/soispha/clients/yt/commit/6c47d93c983b8807032220e107ac2f686abb14e2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/watch/playlist_handler/client_messages)** Add TODO about `current_exe` - ([13a0621](https://git.foss-syndicate.org/soispha/clients/yt/commit/13a062150e4efaf4b87d9213cf68b5a4eabb0235)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Fix typo in `Cargo.toml`'s description - ([848270e](https://git.foss-syndicate.org/soispha/clients/yt/commit/848270ed0d9ed0409fe563a130e2913d9dfcc897)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Features
+- **(yt/cli)** Add support for command line completions - ([e635ee7](https://git.foss-syndicate.org/soispha/clients/yt/commit/e635ee79a4ec0d30dca271cc269fee40150ea821)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select)** Support a directory selection process - ([e2d5dc6](https://git.foss-syndicate.org/soispha/clients/yt/commit/e2d5dc6a9f000a875c3f2a100f660adc2a43275a)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select/split)** Make sorting configurable - ([8b644e4](https://git.foss-syndicate.org/soispha/clients/yt/commit/8b644e4e0e058a003984c02d48e829de437145c6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/status)** Show the percentage of videos that were actually watched - ([ec4e0c9](https://git.foss-syndicate.org/soispha/clients/yt/commit/ec4e0c91d33b2a8c11b71d4cdb1edeaa44ce8247)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/videos)** Validate in DB, that is_focused is UNIQUE - ([cf16b93](https://git.foss-syndicate.org/soispha/clients/yt/commit/cf16b93b563daee88b3bda4b30666b1b0766a8b0)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update)** Print a nice progress number - ([c04d530](https://git.foss-syndicate.org/soispha/clients/yt/commit/c04d530a1a9e09dd26adc4116959e5481b970bc6)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update)** Support grouped updates - ([8a42c83](https://git.foss-syndicate.org/soispha/clients/yt/commit/8a42c835a0dd1fcaa3475938d9442199d57acf75)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/update)** Specify subscriptions to update as positional args - ([51bbd90](https://git.foss-syndicate.org/soispha/clients/yt/commit/51bbd90ab1f08c9056c4e5799e3abba568ae75c9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/videos/list)** Replace the nucleo matcher with a simple `contains` - ([c0a3b61](https://git.foss-syndicate.org/soispha/clients/yt/commit/c0a3b61fb344a5ca86cae1c31d2e42fbe56b6726)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Support a DeArrow post processor - ([ab61a4e](https://git.foss-syndicate.org/soispha/clients/yt/commit/ab61a4e47a955dd4a5dabeef3ade1b85f6576b84)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({yt/update,yt_dlp})** Use yt_dlp errors again - ([078dfa0](https://git.foss-syndicate.org/soispha/clients/yt/commit/078dfa09a40a384b5cb8cf8cffd9b68cc9678556)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **({yt_dlp,yt})** Migrate from pyo3 to rustpython - ([69145b4](https://git.foss-syndicate.org/soispha/clients/yt/commit/69145b4deed4fe512239a9f88e6af69d3b8c0309)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Miscellaneous Chores
+- **(treewide)** Add missing copyright headers - ([fd029a6](https://git.foss-syndicate.org/soispha/clients/yt/commit/fd029a65d43e1eb935b470b88893c16c30c19746)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Migrate to rust edition 2024 - ([8be7171](https://git.foss-syndicate.org/soispha/clients/yt/commit/8be717167ed77f5a1021fa0825b386674c5c1a39)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/wrappers/info_json)** Add additional missing field - ([8ef4cf9](https://git.foss-syndicate.org/soispha/clients/yt/commit/8ef4cf92635003fb79263d22126289d788e34633)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Performance Improvements
+- **(yt/update/updater)** Acknowledge, that `yt_dlp` has a sync API - ([ab56f25](https://git.foss-syndicate.org/soispha/clients/yt/commit/ab56f2550d5086ccd1c6981b62081b70743a1f2c)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Refactoring
+- **(yt)** Move to `crates/yt` - ([394d4f7](https://git.foss-syndicate.org/soispha/clients/yt/commit/394d4f7d105dadd7b516f198b0d6a9dda2d3f1a5)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt)** Consolidate the multiple ANSI escape code wrapper functions - ([efc35b5](https://git.foss-syndicate.org/soispha/clients/yt/commit/efc35b5bd76bf4e4aab6750ead45713a79e851f9)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select)** Split the `select::select` function up - ([fb49841](https://git.foss-syndicate.org/soispha/clients/yt/commit/fb49841e1ec14b3ab2de981e439d4f10f5494cf5)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select/selection_file)** Migrate from `trinitry` to `shlex` - ([56011be](https://git.foss-syndicate.org/soispha/clients/yt/commit/56011be94c09828b104008cb7bf3a19177bc1631)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/migrate)** Factor out duplicated code into macro - ([137339d](https://git.foss-syndicate.org/soispha/clients/yt/commit/137339d1d2924da764c54517fcc6d5d11d46a69d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/storage/migrate/sql)** Use predictable SQL paths - ([420f9c8](https://git.foss-syndicate.org/soispha/clients/yt/commit/420f9c87abe3a3480a2345cbad5ec427636b2cb5)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp)** Remove the unneeded `async` from the public functions - ([5b5caee](https://git.foss-syndicate.org/soispha/clients/yt/commit/5b5caee512dd82bc5106e69259ba916cd143deda)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/lib)** De-duplicate the info json sanitize code - ([e46ab9b](https://git.foss-syndicate.org/soispha/clients/yt/commit/e46ab9bc8bd4ecc35363e27aea9b5445bc858b2d)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt_dlp/lib)** Explicitly convert python exceptions into an error - ([ada9550](https://git.foss-syndicate.org/soispha/clients/yt/commit/ada9550b02ee13a8378bd2ee27d536b83eec4820)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Style
+- **(treewide)** Reformat - ([10b07fa](https://git.foss-syndicate.org/soispha/clients/yt/commit/10b07fa5a4f4080ef5417720b2d15179b72d2fc2)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(treewide)** Adopt rust edition 2024 rustfmt style - ([a78b66e](https://git.foss-syndicate.org/soispha/clients/yt/commit/a78b66ed784cd6f2f97771d9e170c8f8558140b8)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/cli)** Sort the toplevel flags alphabetically - ([77ea1d8](https://git.foss-syndicate.org/soispha/clients/yt/commit/77ea1d8223b57567b448fb973b7240adaab61778)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+- **(yt/select)** Apply clippy's suggestions - ([b4ee42a](https://git.foss-syndicate.org/soispha/clients/yt/commit/b4ee42a62c683632c589f39e6ceac0b48d222e87)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+#### Tests
+- **(yt/cli)** Test the CLI - ([10f9d8b](https://git.foss-syndicate.org/soispha/clients/yt/commit/10f9d8bfd0c84146638cfdaf6b076493f943e650)) - [@bpeetz](https://git.foss-syndicate.org/bpeetz)
+
+- - -
+
+## [v1.5.0](https://git.vhack.eu/soispha/clients/yt/compare/2146109725115a9d01cc08ebbe3ef9c533ef1a89..v1.5.0) - 2025-02-22
+#### Bug Fixes
+- **(crates/libmpv2)** Improve the error message for the `RawError` - ([0bd13d5](https://git.vhack.eu/soispha/clients/yt/commit/0bd13d5c26495649dabc23a4fb6b37fe682e3aec)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/libmpv2/Mpv::command)** Correctly escape arguments - ([66c7392](https://git.vhack.eu/soispha/clients/yt/commit/66c739237cc352fedf6276a3163097c1f1f32bd4)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/libmpv2/mpv)** Log the setting of properties - ([d2081fb](https://git.vhack.eu/soispha/clients/yt/commit/d2081fbfed6b2bde727aaf766bf0435ec05a3574)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/termsize)** Remove all of `clippy`'s warnings - ([4b63c7b](https://git.vhack.eu/soispha/clients/yt/commit/4b63c7be4207bf2ff7884189a388e83633b20b26)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp)** Actually return errors instead of panicing - ([4d1b813](https://git.vhack.eu/soispha/clients/yt/commit/4d1b8136bb23d009ee04d863780225ad9d9f9eed)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp)** Avoid printing the file extension in the progress display - ([325b230](https://git.vhack.eu/soispha/clients/yt/commit/325b23039850953705a57c0a331045b169548751)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/error::PythonError)** Add the python type as `kind` - ([c83dfe5](https://git.vhack.eu/soispha/clients/yt/commit/c83dfe5268e2db39fe731b2d38387b76d9586057)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/lib)** Actually resolve the `entries` generator object - ([c7601c2](https://git.vhack.eu/soispha/clients/yt/commit/c7601c2e6cc86a3123d8c9dc13afce9430520583)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/lib)** Swallow all error logs from yt_dlp - ([d1022c5](https://git.vhack.eu/soispha/clients/yt/commit/d1022c5c5c82557d8c2c45fad88b67cc3e6582e3)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/progress_hook)** Print the progress to stderr - ([1786ba0](https://git.vhack.eu/soispha/clients/yt/commit/1786ba0b87d9883e4c75126e5d72af02134cc8b8)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/wrappers/info_json)** Serialize the `InfoType`s with their correct name - ([dc19623](https://git.vhack.eu/soispha/clients/yt/commit/dc19623a18ac47d9c660d98db768c64d99decff9)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/wrappers/info_json)** Don't serialize `None` values - ([fc5771e](https://git.vhack.eu/soispha/clients/yt/commit/fc5771e35b459af6210cbd9a2e7c33b6c462d337)) - [@soispha](https://git.vhack.eu/soispha)
+- **(package)** Update to account for modifications in `mkdb.sh` - ([a1dbd45](https://git.vhack.eu/soispha/clients/yt/commit/a1dbd45cbe2b21aa50eefcb6c9f016a7aaa4863c)) - [@soispha](https://git.vhack.eu/soispha)
+- **(package/blake3)** Migrate to the new `fetchCargoVendor` fetcher - ([c8ce2b7](https://git.vhack.eu/soispha/clients/yt/commit/c8ce2b7862eb273ed81cd399490e0536b2965a20)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt)** Remove most of the references to the zero version `Video` struct - ([74346a5](https://git.vhack.eu/soispha/clients/yt/commit/74346a5e43235be37bffca6dd0cb0ead66a529b5)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/)** Box large futures - ([335bfe9](https://git.vhack.eu/soispha/clients/yt/commit/335bfe91d7efdfd5a89eaf7728511f96dab3fef3)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/cli)** Make most of the arguments to `yt select <cmd> <hash>` optional - ([ceb0ff2](https://git.vhack.eu/soispha/clients/yt/commit/ceb0ff290707905af56106401b3f2a326971c505)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/download/download_options)** Stop trying to write annotations - ([2bcd30e](https://git.vhack.eu/soispha/clients/yt/commit/2bcd30e3f44fb7dbf859d72caf2a33a96ee5618b)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/main)** Call `watch` with the required `Arc<App>` - ([761a780](https://git.vhack.eu/soispha/clients/yt/commit/761a780499eeb6afe93b55b06d9df5c75aa9d7cc)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/main)** Actually remove the `yt check output-info-json` - ([e07db3a](https://git.vhack.eu/soispha/clients/yt/commit/e07db3a810c2e0f43b20d73ea4258f3ea8f240d4)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/select/cmds/add)** Don't try to add a video that is already added - ([69c94a3](https://git.vhack.eu/soispha/clients/yt/commit/69c94a3068689a575a21a0db6170bde9acad768d)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/select/selection_file/help.str)** Disable vim line wrapping - ([5cf5936](https://git.vhack.eu/soispha/clients/yt/commit/5cf5936c3f2fa0750a2e67d8ff4b6624c3141402)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/status)** Don't show the database version in `yt status` - ([cf8662c](https://git.vhack.eu/soispha/clients/yt/commit/cf8662ce03e5677ddb7de880ceb59d5d84a63259)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/status)** Show the current database version - ([8d7df29](https://git.vhack.eu/soispha/clients/yt/commit/8d7df29bbce0ceb258fa6c591003d379fcdb704f)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/migrate)** Improve error reporting - ([e21c289](https://git.vhack.eu/soispha/clients/yt/commit/e21c289f8d21b802d2dc609233bc28cde65da224)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/migrate/sql/01_zero_to_one.sql)** Account for duration being NULL - ([62cdd76](https://git.vhack.eu/soispha/clients/yt/commit/62cdd76443bbecfbdb70a82a7936a2e602338692)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/notify)** Switch from a polling based system to inotify - ([dc8539e](https://git.vhack.eu/soispha/clients/yt/commit/dc8539e3707c1a281b3aef9c7a6e8f929845d965)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/watch)** Always open a `mpv` window - ([c662429](https://git.vhack.eu/soispha/clients/yt/commit/c6624299c45225ec3971e8119979d7421c56f70d)) - [@soispha](https://git.vhack.eu/soispha)
+#### Build system
+- **(.envrc)** Align with current state of the repository - ([6e35bf4](https://git.vhack.eu/soispha/clients/yt/commit/6e35bf41687f92a109ee36fa3169b9fb3f72b7f2)) - [@soispha](https://git.vhack.eu/soispha)
+- **(.envrc)** Always save the `output.info.json` if in devshell - ([2146109](https://git.vhack.eu/soispha/clients/yt/commit/2146109725115a9d01cc08ebbe3ef9c533ef1a89)) - [@soispha](https://git.vhack.eu/soispha)
+- **(flake)** Add `ffmpeg` to the devshell - ([7cc3e3c](https://git.vhack.eu/soispha/clients/yt/commit/7cc3e3cca0de6638625e2997002f78cfd8e03294)) - [@soispha](https://git.vhack.eu/soispha)
+- **(rustfmt.toml)** Add - ([ae13afa](https://git.vhack.eu/soispha/clients/yt/commit/ae13afa1aeb8aaed94b1d72aa6207bbfe373dd52)) - [@soispha](https://git.vhack.eu/soispha)
+- **(scripts/cprh)** Remove - ([3c91e60](https://git.vhack.eu/soispha/clients/yt/commit/3c91e6046b791664a6e7a0fdc41d369df0ee204a)) - [@soispha](https://git.vhack.eu/soispha)
+- **(treewide)** Update - ([7387b68](https://git.vhack.eu/soispha/clients/yt/commit/7387b6853893b3b6a04edb95a830b810e3311ba0)) - [@soispha](https://git.vhack.eu/soispha)
+- **(treewide)** Update - ([938b4f1](https://git.vhack.eu/soispha/clients/yt/commit/938b4f1b11dce643942d5e6cd505b206319709a2)) - [@soispha](https://git.vhack.eu/soispha)
+- **({.envrc,scripts/mkdb})** Mark the `sqlx` database - ([a1b3f95](https://git.vhack.eu/soispha/clients/yt/commit/a1b3f95bd3f7b447e918eec5bd67d7b5e8333eb0)) - [@soispha](https://git.vhack.eu/soispha)
+#### Documentation
+- **(yt/cli)** Remove last references to the external update and status_change bits - ([6e9a03f](https://git.vhack.eu/soispha/clients/yt/commit/6e9a03f4e2c6b57ed569bf12ca5e2954149006fb)) - [@soispha](https://git.vhack.eu/soispha)
+#### Features
+- **(crates/yt_dlp/lib)** Wrap `process_ie_result` function - ([8a53fbd](https://git.vhack.eu/soispha/clients/yt/commit/8a53fbd8af842e2ca1bca0b352113ff7e965f51f)) - [@soispha](https://git.vhack.eu/soispha)
+- **(version)** Include `yt-dlp` and `python` version in `--version` - ([431538e](https://git.vhack.eu/soispha/clients/yt/commit/431538e9c90090c8802460f3a625a0cd38a5e72a)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt)** Make colorization of the output configurable - ([e30b69d](https://git.vhack.eu/soispha/clients/yt/commit/e30b69dd4c2ebfb4ae77b38037b66f3e6fcb17bc)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/)** Use concrete types in the `Video` structure - ([9e8657c](https://git.vhack.eu/soispha/clients/yt/commit/9e8657c9762dbb66f3322976606a1b4334d45a6b)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/cli)** Make running the migrations of the database optional - ([31f15dc](https://git.vhack.eu/soispha/clients/yt/commit/31f15dc02bdbb815ce2d53f10d710a65b0b7bf0b)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/select/cmds/add)** Support `start` `stop` args - ([b3785ad](https://git.vhack.eu/soispha/clients/yt/commit/b3785ad44cb48143ed44cee48190b8646d668946)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/select/selection_file/duration)** Support durations up to days - ([14db4ea](https://git.vhack.eu/soispha/clients/yt/commit/14db4eadd57d0a3837227d9d74b84a133bacd434)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/status)** Include the approximate total watch time - ([e902437](https://git.vhack.eu/soispha/clients/yt/commit/e9024377cf5b16f9a81b0f750891307cd6acebe4)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/migrate)** Add version two - ([8dbcf33](https://git.vhack.eu/soispha/clients/yt/commit/8dbcf33c8c6114e5699472d47c39f114103dc02e)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/migrate)** Add db version One - ([93b7432](https://git.vhack.eu/soispha/clients/yt/commit/93b74321bf30ef33e82b0e9337d8cc3b6ca6e663)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/migrate)** Init database migration system - ([9d6721b](https://git.vhack.eu/soispha/clients/yt/commit/9d6721bce1ed93e66c589d34e20393c78c7a423b)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/update)** Port the Python updater to rust - ([1d7bc17](https://git.vhack.eu/soispha/clients/yt/commit/1d7bc17e62a64ec213e43530b050bc41f978c610)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/version)** Show _current_ database version - ([e6bdcb4](https://git.vhack.eu/soispha/clients/yt/commit/e6bdcb4816cd54b7477f50cdebf06b07e7b9c58e)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/watch/playlist)** Init - ([686ea29](https://git.vhack.eu/soispha/clients/yt/commit/686ea29b06162b1a70dc473cea70b00e379c4f29)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/watch/playlist_handler)** Rewrite to use new db layout - ([4a008ef](https://git.vhack.eu/soispha/clients/yt/commit/4a008ef549f595af18f7cf2d0e9940d2627ae8c4)) - [@soispha](https://git.vhack.eu/soispha)
+#### Miscellaneous Chores
+- **(crates/libmpv2)** Make `cargo clippy` happy - ([b1474f9](https://git.vhack.eu/soispha/clients/yt/commit/b1474f9dc8dc1ed22c2a78680e40bd315cb82b0f)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/termsize)** Vendor - ([9da970f](https://git.vhack.eu/soispha/clients/yt/commit/9da970f1f44f19432680e255f91f73fbb8fbe3c8)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/wrappers/info_json)** Add further fields - ([832ad82](https://git.vhack.eu/soispha/clients/yt/commit/832ad8265015284f1d95c3426f074aaeacd05864)) - [@soispha](https://git.vhack.eu/soispha)
+- **(crates/yt_dlp/wrappers/info_json)** Add further fields - ([674e499](https://git.vhack.eu/soispha/clients/yt/commit/674e4992d320ca0057121eb4474c370abccee8ab)) - [@soispha](https://git.vhack.eu/soispha)
+- **(old)** Remove - ([e2b90b4](https://git.vhack.eu/soispha/clients/yt/commit/e2b90b40333e35214f0b1c1e1f575bb688a99e74)) - [@soispha](https://git.vhack.eu/soispha)
+- **(treewide)** Add/Update the license headers - ([4cbc424](https://git.vhack.eu/soispha/clients/yt/commit/4cbc424e0d1b51c03dc7c3e49dae361cbf4c4b77)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt)** Change the type of `max_backlog` to `usize` - ([b5145c5](https://git.vhack.eu/soispha/clients/yt/commit/b5145c5d4ef674016f4e4217f67c2969a8dee962)) - [@soispha](https://git.vhack.eu/soispha)
+#### Refactoring
+- **(crates/fmt)** Init forked `uu_fmt` library - ([7cfa693](https://git.vhack.eu/soispha/clients/yt/commit/7cfa6939deb5496a07313a2a34632da1a3fb1b89)) - [@soispha](https://git.vhack.eu/soispha)
+- **(treewide)** Remove all references of the now obsolete update_raw.py - ([6a137c6](https://git.vhack.eu/soispha/clients/yt/commit/6a137c6ca968654810ccfddd90908a227287387f)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/)** Use the new `termsize` and `uu_fmt` crates - ([6da5602](https://git.vhack.eu/soispha/clients/yt/commit/6da5602d083bf26312071e68bfb1eb130da98934)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/description)** Move to the `comments` subdirectory - ([f98665d](https://git.vhack.eu/soispha/clients/yt/commit/f98665d992e3af91e52318e0c6e9334c891343bd)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/storage/video_database)** Move `getters,setters` to `get,set` - ([9496583](https://git.vhack.eu/soispha/clients/yt/commit/9496583cc76fbd7347384716f2898f870743f16d)) - [@soispha](https://git.vhack.eu/soispha)
+- **(yt/videos/display)** Streamline video formatting - ([27fae0b](https://git.vhack.eu/soispha/clients/yt/commit/27fae0bcd380fdf7396c33678f4aa3fa2df192cf)) - [@soispha](https://git.vhack.eu/soispha)
+#### Style
+- **(treewide)** Re-format - ([55a9411](https://git.vhack.eu/soispha/clients/yt/commit/55a94110287ad2b1a55953febac48422a9d3ba89)) - [@soispha](https://git.vhack.eu/soispha)
+#### Tests
+- **(crates/yt_dlp)** Ignore tests that hang forever - ([405858e](https://git.vhack.eu/soispha/clients/yt/commit/405858e3e7d2e5c06e49f1c195c46d64916afb65)) - [@soispha](https://git.vhack.eu/soispha)
+
+- - -
+
 ## [v1.4.1](https://git.vhack.eu/soispha/clients/yt/compare/72434a90d6a3dbba48d40a23b840befe7649b558..v1.4.1) - 2024-12-14
 #### Bug Fixes
 - **(yt_dlp/wrappers/info_json)** Add further fields to `RequestedDownloads` - ([72434a9](https://git.vhack.eu/soispha/clients/yt/commit/72434a90d6a3dbba48d40a23b840befe7649b558)) - [@soispha](https://git.vhack.eu/soispha)
diff --git a/cog.toml b/cog.toml
index 7cc3ac4..781175b 100644
--- a/cog.toml
+++ b/cog.toml
@@ -29,7 +29,7 @@ post_bump_hooks = [
 [changelog]
 path = "NEWS.md"
 template = "remote"
-remote = "git.vhack.eu"
+remote = "git.foss-syndicate.org"
 repository = "clients/yt"
 owner = "soispha"
-authors = [{ signature = "Benedikt Peetz", username = "soispha" }]
+authors = [{ signature = "Benedikt Peetz", username = "bpeetz" }]
diff --git a/crates/bytes/.gitignore b/contrib/external_commands_script.sh
index 8876ea6..219eae7 100644..100755
--- a/crates/bytes/.gitignore
+++ b/contrib/external_commands_script.sh
@@ -1,6 +1,8 @@
+#! /usr/bin/env sh
+
 # yt - A fully featured command line YouTube client
 #
-# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 # SPDX-License-Identifier: GPL-3.0-or-later
 #
 # This file is part of Yt.
@@ -8,4 +10,10 @@
 # You should have received a copy of the License along with this program.
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-/target
+riverctl focus-output next
+
+alacritty --title "floating please" --command "$@"
+
+riverctl focus-output next
+
+# vim: ft=sh
diff --git a/crates/bytes/Cargo.lock b/crates/bytes/Cargo.lock
deleted file mode 100644
index b30ba3d..0000000
--- a/crates/bytes/Cargo.lock
+++ /dev/null
@@ -1,65 +0,0 @@
-# This file is automatically @generated by Cargo.
-# It is not intended for manual editing.
-version = 3
-
-[[package]]
-name = "bytes"
-version = "1.0.0"
-dependencies = [
- "serde",
-]
-
-[[package]]
-name = "proc-macro2"
-version = "1.0.86"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77"
-dependencies = [
- "unicode-ident",
-]
-
-[[package]]
-name = "quote"
-version = "1.0.37"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af"
-dependencies = [
- "proc-macro2",
-]
-
-[[package]]
-name = "serde"
-version = "1.0.210"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a"
-dependencies = [
- "serde_derive",
-]
-
-[[package]]
-name = "serde_derive"
-version = "1.0.210"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn",
-]
-
-[[package]]
-name = "syn"
-version = "2.0.77"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed"
-dependencies = [
- "proc-macro2",
- "quote",
- "unicode-ident",
-]
-
-[[package]]
-name = "unicode-ident"
-version = "1.0.13"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe"
diff --git a/crates/bytes/src/serde.rs b/crates/bytes/src/serde.rs
deleted file mode 100644
index 4341e32..0000000
--- a/crates/bytes/src/serde.rs
+++ /dev/null
@@ -1,19 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use serde::{Serialize, Serializer};
-
-use crate::Bytes;
-
-impl Serialize for Bytes {
-    fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
-        serializer.serialize_str(self.to_string().as_str())
-    }
-}
diff --git a/crates/bytes/Cargo.toml b/crates/colors/Cargo.toml
index 4439aa8..4edefcf 100644
--- a/crates/bytes/Cargo.toml
+++ b/crates/colors/Cargo.toml
@@ -1,7 +1,8 @@
 # yt - A fully featured command line YouTube client
 #
-# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-# SPDX-License-Identifier: GPL-3.0-or-later
+# Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# Copyright (C) 2025 uutils developers
+# SPDX-License-Identifier: MIT
 #
 # This file is part of Yt.
 #
@@ -9,25 +10,17 @@
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 [package]
-name = "bytes"
-description = "Simple byte formatting utilities"
-keywords = []
-categories = []
-version.workspace = true
-edition.workspace = true
+name = "colors"
 authors.workspace = true
 license.workspace = true
+description = "A owo-colors inspired color crate."
+version.workspace = true
+edition.workspace = true
 repository.workspace = true
 rust-version.workspace = true
 publish = false
 
 [dependencies]
-serde.workspace = true
-
-[dev-dependencies]
 
 [lints]
 workspace = true
-
-[package.metadata.docs.rs]
-all-features = true
diff --git a/crates/colors/src/custom.rs b/crates/colors/src/custom.rs
new file mode 100644
index 0000000..fd6b7b3
--- /dev/null
+++ b/crates/colors/src/custom.rs
@@ -0,0 +1,75 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+// Taken from <https://github.com/owo-colors/owo-colors/blob/61f8bba2f5f80e9f4fa600fbfdf2c21656f1d523/src/colors/custom.rs>
+// at 2025-07-16T18:05:55 CEST.
+
+const U8_TO_STR: [[u8; 3]; 256] = generate_lookup();
+
+const fn generate_lookup() -> [[u8; 3]; 256] {
+    let mut table = [[0, 0, 0]; 256];
+
+    let mut i = 0;
+    while i < 256 {
+        table[i] = [
+            b'0' + (i / 100) as u8,
+            b'0' + (i / 10 % 10) as u8,
+            b'0' + (i % 10) as u8,
+        ];
+        i += 1;
+    }
+
+    table
+}
+
+#[derive(Clone, Copy)]
+pub(crate) enum Plane {
+    Fg,
+    Bg,
+}
+
+pub(crate) const fn rgb_to_ansi(r: u8, g: u8, b: u8, plane: Plane) -> [u8; 18] {
+    let mut buf = *b"\x1b[p8;2;rrr;ggg;bbb";
+
+    let r = U8_TO_STR[r as usize];
+    let g = U8_TO_STR[g as usize];
+    let b = U8_TO_STR[b as usize];
+
+    // p 2
+    buf[2] = match plane {
+        Plane::Fg => b'3',
+        Plane::Bg => b'4',
+    };
+
+    // r 7
+    buf[7] = r[0];
+    buf[8] = r[1];
+    buf[9] = r[2];
+
+    // g 11
+    buf[11] = g[0];
+    buf[12] = g[1];
+    buf[13] = g[2];
+
+    // b 15
+    buf[15] = b[0];
+    buf[16] = b[1];
+    buf[17] = b[2];
+
+    buf
+}
+
+/// This exists since [`unwrap()`] isn't const-safe (it invokes formatting infrastructure)
+pub(crate) const fn bytes_to_str(bytes: &'static [u8]) -> &'static str {
+    match core::str::from_utf8(bytes) {
+        Ok(o) => o,
+        Err(_e) => panic!("Const parsing &[u8] to a string failed!"),
+    }
+}
diff --git a/crates/colors/src/lib.rs b/crates/colors/src/lib.rs
new file mode 100644
index 0000000..663e19a
--- /dev/null
+++ b/crates/colors/src/lib.rs
@@ -0,0 +1,97 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::fmt::{Display, Write};
+
+use crate::{
+    list::{elements, methods},
+    support::{CSE, CSI, elements_inner},
+};
+
+pub(crate) mod custom;
+mod list;
+mod support;
+
+#[derive(Debug)]
+pub struct Canvas<I: Display>(I);
+
+impl<I: Display> Colorize for Canvas<I> {
+    fn render_into(self, base: &mut String, use_colors: bool) {
+        write!(base, "{}", self.0).expect("Is written into a string");
+
+        if use_colors {
+            // Reset the color and style, if we used colours.
+            base.write_str(CSI).expect("In-memory write");
+            base.write_str("0").expect("In-memory write");
+            base.write_str(CSE).expect("In-memory write");
+        }
+    }
+}
+
+pub trait IntoCanvas: Display + Sized {
+    fn into_canvas(self) -> Canvas<Self> {
+        Canvas(self)
+    }
+
+    methods! { IntoCanvas }
+}
+
+impl<I: Display> IntoCanvas for I {}
+
+pub trait Colorize: Sized {
+    /// Turn this colorized struct into a string, by writing into the base.
+    fn render_into(self, base: &mut String, use_colors: bool);
+
+    /// Turn this colorized struct into a string for consumption.
+    fn render(self, use_colors: bool) -> String {
+        let mut base = String::new();
+        self.render_into(&mut base, use_colors);
+        base
+    }
+
+    methods! { Colorize }
+}
+
+elements! {}
+
+#[cfg(test)]
+mod tests {
+    use crate::{Colorize, IntoCanvas};
+
+    #[test]
+    fn test_colorize_basic() {
+        let base = "Base".green().render(true);
+        #[rustfmt::skip]
+        let expected = concat!(
+            "\x1b[32m",
+            "Base",
+            "\x1b[0m",
+        );
+
+        assert_eq!(base.as_str(), expected);
+    }
+
+    #[test]
+    fn test_colorize_combo() {
+        let base = "Base".green().on_red().bold().strike_through().render(true);
+
+        #[rustfmt::skip]
+        let expected = concat!(
+            "\x1b[9m",  // strike_through
+            "\x1b[1m",  // bold
+            "\x1b[41m", // on_red
+            "\x1b[32m", // green
+            "Base",
+            "\x1b[0m",
+        );
+
+        assert_eq!(base.as_str(), expected);
+    }
+}
diff --git a/crates/colors/src/list.rs b/crates/colors/src/list.rs
new file mode 100644
index 0000000..35fcb83
--- /dev/null
+++ b/crates/colors/src/list.rs
@@ -0,0 +1,233 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::support::prepend_input;
+
+prepend_input! {
+    crate::support::methods_inner as methods (($tt:tt) -> {$tt}),
+    crate::support::elements_inner as elements,
+
+    <shared_input>
+    {
+        // Colors
+        Black black 30,
+        OnBlack on_black 40,
+
+        Red red 31,
+        OnRed on_red 41,
+
+        Green green 32,
+        OnGreen on_green 42,
+
+        Yellow yellow 33,
+        OnYellow on_yellow 43,
+
+        Blue blue 34,
+        OnBlue on_blue 44,
+
+        Magenta magenta 35,
+        OnMagenta on_magenta 45,
+
+        Cyan cyan 36,
+        OnCyan on_cyan 46,
+
+        White white 37,
+        OnWhite on_white 47,
+
+        Default default 39,
+        OnDefault on_default 49,
+
+        // Bright bright colors
+        BrightBlack bright_black 90,
+        OnBrightBlack on_bright_black 100,
+
+        BrightRed bright_red 91,
+        OnBrightRed on_bright_red 101,
+
+        BrightGreen bright_green 92,
+        OnBrightGreen on_bright_green 102,
+
+        BrightYellow bright_yellow 93,
+        OnBrightYellow on_bright_yellow 103,
+
+        BrightBlue bright_blue 94,
+        OnBrightBlue on_bright_blue 104,
+
+        BrightMagenta bright_magenta 95,
+        OnBrightMagenta on_bright_magenta 105,
+
+        BrightCyan bright_cyan 96,
+        OnBrightCyan on_bright_cyan 106,
+
+        BrightWhite bright_white 97,
+        OnBrightWhite on_bright_white 107,
+
+        // CSS colors
+        // TODO(@bpeetz): Also support background colors with these values. <2025-07-16>
+        AliceBlue alice_blue (240, 248, 255),
+        AntiqueWhite antique_white (250, 235, 215),
+        Aqua aqua (0, 255, 255),
+        Aquamarine aquamarine (127, 255, 212),
+        Azure azure (240, 255, 255),
+        Beige beige (245, 245, 220),
+        Bisque bisque (255, 228, 196),
+        // Black black (0, 0, 0),
+        BlanchedAlmond blanched_almond (255, 235, 205),
+        // Blue blue (0, 0, 255),
+        BlueViolet blue_violet (138, 43, 226),
+        Brown brown (165, 42, 42),
+        BurlyWood burly_wood (222, 184, 135),
+        CadetBlue cadet_blue (95, 158, 160),
+        Chartreuse chartreuse (127, 255, 0),
+        Chocolate chocolate (210, 105, 30),
+        Coral coral (255, 127, 80),
+        CornflowerBlue cornflower_blue (100, 149, 237),
+        Cornsilk cornsilk (255, 248, 220),
+        Crimson crimson (220, 20, 60),
+        DarkBlue dark_blue (0, 0, 139),
+        DarkCyan dark_cyan (0, 139, 139),
+        DarkGoldenRod dark_golden_rod (184, 134, 11),
+        DarkGray dark_gray (169, 169, 169),
+        DarkGrey dark_grey (169, 169, 169),
+        DarkGreen dark_green (0, 100, 0),
+        DarkKhaki dark_khaki (189, 183, 107),
+        DarkMagenta dark_magenta (139, 0, 139),
+        DarkOliveGreen dark_olive_green (85, 107, 47),
+        DarkOrange dark_orange (255, 140, 0),
+        DarkOrchid dark_orchid (153, 50, 204),
+        DarkRed dark_red (139, 0, 0),
+        DarkSalmon dark_salmon (233, 150, 122),
+        DarkSeaGreen dark_sea_green (143, 188, 143),
+        DarkSlateBlue dark_slate_blue (72, 61, 139),
+        DarkSlateGray dark_slate_gray (47, 79, 79),
+        DarkSlateGrey dark_slate_grey (47, 79, 79),
+        DarkTurquoise dark_turquoise (0, 206, 209),
+        DarkViolet dark_violet (148, 0, 211),
+        DeepPink deep_pink (255, 20, 147),
+        DeepSkyBlue deep_sky_blue (0, 191, 255),
+        DimGray dim_gray (105, 105, 105),
+        DimGrey dim_grey (105, 105, 105),
+        DodgerBlue dodger_blue (30, 144, 255),
+        FireBrick fire_brick (178, 34, 34),
+        FloralWhite floral_white (255, 250, 240),
+        ForestGreen forest_green (34, 139, 34),
+        Fuchsia fuchsia (255, 0, 255),
+        Gainsboro gainsboro (220, 220, 220),
+        GhostWhite ghost_white (248, 248, 255),
+        Gold gold (255, 215, 0),
+        GoldenRod golden_rod (218, 165, 32),
+        Gray gray (128, 128, 128),
+        Grey grey (128, 128, 128),
+        // Green green (0, 128, 0),
+        GreenYellow green_yellow (173, 255, 47),
+        HoneyDew honey_dew (240, 255, 240),
+        HotPink hot_pink (255, 105, 180),
+        IndianRed indian_red (205, 92, 92),
+        Indigo indigo (75, 0, 130),
+        Ivory ivory (255, 255, 240),
+        Khaki khaki (240, 230, 140),
+        Lavender lavender (230, 230, 250),
+        LavenderBlush lavender_blush (255, 240, 245),
+        LawnGreen lawn_green (124, 252, 0),
+        LemonChiffon lemon_chiffon (255, 250, 205),
+        LightBlue light_blue (173, 216, 230),
+        LightCoral light_coral (240, 128, 128),
+        LightCyan light_cyan (224, 255, 255),
+        LightGoldenRodYellow light_golden_rod_yellow (250, 250, 210),
+        LightGray light_gray (211, 211, 211),
+        LightGrey light_grey (211, 211, 211),
+        LightGreen light_green (144, 238, 144),
+        LightPink light_pink (255, 182, 193),
+        LightSalmon light_salmon (255, 160, 122),
+        LightSeaGreen light_sea_green (32, 178, 170),
+        LightSkyBlue light_sky_blue (135, 206, 250),
+        LightSlateGray light_slate_gray (119, 136, 153),
+        LightSlateGrey light_slate_grey (119, 136, 153),
+        LightSteelBlue light_steel_blue (176, 196, 222),
+        LightYellow light_yellow (255, 255, 224),
+        Lime lime (0, 255, 0),
+        LimeGreen lime_green (50, 205, 50),
+        Linen linen (250, 240, 230),
+        // Magenta magenta (255, 0, 255),
+        Maroon maroon (128, 0, 0),
+        MediumAquaMarine medium_aqua_marine (102, 205, 170),
+        MediumBlue medium_blue (0, 0, 205),
+        MediumOrchid medium_orchid (186, 85, 211),
+        MediumPurple medium_purple (147, 112, 219),
+        MediumSeaGreen medium_sea_green (60, 179, 113),
+        MediumSlateBlue medium_slate_blue (123, 104, 238),
+        MediumSpringGreen medium_spring_green (0, 250, 154),
+        MediumTurquoise medium_turquoise (72, 209, 204),
+        MediumVioletRed medium_violet_red (199, 21, 133),
+        MidnightBlue midnight_blue (25, 25, 112),
+        MintCream mint_cream (245, 255, 250),
+        MistyRose misty_rose (255, 228, 225),
+        Moccasin moccasin (255, 228, 181),
+        NavajoWhite navajo_white (255, 222, 173),
+        Navy navy (0, 0, 128),
+        OldLace old_lace (253, 245, 230),
+        Olive olive (128, 128, 0),
+        OliveDrab olive_drab (107, 142, 35),
+        Orange orange (255, 165, 0),
+        OrangeRed orange_red (255, 69, 0),
+        Orchid orchid (218, 112, 214),
+        PaleGoldenRod pale_golden_rod (238, 232, 170),
+        PaleGreen pale_green (152, 251, 152),
+        PaleTurquoise pale_turquoise (175, 238, 238),
+        PaleVioletRed pale_violet_red (219, 112, 147),
+        PapayaWhip papaya_whip (255, 239, 213),
+        PeachPuff peach_puff (255, 218, 185),
+        Peru peru (205, 133, 63),
+        Pink pink (255, 192, 203),
+        Plum plum (221, 160, 221),
+        PowderBlue powder_blue (176, 224, 230),
+        Purple purple (128, 0, 128),
+        RebeccaPurple rebecca_purple (102, 51, 153),
+        // Red red (255, 0, 0),
+        RosyBrown rosy_brown (188, 143, 143),
+        RoyalBlue royal_blue (65, 105, 225),
+        SaddleBrown saddle_brown (139, 69, 19),
+        Salmon salmon (250, 128, 114),
+        SandyBrown sandy_brown (244, 164, 96),
+        SeaGreen sea_green (46, 139, 87),
+        SeaShell sea_shell (255, 245, 238),
+        Sienna sienna (160, 82, 45),
+        Silver silver (192, 192, 192),
+        SkyBlue sky_blue (135, 206, 235),
+        SlateBlue slate_blue (106, 90, 205),
+        SlateGray slate_gray (112, 128, 144),
+        SlateGrey slate_grey (112, 128, 144),
+        Snow snow (255, 250, 250),
+        SpringGreen spring_green (0, 255, 127),
+        SteelBlue steel_blue (70, 130, 180),
+        Tan tan (210, 180, 140),
+        Teal teal (0, 128, 128),
+        Thistle thistle (216, 191, 216),
+        Tomato tomato (255, 99, 71),
+        Turquoise turquoise (64, 224, 208),
+        Violet violet (238, 130, 238),
+        Wheat wheat (245, 222, 179),
+        // White white (255, 255, 255),
+        WhiteSmoke white_smoke (245, 245, 245),
+        // Yellow yellow (255, 255, 0),
+        YellowGreen yellow_green (154, 205, 50),
+
+        // Styles
+        Bold bold 1,
+        Dim dim 2,
+        Italic italic 3,
+        Underline underline 4,
+        Blink blink 5,
+        BlinkFast blink_fast 6,
+        Reversed reversed 7,
+        Hidden hidden 8,
+        StrikeThrough strike_through 9,
+    }
+}
diff --git a/crates/colors/src/support.rs b/crates/colors/src/support.rs
new file mode 100644
index 0000000..3c3f87d
--- /dev/null
+++ b/crates/colors/src/support.rs
@@ -0,0 +1,126 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+pub(super) const CSI: &str = "\x1b[";
+pub(super) const CSE: &str = "m";
+
+macro_rules! elements_inner {
+    (
+        $(
+            $name:ident $_:ident $number:tt
+        ),*
+        $(,)?
+    ) => {
+        $(
+            #[derive(Debug)]
+            pub struct $name<I: Colorize>(I);
+
+            impl<I: Colorize> Colorize for $name<I> {
+                fn render_into(self, base: &mut String, use_colors: bool) {
+                    elements_inner! {@parse_number $number}
+
+                    if use_colors {
+                        base.write_str(CSI).expect("In-memory write");
+                        base.write_str(NUMBERS).expect("In-memory write");
+                        base.write_str(CSE).expect("In-memory write");
+                    }
+                    self.0.render_into(base, use_colors);
+                    // The canvas is resetting the colours again.
+                }
+            }
+        )*
+    };
+
+    (@parse_number $single:literal) => {
+        const NUMBERS: &str = stringify!($single);
+    };
+    (@parse_number ($red:literal, $green:literal, $blue:literal)) => {
+        const NUMBERS_U8: [u8; 18] = $crate::custom::rgb_to_ansi($red, $green, $blue, $crate::custom::Plane::Fg);
+
+        const NUMBERS: &str = $crate::custom::bytes_to_str(&NUMBERS_U8);
+    }
+}
+pub(super) use elements_inner;
+
+macro_rules! methods_inner {
+    (
+        Colorize
+
+        $(
+            $struct_name:ident $fn_name:ident $_:tt
+        ),*
+        $(,)?
+    ) => {
+        $(
+            fn $fn_name(self) -> $struct_name<Self> {
+                $struct_name(self)
+            }
+        )*
+    };
+    (
+        IntoCanvas
+
+        $(
+            $struct_name:ident $fn_name:ident $_:tt
+        ),*
+        $(,)?
+    ) => {
+        $(
+            fn $fn_name(self) -> $struct_name<Canvas<Self>> {
+                $struct_name(Canvas(self))
+            }
+        )*
+    };
+}
+pub(super) use methods_inner;
+
+macro_rules! prepend_input {
+    (
+        $(
+            $existing_macro_name:path as $new_macro_name:ident $(($macro_rule:tt -> $macro_apply:tt))?
+        ),*
+        $(,)?
+
+        <shared_input>
+        $shared_input:tt
+    ) => {
+        $(
+            prepend_input! {
+                @generate_macro
+                $existing_macro_name as $new_macro_name $(($macro_rule -> $macro_apply))?
+                <shared_input>
+                $shared_input
+            }
+        )*
+    };
+
+    (
+        @generate_macro
+        $existing_macro_name:path as $new_macro_name:ident $((($($macro_rule:tt)*) -> {$($macro_apply:tt)*}))?
+
+        <shared_input>
+        {
+            $(
+                $shared_input:tt
+            )*
+        }
+    ) => {
+        macro_rules! $new_macro_name {
+            ($($($macro_rule)*)?) => {
+                $existing_macro_name! {
+                    $($($macro_apply)*)?
+                    $($shared_input)*
+                }
+            }
+        }
+        pub(super) use $new_macro_name;
+    }
+}
+pub(crate) use prepend_input;
diff --git a/crates/fmt/Cargo.toml b/crates/fmt/Cargo.toml
index 7f82a09..f3cf4ad 100644
--- a/crates/fmt/Cargo.toml
+++ b/crates/fmt/Cargo.toml
@@ -24,7 +24,7 @@ publish = false
 path = "src/fmt.rs"
 
 [dependencies]
-unicode-width = "0.2.0"
+unicode-width = "0.2.1"
 
 [lints]
 workspace = true
diff --git a/crates/libmpv2/CHANGELOG.md b/crates/libmpv2/CHANGELOG.md
index dc6f861..a3d14d7 100644
--- a/crates/libmpv2/CHANGELOG.md
+++ b/crates/libmpv2/CHANGELOG.md
@@ -16,7 +16,7 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 ## Version 3.0.0
 
-- \[breaking\] Support libmpv version 2.0 (mpv version 0.35.0). Mpv versions \<=
+- [breaking] Support libmpv version 2.0 (mpv version 0.35.0). Mpv versions \<=
   0.34.0 will no longer be supported.
 - Add OpenGL rendering
 
@@ -29,10 +29,10 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 ## Version 2.0.0
 
 - Add method `Mpv::with_initializer` to set options before initialization
-- \[breaking\] Borrow `&mut self` in `wait_event` to disallow using two events
+- [breaking] Borrow `&mut self` in `wait_event` to disallow using two events
   where the first points to data freed in the second `wait_event` call
-- \[breaking\] `PropertyData<'_>` is no longer `Clone` or `PartialEq`,
-  `Event<'_>` is no longer `Clone` to avoid cloning/comparing `MpvNode`
+- [breaking] `PropertyData<'_>` is no longer `Clone` or `PartialEq`, `Event<'_>`
+  is no longer `Clone` to avoid cloning/comparing `MpvNode`
 
 ## Version 1.1.0
 
diff --git a/crates/libmpv2/Cargo.toml b/crates/libmpv2/Cargo.toml
index fb2f5bf..67fbfec 100644
--- a/crates/libmpv2/Cargo.toml
+++ b/crates/libmpv2/Cargo.toml
@@ -28,7 +28,7 @@ log.workspace = true
 
 [dev-dependencies]
 crossbeam = "0.8"
-sdl2 = "0.37.0"
+sdl2 = "0.38.0"
 
 [features]
 default = ["protocols", "render"]
diff --git a/crates/libmpv2/examples/opengl.rs b/crates/libmpv2/examples/opengl.rs
index 8eb9647..9f595aa 100644
--- a/crates/libmpv2/examples/opengl.rs
+++ b/crates/libmpv2/examples/opengl.rs
@@ -38,13 +38,16 @@ fn main() {
         Ok(())
     })
     .unwrap();
-    let mut render_context = RenderContext::new(unsafe { mpv.ctx.as_mut() }, vec![
-        RenderParam::ApiType(RenderParamApiType::OpenGl),
-        RenderParam::InitParams(OpenGLInitParams {
-            get_proc_address,
-            ctx: video,
-        }),
-    ])
+    let mut render_context = RenderContext::new(
+        unsafe { mpv.ctx.as_mut() },
+        vec![
+            RenderParam::ApiType(RenderParamApiType::OpenGl),
+            RenderParam::InitParams(OpenGLInitParams {
+                get_proc_address,
+                ctx: video,
+            }),
+        ],
+    )
     .expect("Failed creating render context");
 
     event_subsystem
diff --git a/crates/libmpv2/libmpv2-sys/Cargo.toml b/crates/libmpv2/libmpv2-sys/Cargo.toml
index b0514b8..96141d3 100644
--- a/crates/libmpv2/libmpv2-sys/Cargo.toml
+++ b/crates/libmpv2/libmpv2-sys/Cargo.toml
@@ -23,4 +23,4 @@ rust-version.workspace = true
 publish = false
 
 [build-dependencies]
-bindgen = { version = "0.71.1" }
+bindgen = { version = "0.72.0" }
diff --git a/crates/libmpv2/libmpv2-sys/build.rs b/crates/libmpv2/libmpv2-sys/build.rs
index bf9a02e..45c2450 100644
--- a/crates/libmpv2/libmpv2-sys/build.rs
+++ b/crates/libmpv2/libmpv2-sys/build.rs
@@ -30,7 +30,9 @@ fn main() {
             ),
             "--verbose",
         ])
-        .generate_comments(true)
+        // NOTE(@bpeetz): The comments are interpreted as doc-tests,
+        // which obviously fail, as the code is c. <2025-06-16>
+        .generate_comments(false)
         .generate()
         .expect("Unable to generate bindings");
 
diff --git a/crates/libmpv2/src/lib.rs b/crates/libmpv2/src/lib.rs
index d47e620..f6c2103 100644
--- a/crates/libmpv2/src/lib.rs
+++ b/crates/libmpv2/src/lib.rs
@@ -35,7 +35,7 @@ use std::os::raw as ctype;
 pub const MPV_CLIENT_API_MAJOR: ctype::c_ulong = 2;
 pub const MPV_CLIENT_API_MINOR: ctype::c_ulong = 2;
 pub const MPV_CLIENT_API_VERSION: ctype::c_ulong =
-    MPV_CLIENT_API_MAJOR << 16 | MPV_CLIENT_API_MINOR;
+    (MPV_CLIENT_API_MAJOR << 16) | MPV_CLIENT_API_MINOR;
 
 mod mpv;
 #[cfg(test)]
diff --git a/crates/libmpv2/src/mpv.rs b/crates/libmpv2/src/mpv.rs
index 29dac8d..d8164c0 100644
--- a/crates/libmpv2/src/mpv.rs
+++ b/crates/libmpv2/src/mpv.rs
@@ -552,21 +552,21 @@ impl Mpv {
     ///
     /// # Examples
     ///
-    /// ```dont_run
-    /// # use libmpv2::{Mpv};
-    /// # use libmpv2::mpv_node::MpvNode;
-    /// # use libmpv2::mpv::errors::Result;
-    /// # use std::collections::HashMap;
-    /// #
-    /// # fn main() -> Result<()> {
-    /// # let mpv = Mpv::new()?;
+    /// ```text
+    ///# use libmpv2::{Mpv};
+    ///# use libmpv2::mpv_node::MpvNode;
+    ///# use libmpv2::mpv::errors::Result;
+    ///# use std::collections::HashMap;
+    ///#
+    ///# fn main() -> Result<()> {
+    ///# let mpv = Mpv::new()?;
     /// mpv.command("loadfile", &["test-data/jellyfish.mp4", "append-play"]).unwrap();
-    /// # let node = mpv.get_property::<MpvNode>("playlist").unwrap();
-    /// # let mut list = node.array().unwrap().collect::<Vec<_>>();
-    /// # let map = list.pop().unwrap().map().unwrap().collect::<HashMap<_, _>>();
-    /// # assert_eq!(map, HashMap::from([(String::from("id"), MpvNode::Int64(1)), (String::from("current"), MpvNode::Flag(true)), (String::from("filename"), MpvNode::String(String::from("test-data/jellyfish.mp4")))]));
-    /// # Ok(())
-    /// # }
+    ///# let node = mpv.get_property::<MpvNode>("playlist").unwrap();
+    ///# let mut list = node.array().unwrap().collect::<Vec<_>>();
+    ///# let map = list.pop().unwrap().map().unwrap().collect::<HashMap<_, _>>();
+    ///# assert_eq!(map, HashMap::from([(String::from("id"), MpvNode::Int64(1)), (String::from("current"), MpvNode::Flag(true)), (String::from("filename"), MpvNode::String(String::from("test-data/jellyfish.mp4")))]));
+    ///# Ok(())
+    ///# }
     /// ```
     pub fn command(&self, name: &str, args: &[&str]) -> Result<()> {
         fn escape(input: &str) -> String {
diff --git a/crates/libmpv2/src/mpv/events.rs b/crates/libmpv2/src/mpv/events.rs
index e27da2c..f10ff6e 100644
--- a/crates/libmpv2/src/mpv/events.rs
+++ b/crates/libmpv2/src/mpv/events.rs
@@ -70,26 +70,28 @@ impl<'a> PropertyData<'a> {
     // SAFETY: meant to extract the data from an event property. See `mpv_event_property` in
     // `client.h`
     unsafe fn from_raw(format: MpvFormat, ptr: *mut ctype::c_void) -> Result<PropertyData<'a>> {
-        assert!(!ptr.is_null());
-        match format {
-            mpv_format::Flag => Ok(PropertyData::Flag(*(ptr as *mut bool))),
-            mpv_format::String => {
-                let char_ptr = *(ptr as *mut *mut ctype::c_char);
-                Ok(PropertyData::Str(mpv_cstr_to_str!(char_ptr)?))
-            }
-            mpv_format::OsdString => {
-                let char_ptr = *(ptr as *mut *mut ctype::c_char);
-                Ok(PropertyData::OsdStr(mpv_cstr_to_str!(char_ptr)?))
-            }
-            mpv_format::Double => Ok(PropertyData::Double(*(ptr as *mut f64))),
-            mpv_format::Int64 => Ok(PropertyData::Int64(*(ptr as *mut i64))),
-            mpv_format::Node => {
-                let sys_node = *(ptr as *mut libmpv2_sys::mpv_node);
-                let node = SysMpvNode::new(sys_node, false);
-                Ok(PropertyData::Node(node.value().unwrap()))
+        unsafe {
+            assert!(!ptr.is_null());
+            match format {
+                mpv_format::Flag => Ok(PropertyData::Flag(*(ptr as *mut bool))),
+                mpv_format::String => {
+                    let char_ptr = *(ptr as *mut *mut ctype::c_char);
+                    Ok(PropertyData::Str(mpv_cstr_to_str!(char_ptr)?))
+                }
+                mpv_format::OsdString => {
+                    let char_ptr = *(ptr as *mut *mut ctype::c_char);
+                    Ok(PropertyData::OsdStr(mpv_cstr_to_str!(char_ptr)?))
+                }
+                mpv_format::Double => Ok(PropertyData::Double(*(ptr as *mut f64))),
+                mpv_format::Int64 => Ok(PropertyData::Int64(*(ptr as *mut i64))),
+                mpv_format::Node => {
+                    let sys_node = *(ptr as *mut libmpv2_sys::mpv_node);
+                    let node = SysMpvNode::new(sys_node, false);
+                    Ok(PropertyData::Node(node.value().unwrap()))
+                }
+                mpv_format::None => unreachable!(),
+                _ => unimplemented!(),
             }
-            mpv_format::None => unreachable!(),
-            _ => unimplemented!(),
         }
     }
 }
@@ -146,11 +148,13 @@ pub enum Event<'a> {
 }
 
 unsafe extern "C" fn wu_wrapper<F: Fn() + Send + 'static>(ctx: *mut c_void) {
-    if ctx.is_null() {
-        panic!("ctx for wakeup wrapper is NULL");
-    }
+    unsafe {
+        if ctx.is_null() {
+            panic!("ctx for wakeup wrapper is NULL");
+        }
 
-    (*(ctx as *mut F))();
+        (*(ctx as *mut F))();
+    }
 }
 
 /// Context to listen to events.
diff --git a/crates/libmpv2/src/mpv/protocol.rs b/crates/libmpv2/src/mpv/protocol.rs
index ec840d8..ee33411 100644
--- a/crates/libmpv2/src/mpv/protocol.rs
+++ b/crates/libmpv2/src/mpv/protocol.rs
@@ -63,26 +63,28 @@ where
     T: RefUnwindSafe,
     U: RefUnwindSafe,
 {
-    let data = user_data as *mut ProtocolData<T, U>;
+    unsafe {
+        let data = user_data as *mut ProtocolData<T, U>;
 
-    (*info).cookie = user_data;
-    (*info).read_fn = Some(read_wrapper::<T, U>);
-    (*info).seek_fn = Some(seek_wrapper::<T, U>);
-    (*info).size_fn = Some(size_wrapper::<T, U>);
-    (*info).close_fn = Some(close_wrapper::<T, U>);
+        (*info).cookie = user_data;
+        (*info).read_fn = Some(read_wrapper::<T, U>);
+        (*info).seek_fn = Some(seek_wrapper::<T, U>);
+        (*info).size_fn = Some(size_wrapper::<T, U>);
+        (*info).close_fn = Some(close_wrapper::<T, U>);
 
-    let ret = panic::catch_unwind(|| {
-        let uri = mpv_cstr_to_str!(uri as *const _).unwrap();
-        ptr::write(
-            (*data).cookie,
-            ((*data).open_fn)(&mut (*data).user_data, uri),
-        );
-    });
+        let ret = panic::catch_unwind(|| {
+            let uri = mpv_cstr_to_str!(uri as *const _).unwrap();
+            ptr::write(
+                (*data).cookie,
+                ((*data).open_fn)(&mut (*data).user_data, uri),
+            );
+        });
 
-    if ret.is_ok() {
-        0
-    } else {
-        mpv_error::Generic as _
+        if ret.is_ok() {
+            0
+        } else {
+            mpv_error::Generic as _
+        }
     }
 }
 
@@ -95,13 +97,15 @@ where
     T: RefUnwindSafe,
     U: RefUnwindSafe,
 {
-    let data = cookie as *mut ProtocolData<T, U>;
+    unsafe {
+        let data = cookie as *mut ProtocolData<T, U>;
 
-    let ret = panic::catch_unwind(|| {
-        let slice = slice::from_raw_parts_mut(buf, nbytes as _);
-        ((*data).read_fn)(&mut *(*data).cookie, slice)
-    });
-    ret.unwrap_or(-1)
+        let ret = panic::catch_unwind(|| {
+            let slice = slice::from_raw_parts_mut(buf, nbytes as _);
+            ((*data).read_fn)(&mut *(*data).cookie, slice)
+        });
+        ret.unwrap_or(-1)
+    }
 }
 
 unsafe extern "C" fn seek_wrapper<T, U>(cookie: *mut ctype::c_void, offset: i64) -> i64
@@ -109,18 +113,21 @@ where
     T: RefUnwindSafe,
     U: RefUnwindSafe,
 {
-    let data = cookie as *mut ProtocolData<T, U>;
+    unsafe {
+        let data = cookie as *mut ProtocolData<T, U>;
 
-    if (*data).seek_fn.is_none() {
-        return mpv_error::Unsupported as _;
-    }
+        if (*data).seek_fn.is_none() {
+            return mpv_error::Unsupported as _;
+        }
 
-    let ret =
-        panic::catch_unwind(|| (*(*data).seek_fn.as_ref().unwrap())(&mut *(*data).cookie, offset));
-    if let Ok(ret) = ret {
-        ret
-    } else {
-        mpv_error::Generic as _
+        let ret = panic::catch_unwind(|| {
+            (*(*data).seek_fn.as_ref().unwrap())(&mut *(*data).cookie, offset)
+        });
+        if let Ok(ret) = ret {
+            ret
+        } else {
+            mpv_error::Generic as _
+        }
     }
 }
 
@@ -129,17 +136,20 @@ where
     T: RefUnwindSafe,
     U: RefUnwindSafe,
 {
-    let data = cookie as *mut ProtocolData<T, U>;
+    unsafe {
+        let data = cookie as *mut ProtocolData<T, U>;
 
-    if (*data).size_fn.is_none() {
-        return mpv_error::Unsupported as _;
-    }
+        if (*data).size_fn.is_none() {
+            return mpv_error::Unsupported as _;
+        }
 
-    let ret = panic::catch_unwind(|| (*(*data).size_fn.as_ref().unwrap())(&mut *(*data).cookie));
-    if let Ok(ret) = ret {
-        ret
-    } else {
-        mpv_error::Unsupported as _
+        let ret =
+            panic::catch_unwind(|| (*(*data).size_fn.as_ref().unwrap())(&mut *(*data).cookie));
+        if let Ok(ret) = ret {
+            ret
+        } else {
+            mpv_error::Unsupported as _
+        }
     }
 }
 
@@ -149,9 +159,11 @@ where
     T: RefUnwindSafe,
     U: RefUnwindSafe,
 {
-    let data = Box::from_raw(cookie as *mut ProtocolData<T, U>);
+    unsafe {
+        let data = Box::from_raw(cookie as *mut ProtocolData<T, U>);
 
-    panic::catch_unwind(|| (data.close_fn)(Box::from_raw(data.cookie)));
+        panic::catch_unwind(|| (data.close_fn)(Box::from_raw(data.cookie)));
+    }
 }
 
 struct ProtocolData<T, U> {
@@ -224,20 +236,23 @@ impl<T: RefUnwindSafe, U: RefUnwindSafe> Protocol<T, U> {
         seek_fn: Option<StreamSeek<T>>,
         size_fn: Option<StreamSize<T>>,
     ) -> Protocol<T, U> {
-        let c_layout = Layout::from_size_align(mem::size_of::<T>(), mem::align_of::<T>()).unwrap();
-        let cookie = alloc::alloc(c_layout) as *mut T;
-        let data = Box::into_raw(Box::new(ProtocolData {
-            cookie,
-            user_data,
+        unsafe {
+            let c_layout =
+                Layout::from_size_align(mem::size_of::<T>(), mem::align_of::<T>()).unwrap();
+            let cookie = alloc::alloc(c_layout) as *mut T;
+            let data = Box::into_raw(Box::new(ProtocolData {
+                cookie,
+                user_data,
 
-            open_fn,
-            close_fn,
-            read_fn,
-            seek_fn,
-            size_fn,
-        }));
+                open_fn,
+                close_fn,
+                read_fn,
+                seek_fn,
+                size_fn,
+            }));
 
-        Protocol { name, data }
+            Protocol { name, data }
+        }
     }
 
     fn register(&self, ctx: *mut libmpv2_sys::mpv_handle) -> Result<()> {
diff --git a/crates/libmpv2/src/mpv/render.rs b/crates/libmpv2/src/mpv/render.rs
index 6457048..02f70bb 100644
--- a/crates/libmpv2/src/mpv/render.rs
+++ b/crates/libmpv2/src/mpv/render.rs
@@ -125,26 +125,30 @@ impl<C> From<&RenderParam<C>> for u32 {
 }
 
 unsafe extern "C" fn gpa_wrapper<GLContext>(ctx: *mut c_void, name: *const i8) -> *mut c_void {
-    if ctx.is_null() {
-        panic!("ctx for get_proc_address wrapper is NULL");
-    }
+    unsafe {
+        if ctx.is_null() {
+            panic!("ctx for get_proc_address wrapper is NULL");
+        }
 
-    let params: *mut OpenGLInitParams<GLContext> = ctx as _;
-    let params = &*params;
-    (params.get_proc_address)(
-        &params.ctx,
-        CStr::from_ptr(name)
-            .to_str()
-            .expect("Could not convert function name to str"),
-    )
+        let params: *mut OpenGLInitParams<GLContext> = ctx as _;
+        let params = &*params;
+        (params.get_proc_address)(
+            &params.ctx,
+            CStr::from_ptr(name)
+                .to_str()
+                .expect("Could not convert function name to str"),
+        )
+    }
 }
 
 unsafe extern "C" fn ru_wrapper<F: Fn() + Send + 'static>(ctx: *mut c_void) {
-    if ctx.is_null() {
-        panic!("ctx for render_update wrapper is NULL");
-    }
+    unsafe {
+        if ctx.is_null() {
+            panic!("ctx for render_update wrapper is NULL");
+        }
 
-    (*(ctx as *mut F))();
+        (*(ctx as *mut F))();
+    }
 }
 
 impl<C> From<OpenGLInitParams<C>> for libmpv2_sys::mpv_opengl_init_params {
@@ -197,14 +201,18 @@ impl<C> From<RenderParam<C>> for libmpv2_sys::mpv_render_param {
 }
 
 unsafe fn free_void_data<T>(ptr: *mut c_void) {
-    drop(Box::<T>::from_raw(ptr as *mut T));
+    unsafe {
+        drop(Box::<T>::from_raw(ptr as *mut T));
+    }
 }
 
 unsafe fn free_init_params<C>(ptr: *mut c_void) {
-    let params = Box::from_raw(ptr as *mut libmpv2_sys::mpv_opengl_init_params);
-    drop(Box::from_raw(
-        params.get_proc_address_ctx as *mut OpenGLInitParams<C>,
-    ));
+    unsafe {
+        let params = Box::from_raw(ptr as *mut libmpv2_sys::mpv_opengl_init_params);
+        drop(Box::from_raw(
+            params.get_proc_address_ctx as *mut OpenGLInitParams<C>,
+        ));
+    }
 }
 
 impl RenderContext {
diff --git a/crates/libmpv2/src/tests.rs b/crates/libmpv2/src/tests.rs
index 6106eb2..68753fc 100644
--- a/crates/libmpv2/src/tests.rs
+++ b/crates/libmpv2/src/tests.rs
@@ -54,10 +54,10 @@ fn properties() {
         0.6,
         f64::round(subg * f64::powi(10.0, 4)) / f64::powi(10.0, 4)
     );
-    mpv.command("loadfile", &[
-        "test-data/speech_12kbps_mb.wav",
-        "append-play",
-    ])
+    mpv.command(
+        "loadfile",
+        &["test-data/speech_12kbps_mb.wav", "append-play"],
+    )
     .unwrap();
     thread::sleep(Duration::from_millis(250));
 
@@ -185,10 +185,10 @@ fn events() {
 fn node_map() {
     let mpv = Mpv::new().unwrap();
 
-    mpv.command("loadfile", &[
-        "test-data/speech_12kbps_mb.wav",
-        "append-play",
-    ])
+    mpv.command(
+        "loadfile",
+        &["test-data/speech_12kbps_mb.wav", "append-play"],
+    )
     .unwrap();
 
     thread::sleep(Duration::from_millis(250));
@@ -217,10 +217,10 @@ fn node_map() {
 fn node_array() -> Result<()> {
     let mpv = Mpv::new()?;
 
-    mpv.command("loadfile", &[
-        "test-data/speech_12kbps_mb.wav",
-        "append-play",
-    ])
+    mpv.command(
+        "loadfile",
+        &["test-data/speech_12kbps_mb.wav", "append-play"],
+    )
     .unwrap();
 
     thread::sleep(Duration::from_millis(250));
diff --git a/crates/libmpv2/update.sh b/crates/libmpv2/update.sh
index ecd5aa8..591684a 100755
--- a/crates/libmpv2/update.sh
+++ b/crates/libmpv2/update.sh
@@ -10,8 +10,4 @@
 # You should have received a copy of the License along with this program.
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-cd "$(dirname "$0")" || exit 1
-[ "$1" = "upgrade" ] && cargo upgrade --incompatible
-cargo update
-
 ./libmpv2-sys/update.sh "$@"
diff --git a/yt/Cargo.toml b/crates/yt/Cargo.toml
index 6f6e470..0b6c581 100644
--- a/yt/Cargo.toml
+++ b/crates/yt/Cargo.toml
@@ -24,42 +24,44 @@ rust-version.workspace = true
 publish = false
 
 [dependencies]
-anyhow = "1.0.96"
-blake3 = "1.6.0"
-chrono = { version = "0.4.39", features = ["now"] }
+anyhow = "1.0.98"
+blake3 = { version = "1.8.2", features = ["serde"] }
+chrono = { version = "0.4.41", features = ["now"] }
 chrono-humanize = "0.2.3"
-clap = { version = "4.5.30", features = ["derive"] }
+clap = { version = "4.5.41", features = ["derive"] }
+clap_complete = { version = "4.5.55", features = ["unstable-dynamic"] }
+colors.workspace = true
 futures = "0.3.31"
-nucleo-matcher = "0.3.1"
-owo-colors = "4.1.0"
-regex = "1.11.1"
-sqlx = { version = "0.8.3", features = ["runtime-tokio", "sqlite"] }
-stderrlog = "0.6.0"
-tempfile = "3.17.1"
-toml = "0.8.20"
-trinitry = { version = "0.2.2" }
-xdg = "2.5.2"
-bytes.workspace = true
 libmpv2.workspace = true
 log.workspace = true
+notify = { version = "8.1.0", default-features = false }
+regex = "1.11.1"
 serde.workspace = true
 serde_json.workspace = true
+shlex = "1.3.0"
+sqlx = { version = "0.8.6", features = ["runtime-tokio", "sqlite"] }
+stderrlog = "0.6.0"
+tempfile = "3.20.0"
+termsize.workspace = true
+tokio-util = { version = "0.7.15", features = ["rt"] }
 tokio.workspace = true
+toml = "0.9.2"
 url.workspace = true
-yt_dlp.workspace = true
-termsize.workspace = true
 uu_fmt.workspace = true
-notify = { version = "8.0.0", default-features = false }
+xdg = "3.0.0"
+yt_dlp.workspace = true
+reqwest = "0.12.22"
 
 [[bin]]
 name = "yt"
 doc = false
 path = "src/main.rs"
 
-[dev-dependencies]
-
 [lints]
 workspace = true
 
+[dev-dependencies]
+pretty_assertions = "1.4.1"
+
 [package.metadata.docs.rs]
 all-features = true
diff --git a/crates/yt/src/ansi_escape_codes.rs b/crates/yt/src/ansi_escape_codes.rs
new file mode 100644
index 0000000..28a8370
--- /dev/null
+++ b/crates/yt/src/ansi_escape_codes.rs
@@ -0,0 +1,29 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+// see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
+const CSI: &str = "\x1b[";
+pub(crate) fn erase_from_cursor_to_bottom() {
+    print!("{CSI}0J");
+}
+pub(crate) fn cursor_up(number: usize) {
+    // HACK(@bpeetz): The default is `1` and running this command with a
+    // number of `0` results in it using the default (i.e., `1`) <2025-03-25>
+    if number != 0 {
+        print!("{CSI}{number}A");
+    }
+}
+
+pub(crate) fn clear_whole_line() {
+    eprint!("{CSI}2K");
+}
+pub(crate) fn move_to_col(x: usize) {
+    eprint!("{CSI}{x}G");
+}
diff --git a/yt/src/app.rs b/crates/yt/src/app.rs
index 15a9388..3ea12a4 100644
--- a/yt/src/app.rs
+++ b/crates/yt/src/app.rs
@@ -16,13 +16,13 @@ use sqlx::{SqlitePool, sqlite::SqliteConnectOptions};
 use crate::{config::Config, storage::migrate::migrate_db};
 
 #[derive(Debug)]
-pub struct App {
-    pub database: SqlitePool,
-    pub config: Config,
+pub(crate) struct App {
+    pub(crate) database: SqlitePool,
+    pub(crate) config: Config,
 }
 
 impl App {
-    pub async fn new(config: Config, should_migrate_db: bool) -> Result<Self> {
+    pub(crate) async fn new(config: Config, should_migrate_db: bool) -> Result<Self> {
         let options = SqliteConnectOptions::new()
             .filename(&config.paths.database_path)
             .optimize_on_close(true, None)
diff --git a/crates/yt/src/cli.rs b/crates/yt/src/cli.rs
new file mode 100644
index 0000000..9a24403
--- /dev/null
+++ b/crates/yt/src/cli.rs
@@ -0,0 +1,67 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::path::PathBuf;
+
+use clap::{ArgAction, Parser};
+
+use crate::commands::Command;
+
+#[derive(Parser, Debug)]
+#[clap(author, about, long_about = None)]
+#[allow(clippy::module_name_repetitions)]
+/// An command line interface to select, download and watch videos
+pub(crate) struct CliArgs {
+    #[command(subcommand)]
+    /// The subcommand to execute [default: select]
+    pub(crate) command: Option<Command>,
+
+    /// Show the version and exit
+    #[arg(long, short = 'V', action= ArgAction::SetTrue)]
+    pub(crate) version: bool,
+
+    /// Do not perform database migration before starting.
+    /// Setting this could cause runtime database access errors.
+    #[arg(long, short, action=ArgAction::SetTrue, default_value_t = false)]
+    pub(crate) no_migrate_db: bool,
+
+    /// Display colors [defaults to true, if the config file has no value]
+    #[arg(long, short = 'C')]
+    pub(crate) color: Option<bool>,
+
+    /// Set the path to the videos.db. This overrides the default and the config file.
+    #[arg(long, short)]
+    pub(crate) db_path: Option<PathBuf>,
+
+    /// Set the path to the config.toml.
+    /// This overrides the default.
+    #[arg(long, short)]
+    pub(crate) config_path: Option<PathBuf>,
+
+    /// Increase message verbosity
+    #[arg(long="verbose", short = 'v', action = ArgAction::Count)]
+    pub(crate) verbosity: u8,
+
+    /// Silence all output
+    #[arg(long, short = 'q')]
+    pub(crate) quiet: bool,
+}
+
+#[cfg(test)]
+mod test {
+    use clap::CommandFactory;
+
+    use super::CliArgs;
+    #[test]
+    fn verify_cli() {
+        CliArgs::command().debug_assert();
+    }
+}
diff --git a/crates/yt/src/commands/cache/implm.rs b/crates/yt/src/commands/cache/implm.rs
new file mode 100644
index 0000000..fd0fbce
--- /dev/null
+++ b/crates/yt/src/commands/cache/implm.rs
@@ -0,0 +1,40 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    commands::CacheCommand,
+    storage::db::{
+        insert::Operations,
+        video::{Video, VideoStatusMarker},
+    },
+};
+
+use anyhow::Result;
+
+impl CacheCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        match self {
+            CacheCommand::Clear {} => {
+                let mut videos = Video::in_states(app, &[VideoStatusMarker::Cached]).await?;
+
+                let mut ops = Operations::new("Cache clear");
+
+                for vid in &mut videos {
+                    vid.remove_download_path(&mut ops);
+                }
+
+                ops.commit(app).await?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/yt/src/commands/cache/mod.rs b/crates/yt/src/commands/cache/mod.rs
new file mode 100644
index 0000000..4ed4b40
--- /dev/null
+++ b/crates/yt/src/commands/cache/mod.rs
@@ -0,0 +1,19 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::Subcommand;
+
+mod implm;
+
+#[derive(Debug, Subcommand)]
+pub(super) enum CacheCommand {
+    /// Remove all downloaded video files.
+    Clear {},
+}
diff --git a/crates/yt/src/commands/config/implm.rs b/crates/yt/src/commands/config/implm.rs
new file mode 100644
index 0000000..00c28a9
--- /dev/null
+++ b/crates/yt/src/commands/config/implm.rs
@@ -0,0 +1,23 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{app::App, commands::config::ConfigCommand};
+
+use anyhow::Result;
+
+impl ConfigCommand {
+    pub(in crate::commands) fn implm(self, app: &App) -> Result<()> {
+        let config_str = toml::to_string(&app.config)?;
+
+        print!("{config_str}");
+
+        Ok(())
+    }
+}
diff --git a/yt/src/constants.rs b/crates/yt/src/commands/config/mod.rs
index 0f5b918..503b4f7 100644
--- a/yt/src/constants.rs
+++ b/crates/yt/src/commands/config/mod.rs
@@ -1,6 +1,5 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
@@ -9,4 +8,9 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-pub const HELP_STR: &str = include_str!("./select/selection_file/help.str");
+use clap::Parser;
+
+mod implm;
+
+#[derive(Parser, Debug)]
+pub(super) struct ConfigCommand {}
diff --git a/crates/yt/src/commands/database/implm.rs b/crates/yt/src/commands/database/implm.rs
new file mode 100644
index 0000000..07d346b
--- /dev/null
+++ b/crates/yt/src/commands/database/implm.rs
@@ -0,0 +1,45 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    commands::DatabaseCommand,
+    storage::db::{
+        insert::{Committable, subscription, video},
+        txn_log::TxnLog,
+    },
+};
+
+use anyhow::Result;
+
+impl DatabaseCommand {
+    pub(in crate::commands) async fn implm(&self, app: &App) -> Result<()> {
+        match self {
+            DatabaseCommand::Log { kind } => match kind {
+                super::OperationType::Video => {
+                    let log = TxnLog::<video::Operation>::get(app).await?;
+                    display_log(&log);
+                }
+                super::OperationType::Subscription => {
+                    let log = TxnLog::<subscription::Operation>::get(app).await?;
+                    display_log(&log);
+                }
+            },
+        }
+
+        Ok(())
+    }
+}
+
+fn display_log<O: Committable>(log: &TxnLog<O>) {
+    for (time, value) in log.inner() {
+        println!("At {time}: {value:?}");
+    }
+}
diff --git a/crates/yt/src/commands/database/mod.rs b/crates/yt/src/commands/database/mod.rs
new file mode 100644
index 0000000..06e3169
--- /dev/null
+++ b/crates/yt/src/commands/database/mod.rs
@@ -0,0 +1,41 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::fmt::{self, Display};
+
+use clap::{Subcommand, ValueEnum};
+
+mod implm;
+
+#[derive(Subcommand, Debug)]
+pub(super) enum DatabaseCommand {
+    /// Show the history of operations, in they groups they were committed in.
+    Log {
+        /// What kind of operation to show.
+        #[arg(short, long, default_value_t)]
+        kind: OperationType,
+    },
+}
+
+#[derive(Debug, Clone, Copy, ValueEnum, Default)]
+pub(super) enum OperationType {
+    #[default]
+    Video,
+    Subscription,
+}
+
+impl Display for OperationType {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        match self {
+            OperationType::Video => f.write_str("video"),
+            OperationType::Subscription => f.write_str("subscription"),
+        }
+    }
+}
diff --git a/crates/yt/src/commands/download/implm/download/download_options.rs b/crates/yt/src/commands/download/implm/download/download_options.rs
new file mode 100644
index 0000000..15fed7e
--- /dev/null
+++ b/crates/yt/src/commands/download/implm/download/download_options.rs
@@ -0,0 +1,121 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use anyhow::Context;
+use serde_json::{Value, json};
+use yt_dlp::{YoutubeDL, options::YoutubeDLOptions};
+
+use crate::app::App;
+
+use super::progress_hook::wrapped_progress_hook;
+
+pub(crate) fn download_opts(
+    app: &App,
+    subtitle_langs: Option<&String>,
+) -> anyhow::Result<YoutubeDL> {
+    YoutubeDLOptions::new()
+        .with_progress_hook(wrapped_progress_hook)
+        .set("extract_flat", "in_playlist")
+        .set(
+            "extractor_args",
+            json! {
+            {
+                "youtube": {
+                    "comment_sort": [ "top" ],
+                    "max_comments": [ "150", "all", "100" ]
+                }
+            }
+            },
+        )
+        //.set("cookiesfrombrowser", json! {("firefox", "me.google", None::<String>, "youtube_dlp")})
+        .set("prefer_free_formats", true)
+        .set("ffmpeg_location", env!("FFMPEG_LOCATION"))
+        .set("format", "bestvideo[height<=?1080]+bestaudio/best")
+        .set("fragment_retries", 10)
+        .set("getcomments", true)
+        .set("ignoreerrors", false)
+        .set("retries", 10)
+        .set("writeinfojson", true)
+        // NOTE: This results in a constant warning message.  <2025-01-04>
+        //.set("writeannotations", true)
+        .set("writesubtitles", true)
+        .set("writeautomaticsub", true)
+        .set(
+            "outtmpl",
+            json! {
+            {
+                "default": app.config.paths.download_dir.join("%(channel)s/%(title)s.%(ext)s"),
+                "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s"
+            }
+            },
+        )
+        .set("compat_opts", json! {{}})
+        .set("forceprint", json! {{}})
+        .set("print_to_file", json! {{}})
+        .set("windowsfilenames", false)
+        .set("restrictfilenames", false)
+        .set("trim_file_names", false)
+        .set(
+            "postprocessors",
+            json! {
+            [
+                {
+                    "api": "https://sponsor.ajay.app",
+                    "categories": [
+                        "interaction",
+                        "intro",
+                        "music_offtopic",
+                        "sponsor",
+                        "outro",
+                        "poi_highlight",
+                        "preview",
+                        "selfpromo",
+                        "filler",
+                        "chapter"
+                    ],
+                    "key": "SponsorBlock",
+                    "when": "after_filter"
+                },
+                {
+                    "force_keyframes": false,
+                    "key": "ModifyChapters",
+                    "remove_chapters_patterns": [],
+                    "remove_ranges": [],
+                    "remove_sponsor_segments": [ "sponsor" ],
+                    "sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l"
+                },
+                {
+                    "add_chapters": true,
+                    "add_infojson": null,
+                    "add_metadata": false,
+                    "key": "FFmpegMetadata"
+                },
+                {
+                    "key": "FFmpegConcat",
+                    "only_multi_video": true,
+                    "when": "playlist"
+                }
+            ]
+            },
+        )
+        .set(
+            "subtitleslangs",
+            Value::Array(
+                subtitle_langs
+                    .map_or("", String::as_str)
+                    .split(',')
+                    .map(|val| Value::String(val.to_owned()))
+                    .collect::<Vec<_>>(),
+            ),
+        )
+        .build()
+        .context("Failed to instanciate download yt_dlp")
+}
diff --git a/crates/yt/src/commands/download/implm/download/mod.rs b/crates/yt/src/commands/download/implm/download/mod.rs
new file mode 100644
index 0000000..876d6e6
--- /dev/null
+++ b/crates/yt/src/commands/download/implm/download/mod.rs
@@ -0,0 +1,290 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{collections::HashMap, path::PathBuf, sync::Arc, time::Duration};
+
+use crate::{
+    app::App,
+    commands::download::implm::download::download_options::download_opts,
+    shared::bytes::Bytes,
+    storage::{
+        db::{extractor_hash::ExtractorHash, insert::Operations, video::Video},
+        notify::{wait_for_cache_reduction, wait_for_db_write},
+    },
+    yt_dlp::get_current_cache_allocation,
+};
+
+use anyhow::{Context, Result, bail};
+use log::{debug, error, info, warn};
+use tokio::{select, task::JoinHandle, time};
+use yt_dlp::YoutubeDL;
+
+#[allow(clippy::module_name_repetitions)]
+pub(crate) mod download_options;
+pub(crate) mod progress_hook;
+
+#[derive(Debug)]
+#[allow(clippy::module_name_repetitions)]
+pub(crate) struct CurrentDownload {
+    task_handle: JoinHandle<Result<(PathBuf, Video)>>,
+    yt_dlp: Arc<YoutubeDL>,
+    extractor_hash: ExtractorHash,
+}
+
+impl CurrentDownload {
+    fn new_from_video(app: &App, video: Video) -> Result<Self> {
+        let extractor_hash = video.extractor_hash;
+
+        debug!("Download started: {}", &video.title);
+        let yt_dlp = Arc::new(download_opts(app, video.subtitle_langs.as_ref())?);
+
+        let local_yt_dlp = Arc::clone(&yt_dlp);
+
+        let task_handle = tokio::task::spawn_blocking(move || {
+            let mut result = local_yt_dlp
+                .download(&[video.url.clone()])
+                .with_context(|| format!("Failed to download video: '{}'", video.title))?;
+
+            assert_eq!(result.len(), 1);
+            Ok((result.remove(0), video))
+        });
+
+        Ok(Self {
+            task_handle,
+            yt_dlp,
+            extractor_hash,
+        })
+    }
+
+    fn abort(self) -> Result<()> {
+        debug!("Cancelling download.");
+        self.yt_dlp.close()?;
+
+        Ok(())
+    }
+
+    fn is_finished(&self) -> bool {
+        self.task_handle.is_finished()
+    }
+
+    async fn finalize(self, app: &App) -> Result<()> {
+        let (result, mut video) = self.task_handle.await??;
+
+        let mut ops = Operations::new("Downloader: Set download path");
+        video.set_download_path(&result, &mut ops);
+        ops.commit(app)
+            .await
+            .with_context(|| format!("Failed to committ download of video: '{}'", video.title))?;
+
+        info!(
+            "Video '{}' was downlaoded to path: {}",
+            video.title,
+            result.display()
+        );
+
+        Ok(())
+    }
+}
+
+enum CacheSizeCheck {
+    /// The video can be downloaded
+    Fits,
+
+    /// The video and the current cache size together would exceed the size
+    TooLarge,
+
+    /// The video would not even fit into the empty cache
+    ExceedsMaxCacheSize,
+}
+
+#[derive(Debug)]
+pub(crate) struct Downloader {
+    current_download: Option<CurrentDownload>,
+    video_size_cache: HashMap<ExtractorHash, u64>,
+    printed_warning: bool,
+    cached_cache_allocation: Option<Bytes>,
+}
+
+impl Default for Downloader {
+    fn default() -> Self {
+        Self::new()
+    }
+}
+
+impl Downloader {
+    #[must_use]
+    pub(crate) fn new() -> Self {
+        Self {
+            current_download: None,
+            video_size_cache: HashMap::new(),
+            printed_warning: false,
+            cached_cache_allocation: None,
+        }
+    }
+
+    /// Check if enough cache is available.
+    ///
+    /// Will wait for the next cache deletion if not.
+    async fn is_enough_cache_available(
+        &mut self,
+        app: &App,
+        max_cache_size: u64,
+        next_video: &Video,
+    ) -> Result<CacheSizeCheck> {
+        if let Some(cdownload) = &self.current_download {
+            if cdownload.extractor_hash == next_video.extractor_hash {
+                // If the video is already being downloaded it will always fit. Otherwise the
+                // download would not have been started.
+                return Ok(CacheSizeCheck::Fits);
+            }
+        }
+        let cache_allocation = get_current_cache_allocation(app).await?;
+        let video_size = self.get_approx_video_size(next_video)?;
+
+        if video_size >= max_cache_size {
+            error!(
+                "The video '{}' ({}) exceeds the maximum cache size ({})! \
+                 Please set a bigger maximum (`--max-cache-size`) or skip it.",
+                next_video.title,
+                Bytes::new(video_size),
+                Bytes::new(max_cache_size)
+            );
+
+            return Ok(CacheSizeCheck::ExceedsMaxCacheSize);
+        }
+
+        if cache_allocation.as_u64() + video_size >= max_cache_size {
+            if !self.printed_warning {
+                warn!(
+                    "Can't download video: '{}' ({}) as it's too large for the cache ({} of {} allocated). \
+                     Waiting for cache size reduction..",
+                    next_video.title,
+                    Bytes::new(video_size),
+                    &cache_allocation,
+                    Bytes::new(max_cache_size)
+                );
+                self.printed_warning = true;
+
+                // Update this value immediately.
+                // This avoids printing the "Current cache size has changed .." warning below.
+                self.cached_cache_allocation = Some(cache_allocation);
+            }
+
+            if let Some(cca) = self.cached_cache_allocation {
+                if cca != cache_allocation {
+                    // Only print the warning if the display string has actually changed.
+                    // Otherwise, we might confuse the user
+                    if cca.to_string() != cache_allocation.to_string() {
+                        warn!("Current cache size has changed, it's now: '{cache_allocation}'");
+                    }
+                    debug!(
+                        "Cache size has changed: {} -> {}",
+                        cca.as_u64(),
+                        cache_allocation.as_u64()
+                    );
+                    self.cached_cache_allocation = Some(cache_allocation);
+                }
+            } else {
+                unreachable!(
+                    "The `printed_warning` should be false in this case, \
+                    and thus should have already set the `cached_cache_allocation`."
+                );
+            }
+
+            // Wait and hope, that a large video is deleted from the cache.
+            wait_for_cache_reduction(app).await?;
+            Ok(CacheSizeCheck::TooLarge)
+        } else {
+            self.printed_warning = false;
+            Ok(CacheSizeCheck::Fits)
+        }
+    }
+
+    /// The entry point to the Downloader.
+    /// This Downloader will periodically check if the database has changed, and then also
+    /// change which videos it downloads.
+    /// This will run, until the database doesn't contain any watchable videos
+    pub(crate) async fn consume(&mut self, app: Arc<App>, max_cache_size: u64) -> Result<()> {
+        while let Some(next_video) = Video::next_to_download(&app).await? {
+            match self
+                .is_enough_cache_available(&app, max_cache_size, &next_video)
+                .await?
+            {
+                CacheSizeCheck::Fits => (),
+                CacheSizeCheck::TooLarge => continue,
+                CacheSizeCheck::ExceedsMaxCacheSize => bail!("Giving up."),
+            }
+
+            if self.current_download.is_some() {
+                let current_download = self.current_download.take().expect("It is `Some`.");
+
+                if current_download.is_finished() {
+                    // The download is done, finalize it and leave it removed.
+                    current_download.finalize(&app).await?;
+                    continue;
+                }
+
+                if next_video.extractor_hash == current_download.extractor_hash {
+                    // We still want to download the same video.
+                    // reset the taken value
+                    self.current_download = Some(current_download);
+                } else {
+                    info!(
+                        "Noticed, that the next video is not the video being downloaded, replacing it ('{}' vs. '{}')!",
+                        next_video.extractor_hash.as_short_hash(&app).await?,
+                        current_download.extractor_hash.as_short_hash(&app).await?
+                    );
+
+                    // Replace the currently downloading video
+                    current_download
+                        .abort()
+                        .context("Failed to abort last download")?;
+
+                    let new_current_download = CurrentDownload::new_from_video(&app, next_video)?;
+
+                    self.current_download = Some(new_current_download);
+                }
+            } else {
+                info!(
+                    "No video is being downloaded right now, setting it to '{}'",
+                    next_video.title
+                );
+                let new_current_download = CurrentDownload::new_from_video(&app, next_video)?;
+                self.current_download = Some(new_current_download);
+            }
+
+            // We have to continuously check, if the current download is done.
+            // As such we simply wait or recheck on the next write to the db.
+            select! {
+                () = time::sleep(Duration::from_secs(1)) => (),
+                Ok(()) = wait_for_db_write(&app) => (),
+            }
+        }
+
+        info!("Finished downloading!");
+        Ok(())
+    }
+
+    fn get_approx_video_size(&mut self, video: &Video) -> Result<u64> {
+        if let Some(value) = self.video_size_cache.get(&video.extractor_hash) {
+            Ok(*value)
+        } else {
+            let size = video.get_approx_size()?;
+
+            assert_eq!(
+                self.video_size_cache.insert(video.extractor_hash, size),
+                None
+            );
+
+            Ok(size)
+        }
+    }
+}
diff --git a/crates/yt/src/commands/download/implm/download/progress_hook.rs b/crates/yt/src/commands/download/implm/download/progress_hook.rs
new file mode 100644
index 0000000..19fe122
--- /dev/null
+++ b/crates/yt/src/commands/download/implm/download/progress_hook.rs
@@ -0,0 +1,175 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    io::{Write, stderr},
+    process,
+    sync::atomic::Ordering,
+};
+
+use colors::{Colorize, IntoCanvas};
+use log::{Level, log_enabled};
+use yt_dlp::{json_cast, json_get, wrap_progress_hook};
+
+use crate::{
+    ansi_escape_codes::{clear_whole_line, move_to_col},
+    config::SHOULD_DISPLAY_COLOR,
+    select::duration::MaybeDuration,
+    shared::bytes::Bytes,
+};
+
+macro_rules! json_get_default {
+    ($value:expr, $name:literal, $convert:ident, $default:expr) => {
+        $value.get($name).map_or($default, |v| {
+            if v == &serde_json::Value::Null {
+                $default
+            } else {
+                json_cast!(@log_key $name, v, $convert)
+            }
+        })
+    };
+}
+
+fn format_bytes(bytes: u64) -> String {
+    let bytes = Bytes::new(bytes);
+    bytes.to_string()
+}
+
+fn format_speed(speed: f64) -> String {
+    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+    let bytes = Bytes::new(speed.floor() as u64);
+    format!("{bytes}/s")
+}
+
+/// # Panics
+/// If expectations fail.
+#[allow(clippy::needless_pass_by_value)]
+pub(crate) fn progress_hook(
+    input: serde_json::Map<String, serde_json::Value>,
+) -> Result<(), std::io::Error> {
+    // Only add the handler, if the log-level is higher than Debug (this avoids covering debug
+    // messages).
+    if log_enabled!(Level::Debug) {
+        return Ok(());
+    }
+
+    let info_dict = json_get!(input, "info_dict", as_object);
+
+    let get_title = || -> String {
+        match json_get!(info_dict, "ext", as_str) {
+            "vtt" => {
+                format!(
+                    "Subtitles ({})",
+                    json_get_default!(info_dict, "name", as_str, "<No Subtitle Language>")
+                )
+            }
+            "webm" | "mp4" | "mp3" | "m4a" => {
+                json_get_default!(info_dict, "title", as_str, "<No title>").to_owned()
+            }
+            other => panic!("The extension '{other}' is not yet implemented"),
+        }
+    };
+
+    match json_get!(input, "status", as_str) {
+        "downloading" => {
+            let elapsed = json_get_default!(input, "elapsed", as_f64, 0.0);
+            let eta = json_get_default!(input, "eta", as_f64, 0.0);
+            let speed = json_get_default!(input, "speed", as_f64, 0.0);
+
+            let downloaded_bytes = json_get!(input, "downloaded_bytes", as_u64);
+            let (total_bytes, bytes_is_estimate): (u64, &'static str) = {
+                let total_bytes = json_get_default!(input, "total_bytes", as_u64, 0);
+
+                if total_bytes == 0 {
+                    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+                    let maybe_estimate =
+                        json_get_default!(input, "total_bytes_estimate", as_f64, 0.0) as u64;
+
+                    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
+                    if maybe_estimate == 0 {
+                        // The download speed should be in bytes
+                        // per second and the eta in seconds.
+                        // Thus multiplying them gets us the raw bytes
+                        // (which were estimated by `yt_dlp`, from their `info.json`)
+                        let bytes_still_needed = (speed * eta).ceil() as u64;
+
+                        (downloaded_bytes + bytes_still_needed, "~")
+                    } else {
+                        (maybe_estimate, "~")
+                    }
+                } else {
+                    (total_bytes, "")
+                }
+            };
+
+            let percent: f64 = {
+                if total_bytes == 0 {
+                    100.0
+                } else {
+                    #[allow(
+                        clippy::cast_possible_truncation,
+                        clippy::cast_sign_loss,
+                        clippy::cast_precision_loss
+                    )]
+                    {
+                        (downloaded_bytes as f64 / total_bytes as f64) * 100.0
+                    }
+                }
+            };
+
+            clear_whole_line();
+            move_to_col(1);
+
+            let should_use_color = SHOULD_DISPLAY_COLOR.load(Ordering::Relaxed);
+
+            eprint!(
+                "{} [{}/{} at {}] -> [{} of {}{} {}] ",
+                get_title().bold().blue().render(should_use_color),
+                MaybeDuration::from_secs_f64(elapsed)
+                    .bold()
+                    .yellow()
+                    .render(should_use_color),
+                MaybeDuration::from_secs_f64(eta)
+                    .bold()
+                    .yellow()
+                    .render(should_use_color),
+                format_speed(speed).bold().green().render(should_use_color),
+                format_bytes(downloaded_bytes)
+                    .bold()
+                    .red()
+                    .render(should_use_color),
+                bytes_is_estimate.bold().red().render(should_use_color),
+                format_bytes(total_bytes)
+                    .bold()
+                    .red()
+                    .render(should_use_color),
+                format!("{percent:.02}%")
+                    .bold()
+                    .cyan()
+                    .render(should_use_color),
+            );
+            stderr().flush()?;
+        }
+        "finished" => {
+            eprintln!("-> Finished downloading.");
+        }
+        "error" => {
+            // TODO: This should probably return an Err. But I'm not so sure where the error would
+            // bubble up to (i.e., who would catch it) <2025-01-21>
+            eprintln!("-> Error while downloading: {}", get_title());
+            process::exit(1);
+        }
+        other => unreachable!("'{other}' should not be a valid state!"),
+    }
+
+    Ok(())
+}
+
+wrap_progress_hook!(progress_hook, wrapped_progress_hook);
diff --git a/crates/yt/src/commands/download/implm/mod.rs b/crates/yt/src/commands/download/implm/mod.rs
new file mode 100644
index 0000000..c74a909
--- /dev/null
+++ b/crates/yt/src/commands/download/implm/mod.rs
@@ -0,0 +1,55 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::sync::Arc;
+
+use crate::{
+    app::App,
+    commands::download::DownloadCommand,
+    shared::bytes::Bytes,
+    storage::db::{
+        insert::{Operations, maintenance::clear_stale_downloaded_paths},
+        video::{Video, VideoStatusMarker},
+    },
+};
+
+use anyhow::Result;
+use log::info;
+
+mod download;
+
+impl DownloadCommand {
+    pub(in crate::commands) async fn implm(self, app: Arc<App>) -> Result<()> {
+        let DownloadCommand {
+            force,
+            max_cache_size,
+        } = self;
+
+        let max_cache_size = max_cache_size.unwrap_or(app.config.download.max_cache_size.as_u64());
+        info!("Max cache size: '{}'", Bytes::new(max_cache_size));
+
+        clear_stale_downloaded_paths(&app).await?;
+        if force {
+            let mut all = Video::in_states(&app, &[VideoStatusMarker::Cached]).await?;
+
+            let mut ops = Operations::new("Download: Clear old download paths due to `--force`");
+            for a in &mut all {
+                a.remove_download_path(&mut ops);
+            }
+            ops.commit(&app).await?;
+        }
+
+        download::Downloader::new()
+            .consume(app, max_cache_size)
+            .await?;
+
+        Ok(())
+    }
+}
diff --git a/crates/yt/src/commands/download/mod.rs b/crates/yt/src/commands/download/mod.rs
new file mode 100644
index 0000000..15026ba
--- /dev/null
+++ b/crates/yt/src/commands/download/mod.rs
@@ -0,0 +1,34 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use anyhow::Context;
+use clap::Parser;
+
+use crate::shared::bytes::Bytes;
+
+mod implm;
+
+#[derive(Parser, Debug)]
+pub(super) struct DownloadCommand {
+    /// Forcefully re-download all cached videos (i.e. delete all already downloaded paths, then download).
+    #[arg(short, long)]
+    force: bool,
+
+    /// The maximum size the download dir should have.
+    #[arg(short, long, value_parser = byte_parser)]
+    max_cache_size: Option<u64>,
+}
+
+fn byte_parser(input: &str) -> Result<u64, anyhow::Error> {
+    Ok(input
+        .parse::<Bytes>()
+        .with_context(|| format!("Failed to parse '{input}' as bytes!"))?
+        .as_u64())
+}
diff --git a/crates/yt/src/commands/implm.rs b/crates/yt/src/commands/implm.rs
new file mode 100644
index 0000000..7c60c6a
--- /dev/null
+++ b/crates/yt/src/commands/implm.rs
@@ -0,0 +1,38 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::sync::Arc;
+
+use crate::commands::Command;
+
+use anyhow::Result;
+
+impl Command {
+    pub(crate) async fn implm(self, app: crate::app::App) -> Result<()> {
+        match self {
+            Command::Cache { cmd } => cmd.implm(&app).await?,
+            Command::Config { cmd } => cmd.implm(&app)?,
+            Command::Database { cmd } => cmd.implm(&app).await?,
+            Command::Download { cmd } => cmd.implm(Arc::new(app)).await?,
+            Command::Playlist { cmd } => cmd.implm(&app).await?,
+            Command::Select { cmd } => {
+                cmd.unwrap_or_default().implm(&app).await?;
+            }
+            Command::Show { cmd } => cmd.implm(&app).await?,
+            Command::Status { cmd } => cmd.implm(&app).await?,
+            Command::Subscriptions { cmd } => cmd.implm(&app).await?,
+            Command::Update { cmd } => cmd.implm(&app).await?,
+            Command::Videos { cmd } => cmd.implm(&app).await?,
+            Command::Watch { cmd } => cmd.implm(Arc::new(app)).await?,
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/yt/src/commands/mod.rs b/crates/yt/src/commands/mod.rs
new file mode 100644
index 0000000..431acef
--- /dev/null
+++ b/crates/yt/src/commands/mod.rs
@@ -0,0 +1,164 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{ffi::OsStr, thread};
+
+use clap::Subcommand;
+use clap_complete::CompletionCandidate;
+use tokio::runtime::Runtime;
+
+use crate::{
+    app::App,
+    commands::{
+        cache::CacheCommand, config::ConfigCommand, database::DatabaseCommand,
+        download::DownloadCommand, playlist::PlaylistCommand, select::SelectCommand,
+        show::ShowCommand, status::StatusCommand, subscriptions::SubscriptionCommand,
+        update::UpdateCommand, videos::VideosCommand, watch::WatchCommand,
+    },
+    config::Config,
+    storage::db::subscription::Subscriptions,
+};
+
+pub(super) mod implm;
+
+mod cache;
+mod config;
+mod database;
+mod download;
+mod playlist;
+mod select;
+mod show;
+mod status;
+mod subscriptions;
+mod update;
+mod videos;
+mod watch;
+
+#[derive(Subcommand, Debug)]
+#[allow(private_interfaces)] // Only the main `implm` method should be accessible.
+pub(super) enum Command {
+    /// Manipulate the download cache
+    Cache {
+        #[command(subcommand)]
+        cmd: CacheCommand,
+    },
+
+    /// Show, the configuration options in effect.
+    Config {
+        #[command(flatten)]
+        cmd: ConfigCommand,
+    },
+
+    /// Interact with the video database.
+    #[command(visible_alias = "db")]
+    Database {
+        #[command(subcommand)]
+        cmd: DatabaseCommand,
+    },
+
+    /// Download and cache URLs
+    Download {
+        #[command(flatten)]
+        cmd: DownloadCommand,
+    },
+
+    /// Visualize the current playlist
+    Playlist {
+        #[command(flatten)]
+        cmd: PlaylistCommand,
+    },
+
+    /// Change the state of videos in the database (the default)
+    Select {
+        #[command(subcommand)]
+        cmd: Option<SelectCommand>,
+    },
+
+    /// Show things about the currently playing video.
+    Show {
+        #[command(subcommand)]
+        cmd: ShowCommand,
+    },
+
+    /// Show, which videos have been selected to be watched (and their cache status)
+    Status {
+        #[command(flatten)]
+        cmd: StatusCommand,
+    },
+
+    /// Manipulate subscription
+    #[command(visible_alias = "subs")]
+    Subscriptions {
+        #[command(subcommand)]
+        cmd: SubscriptionCommand,
+    },
+
+    /// Update the video database
+    Update {
+        #[command(flatten)]
+        cmd: UpdateCommand,
+    },
+
+    /// Work with single videos
+    Videos {
+        #[command(subcommand)]
+        cmd: VideosCommand,
+    },
+
+    /// Watch the already cached (and selected) videos
+    Watch {
+        #[command(flatten)]
+        cmd: WatchCommand,
+    },
+}
+
+impl Default for Command {
+    fn default() -> Self {
+        Self::Select {
+            cmd: Some(SelectCommand::default()),
+        }
+    }
+}
+
+fn complete_subscription(current: &OsStr) -> Vec<CompletionCandidate> {
+    let mut output = vec![];
+
+    let Some(current_prog) = current.to_str().map(ToOwned::to_owned) else {
+        return output;
+    };
+
+    let Ok(config) = Config::from_config_file(None, None, None) else {
+        return output;
+    };
+
+    let handle = thread::spawn(move || {
+        let Ok(rt) = Runtime::new() else {
+            return output;
+        };
+
+        let Ok(app) = rt.block_on(App::new(config, false)) else {
+            return output;
+        };
+
+        let Ok(all) = rt.block_on(Subscriptions::get(&app)) else {
+            return output;
+        };
+
+        for sub in all.0.into_keys() {
+            if sub.starts_with(&current_prog) {
+                output.push(CompletionCandidate::new(sub));
+            }
+        }
+
+        output
+    });
+
+    handle.join().unwrap_or_default()
+}
diff --git a/crates/yt/src/commands/playlist/implm.rs b/crates/yt/src/commands/playlist/implm.rs
new file mode 100644
index 0000000..603184b
--- /dev/null
+++ b/crates/yt/src/commands/playlist/implm.rs
@@ -0,0 +1,110 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{fmt::Write, path::Path};
+
+use crate::{
+    ansi_escape_codes,
+    app::App,
+    commands::playlist::PlaylistCommand,
+    storage::{
+        db::{
+            playlist::Playlist,
+            video::{Video, VideoStatus},
+        },
+        notify::wait_for_db_write,
+    },
+    videos::RenderWithApp,
+};
+
+use anyhow::Result;
+use futures::{TryStreamExt, stream::FuturesOrdered};
+
+impl PlaylistCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        let PlaylistCommand { watch } = self;
+
+        let mut previous_output_length = 0;
+        loop {
+            let playlist = Playlist::create(app).await?.videos;
+
+            let output = playlist
+                .into_iter()
+                .map(|video| async move {
+                    let mut output = String::new();
+
+                    let (_, is_focused) = cache_values(&video);
+
+                    if is_focused {
+                        output.push_str("🔻 ");
+                    } else {
+                        output.push_str("  ");
+                    }
+
+                    output.push_str(&video.title_fmt().to_string(app));
+
+                    output.push_str(" (");
+                    output.push_str(&video.parent_subscription_name_fmt().to_string(app));
+                    output.push(')');
+
+                    output.push_str(" [");
+                    output.push_str(&video.duration_fmt().to_string(app));
+
+                    if is_focused {
+                        output.push_str(" (");
+                        if let Some(percent) = video.watch_progress_percent_fmt() {
+                            write!(output, "{}", percent.to_string(app))?;
+                        } else {
+                            write!(output, "{}", video.watch_progress_fmt().to_string(app))?;
+                        }
+
+                        output.push(')');
+                    }
+                    output.push(']');
+
+                    output.push('\n');
+
+                    Ok::<String, anyhow::Error>(output)
+                })
+                .collect::<FuturesOrdered<_>>()
+                .try_collect::<String>()
+                .await?;
+
+            // Delete the previous output
+            ansi_escape_codes::cursor_up(previous_output_length);
+            ansi_escape_codes::erase_from_cursor_to_bottom();
+
+            previous_output_length = output.chars().filter(|ch| *ch == '\n').count();
+
+            print!("{output}");
+
+            if !watch {
+                break;
+            }
+
+            wait_for_db_write(app).await?;
+        }
+
+        Ok(())
+    }
+}
+
+/// Extract the values of the [`VideoStatus::Cached`] value from a Video.
+fn cache_values(video: &Video) -> (&Path, bool) {
+    if let VideoStatus::Cached {
+        cache_path,
+        is_focused,
+    } = &video.status
+    {
+        (cache_path, *is_focused)
+    } else {
+        unreachable!("All of these videos should be cached");
+    }
+}
diff --git a/crates/yt/src/commands/playlist/mod.rs b/crates/yt/src/commands/playlist/mod.rs
new file mode 100644
index 0000000..8d3407d
--- /dev/null
+++ b/crates/yt/src/commands/playlist/mod.rs
@@ -0,0 +1,20 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::Parser;
+
+mod implm;
+
+#[derive(Parser, Debug)]
+pub(super) struct PlaylistCommand {
+    /// Linger and display changes
+    #[arg(short, long)]
+    watch: bool,
+}
diff --git a/yt/src/select/selection_file/help.str b/crates/yt/src/commands/select/implm/fs_generators/help.str
index e3cc347..e3cc347 100644
--- a/yt/src/select/selection_file/help.str
+++ b/crates/yt/src/commands/select/implm/fs_generators/help.str
diff --git a/yt/src/select/selection_file/help.str.license b/crates/yt/src/commands/select/implm/fs_generators/help.str.license
index a0e196c..a0e196c 100644
--- a/yt/src/select/selection_file/help.str.license
+++ b/crates/yt/src/commands/select/implm/fs_generators/help.str.license
diff --git a/crates/yt/src/commands/select/implm/fs_generators/mod.rs b/crates/yt/src/commands/select/implm/fs_generators/mod.rs
new file mode 100644
index 0000000..10da032
--- /dev/null
+++ b/crates/yt/src/commands/select/implm/fs_generators/mod.rs
@@ -0,0 +1,355 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    collections::HashMap,
+    env,
+    fs::{self, File, OpenOptions},
+    io::{BufRead, BufReader, BufWriter, Read, Write},
+    iter,
+    os::fd::{AsFd, AsRawFd},
+    path::Path,
+};
+
+use crate::{
+    app::App,
+    cli::CliArgs,
+    commands::{
+        Command,
+        select::{
+            SelectCommand, SelectSplitSortKey, SelectSplitSortMode,
+            implm::standalone::{self, handle_select_cmd},
+        },
+    },
+    storage::db::{
+        extractor_hash::ExtractorHash,
+        insert::Operations,
+        video::{Video, VideoStatusMarker},
+    },
+};
+
+use anyhow::{Context, Result, bail};
+use clap::Parser;
+use futures::{TryStreamExt, stream::FuturesOrdered};
+use log::info;
+use shlex::Shlex;
+use tokio::process;
+
+const HELP_STR: &str = include_str!("./help.str");
+
+pub(crate) async fn select_split(
+    app: &App,
+    done: bool,
+    sort_key: SelectSplitSortKey,
+    sort_mode: SelectSplitSortMode,
+) -> Result<()> {
+    let temp_dir = tempfile::Builder::new()
+        .prefix("yt_video_select-")
+        .rand_bytes(6)
+        .tempdir()
+        .context("Failed to get tempdir")?;
+
+    let matching_videos = get_videos(app, done).await?;
+
+    let mut no_author = vec![];
+    let mut author_map = HashMap::new();
+    for video in matching_videos {
+        if let Some(sub) = &video.parent_subscription_name {
+            if author_map.contains_key(sub) {
+                let vec: &mut Vec<_> = author_map
+                    .get_mut(sub)
+                    .expect("This key is set, we checked in the if above");
+
+                vec.push(video);
+            } else {
+                author_map.insert(sub.to_owned(), vec![video]);
+            }
+        } else {
+            no_author.push(video);
+        }
+    }
+
+    let author_map = {
+        let mut temp_vec: Vec<_> = author_map.into_iter().collect();
+
+        match sort_key {
+            SelectSplitSortKey::Publisher => {
+                // PERFORMANCE: The clone here should not be neeed.  <2025-06-15>
+                temp_vec.sort_by_key(|(name, _): &(String, Vec<Video>)| name.to_owned());
+            }
+            SelectSplitSortKey::Videos => {
+                temp_vec.sort_by_key(|(_, videos): &(String, Vec<Video>)| videos.len());
+            }
+        }
+
+        match sort_mode {
+            SelectSplitSortMode::Asc => {
+                // Std's default mode is ascending.
+            }
+            SelectSplitSortMode::Desc => {
+                temp_vec.reverse();
+            }
+        }
+
+        temp_vec
+    };
+
+    for (index, (name, videos)) in author_map
+        .into_iter()
+        .chain(iter::once((
+            "<No parent subscription>".to_owned(),
+            no_author,
+        )))
+        .enumerate()
+    {
+        let mut file_path = temp_dir.path().join(format!("{index:02}_{name}"));
+        file_path.set_extension("yts");
+
+        let tmp_file = File::create(&file_path)
+            .with_context(|| format!("Falied to create file at: {}", file_path.display()))?;
+
+        write_videos_to_file(app, &tmp_file, &videos)
+            .await
+            .with_context(|| format!("Falied to populate file at: {}", file_path.display()))?;
+    }
+
+    open_editor_at(temp_dir.path()).await?;
+
+    let mut paths = vec![];
+    for maybe_entry in temp_dir
+        .path()
+        .read_dir()
+        .context("Failed to open temp dir for reading")?
+    {
+        let entry = maybe_entry.context("Failed to read entry in temp dir")?;
+
+        if !entry.file_type()?.is_file() {
+            bail!("Found non-file entry: {}", entry.path().display());
+        }
+
+        paths.push(entry.path());
+    }
+
+    paths.sort();
+
+    let mut persistent_file = OpenOptions::new()
+        .read(false)
+        .write(true)
+        .create(true)
+        .truncate(true)
+        .open(&app.config.paths.last_selection_path)
+        .context("Failed to open persistent selection file")?;
+
+    for path in paths {
+        let mut read_file = File::open(path)?;
+
+        let mut buffer = vec![];
+        read_file.read_to_end(&mut buffer)?;
+        persistent_file.write_all(&buffer)?;
+    }
+
+    persistent_file.flush()?;
+    let persistent_file = OpenOptions::new()
+        .read(true)
+        .open(format!(
+            "/proc/self/fd/{}",
+            persistent_file.as_fd().as_raw_fd()
+        ))
+        .context("Failed to re-open persistent file")?;
+
+    let processed = process_file(app, &persistent_file).await?;
+
+    info!("Processed {processed} records.");
+    temp_dir.close().context("Failed to close the temp dir")?;
+    Ok(())
+}
+
+pub(crate) async fn select_file(app: &App, done: bool, use_last_selection: bool) -> Result<()> {
+    let temp_file = tempfile::Builder::new()
+        .prefix("yt_video_select-")
+        .suffix(".yts")
+        .rand_bytes(6)
+        .tempfile()
+        .context("Failed to get tempfile")?;
+
+    if use_last_selection {
+        fs::copy(&app.config.paths.last_selection_path, &temp_file)?;
+    } else {
+        let matching_videos = get_videos(app, done).await?;
+
+        write_videos_to_file(app, temp_file.as_file(), &matching_videos).await?;
+    }
+
+    open_editor_at(temp_file.path()).await?;
+
+    let read_file = OpenOptions::new().read(true).open(temp_file.path())?;
+    fs::copy(temp_file.path(), &app.config.paths.last_selection_path)
+        .context("Failed to persist selection file")?;
+
+    let processed = process_file(app, &read_file).await?;
+    info!("Processed {processed} records.");
+
+    Ok(())
+}
+
+async fn get_videos(app: &App, include_done: bool) -> Result<Vec<Video>> {
+    if include_done {
+        Video::in_states(app, VideoStatusMarker::ALL).await
+    } else {
+        Video::in_states(
+            app,
+            &[
+                VideoStatusMarker::Pick,
+                //
+                VideoStatusMarker::Watch,
+                VideoStatusMarker::Cached,
+            ],
+        )
+        .await
+    }
+}
+
+async fn write_videos_to_file(app: &App, file: &File, videos: &[Video]) -> Result<()> {
+    // Warm-up the cache for the display rendering of the videos.
+    // Otherwise the futures would all try to warm it up at the same time.
+    if let Some(vid) = videos.first() {
+        drop(vid.to_line_display(app, None).await?);
+    }
+
+    let mut edit_file = BufWriter::new(file);
+
+    videos
+        .iter()
+        .map(|vid| vid.to_select_file_display(app))
+        .collect::<FuturesOrdered<_>>()
+        .try_collect::<Vec<String>>()
+        .await?
+        .into_iter()
+        .try_for_each(|line| -> Result<()> {
+            edit_file
+                .write_all(line.as_bytes())
+                .context("Failed to write to `edit_file`")?;
+
+            Ok(())
+        })?;
+
+    edit_file.write_all(HELP_STR.as_bytes())?;
+    edit_file.flush().context("Failed to flush edit file")?;
+
+    Ok(())
+}
+
+async fn process_file(app: &App, file: &File) -> Result<i64> {
+    let mut line_number = 0;
+
+    let mut ops = Operations::new("Select: process file");
+
+    // Fetch all the hashes once, instead of every time we need to process a line.
+    let all_hashes = ExtractorHash::get_all(app).await?;
+
+    let reader = BufReader::new(file);
+    for line in reader.lines() {
+        let line = line.context("Failed to read a line")?;
+
+        if let Some(line) = process_line(&line)? {
+            line_number -= 1;
+
+            // debug!(
+            //     "Parsed command: `{}`",
+            //     line.iter()
+            //         .map(|val| format!("\"{}\"", val))
+            //         .collect::<Vec<String>>()
+            //         .join(" ")
+            // );
+
+            let arg_line = ["yt", "select"]
+                .into_iter()
+                .chain(line.iter().map(String::as_str));
+
+            let args = CliArgs::parse_from(arg_line);
+
+            let Command::Select { cmd } = args
+                .command
+                .expect("This will be some, as we constructed it above.")
+            else {
+                unreachable!("This is checked in the `filter_line` function")
+            };
+
+            match cmd.expect(
+                "This value should always be some \
+                    here, as it would otherwise thrown an error above.",
+            ) {
+                SelectCommand::File { .. } | SelectCommand::Split { .. } => {
+                    bail!("You cannot use `select file` or `select split` recursively.")
+                }
+                SelectCommand::Add { urls, start, stop } => {
+                    Box::pin(standalone::add::add(app, urls, start, stop)).await?;
+                }
+                other => {
+                    let shared = other
+                        .clone()
+                        .into_shared()
+                        .expect("The ones without shared should have been filtered out.");
+
+                    let hash = shared.hash.realize(app, Some(&all_hashes)).await?;
+                    let mut video = hash
+                        .get_with_app(app)
+                        .await
+                        .expect("The hash was already realized, it should therefore exist");
+
+                    handle_select_cmd(app, other, &mut video, Some(line_number), &mut ops).await?;
+                }
+            }
+        }
+    }
+
+    ops.commit(app).await?;
+    Ok(-line_number)
+}
+
+async fn open_editor_at(path: &Path) -> Result<()> {
+    let editor = env::var("EDITOR").unwrap_or("nvim".to_owned());
+
+    let mut nvim = process::Command::new(&editor);
+    nvim.arg(path);
+    let status = nvim
+        .status()
+        .await
+        .with_context(|| format!("Falied to run editor: {editor}"))?;
+
+    if status.success() {
+        Ok(())
+    } else {
+        bail!("Editor ({editor}) exited with error status: {}", status)
+    }
+}
+
+fn process_line(line: &str) -> Result<Option<Vec<String>>> {
+    // Filter out comments and empty lines
+    if line.starts_with('#') || line.trim().is_empty() {
+        Ok(None)
+    } else {
+        let split: Vec<_> = {
+            let mut shl = Shlex::new(line);
+            let res = shl.by_ref().collect();
+
+            if shl.had_error {
+                bail!("Failed to parse line '{line}'")
+            }
+
+            assert_eq!(shl.line_no, 1, "A unexpected newline appeared");
+            res
+        };
+
+        assert!(!split.is_empty());
+
+        Ok(Some(split))
+    }
+}
diff --git a/crates/yt/src/commands/select/implm/mod.rs b/crates/yt/src/commands/select/implm/mod.rs
new file mode 100644
index 0000000..f39c77f
--- /dev/null
+++ b/crates/yt/src/commands/select/implm/mod.rs
@@ -0,0 +1,52 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{app::App, commands::select::SelectCommand, storage::db::insert::Operations};
+
+use anyhow::Result;
+
+mod fs_generators;
+mod standalone;
+
+impl SelectCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        match self {
+            SelectCommand::File {
+                done,
+                use_last_selection,
+            } => Box::pin(fs_generators::select_file(app, done, use_last_selection)).await?,
+            SelectCommand::Split {
+                done,
+                sort_key,
+                sort_mode,
+            } => Box::pin(fs_generators::select_split(app, done, sort_key, sort_mode)).await?,
+            SelectCommand::Add { urls, start, stop } => {
+                Box::pin(standalone::add::add(app, urls, start, stop)).await?;
+            }
+            other => {
+                let shared = other
+                    .clone()
+                    .into_shared()
+                    .expect("The ones without shared should have been filtered out.");
+                let hash = shared.hash.realize(app, None).await?;
+                let mut video = hash
+                    .get_with_app(app)
+                    .await
+                    .expect("The hash was already realized, it should therefore exist");
+
+                let mut ops = Operations::new("Main: handle select cmd");
+                standalone::handle_select_cmd(app, other, &mut video, None, &mut ops).await?;
+                ops.commit(app).await?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/yt/src/select/cmds/add.rs b/crates/yt/src/commands/select/implm/standalone/add.rs
index da58ec2..dd11cb4 100644
--- a/yt/src/select/cmds/add.rs
+++ b/crates/yt/src/commands/select/implm/standalone/add.rs
@@ -10,39 +10,28 @@
 
 use crate::{
     app::App,
-    download::download_options::download_opts,
-    storage::video_database::{
-        self, extractor_hash::ExtractorHash, get::get_all_hashes, set::add_video,
-    },
-    unreachable::Unreachable,
-    update::video_entry_to_video,
+    storage::db::{extractor_hash::ExtractorHash, insert::Operations, video::Video},
+    yt_dlp::yt_dlp_opts_updating,
 };
 
 use anyhow::{Context, Result, bail};
 use log::{error, warn};
-use serde_json::{Map, Value};
 use url::Url;
-use yt_dlp::wrapper::info_json::InfoType;
+use yt_dlp::{YoutubeDL, info_json::InfoJson, json_cast, json_get, json_try_get};
 
 #[allow(clippy::too_many_lines)]
-pub(super) async fn add(
+pub(crate) async fn add(
     app: &App,
     urls: Vec<Url>,
     start: Option<usize>,
     stop: Option<usize>,
 ) -> Result<()> {
     for url in urls {
-        async fn process_and_add(
-            app: &App,
-            entry: yt_dlp::wrapper::info_json::InfoJson,
-            opts: &Map<String, Value>,
-        ) -> Result<()> {
-            let url = entry
-                .url
-                .unreachable("`yt_dlp` should guarantee that this is Some at this point");
-
-            let entry = yt_dlp::extract_info(opts, &url, false, true)
-                .await
+        async fn process_and_add(app: &App, entry: InfoJson, yt_dlp: &YoutubeDL) -> Result<()> {
+            let url = json_get!(entry, "url", as_str).parse()?;
+
+            let entry = yt_dlp
+                .extract_info(&url, false, true)
                 .with_context(|| format!("Failed to fetch entry for url: '{url}'"))?;
 
             add_entry(app, entry).await?;
@@ -50,56 +39,47 @@ pub(super) async fn add(
             Ok(())
         }
 
-        async fn add_entry(app: &App, entry: yt_dlp::wrapper::info_json::InfoJson) -> Result<()> {
+        async fn add_entry(app: &App, entry: InfoJson) -> Result<()> {
             // We have to re-fetch all hashes every time, because a user could try to add the same
             // URL twice (for whatever reason.)
-            let hashes = get_all_hashes(app)
+            let hashes = ExtractorHash::get_all(app)
                 .await
                 .context("Failed to fetch all video hashes")?;
-            let extractor_hash = blake3::hash(
-                entry
-                    .id
-                    .as_ref()
-                    .expect("This should be some at this point")
-                    .as_bytes(),
-            );
+
+            let extractor_hash = ExtractorHash::from_info_json(&entry);
             if hashes.contains(&extractor_hash) {
                 error!(
                     "Video '{}'{} is already in the database. Skipped adding it",
-                    ExtractorHash::from_hash(extractor_hash)
-                        .into_short_hash(app)
+                    extractor_hash
+                        .as_short_hash(app)
                         .await
                         .with_context(|| format!(
                             "Failed to format hash of video '{}' as short hash",
-                            entry
-                                .url
-                                .map_or("<Unknown video Url>".to_owned(), |url| url.to_string())
+                            json_try_get!(entry, "url", as_str).unwrap_or("<Unknown video Url>")
                         ))?,
-                    entry
-                        .title
-                        .map_or(String::new(), |title| format!(" ('{title}')"))
+                    json_try_get!(entry, "title", as_str)
+                        .map_or(String::new(), |title| format!(" (\"{title}\")"))
                 );
                 return Ok(());
             }
 
-            let video = video_entry_to_video(entry, None)?;
-            add_video(app, video.clone()).await?;
+            let mut ops = Operations::new("SelectAdd: Video entry to video");
+            let video = Video::from_info_json(&entry, None)?.add(&mut ops)?;
+            ops.commit(app).await?;
 
-            println!("{}", &video.to_line_display(app).await?);
+            println!("{}", &video.to_line_display(app, None).await?);
 
             Ok(())
         }
 
-        let opts = download_opts(app, &video_database::YtDlpOptions {
-            subtitle_langs: String::new(),
-        });
+        let yt_dlp = yt_dlp_opts_updating(start.unwrap_or(0) + stop.unwrap_or(0))?;
 
-        let entry = yt_dlp::extract_info(&opts, &url, false, true)
-            .await
+        let entry = yt_dlp
+            .extract_info(&url, false, true)
             .with_context(|| format!("Failed to fetch entry for url: '{url}'"))?;
 
-        match entry._type {
-            Some(InfoType::Video) => {
+        match json_try_get!(entry, "_type", as_str) {
+            Some("video") => {
                 add_entry(app, entry).await?;
                 if start.is_some() || stop.is_some() {
                     warn!(
@@ -107,13 +87,13 @@ pub(super) async fn add(
                     );
                 }
             }
-            Some(InfoType::Playlist) => {
-                if let Some(entries) = entry.entries {
+            Some("playlist") => {
+                if let Some(entries) = json_try_get!(entry, "entries", as_array) {
                     let start = start.unwrap_or(0);
                     let stop = stop.unwrap_or(entries.len() - 1);
 
-                    let mut respected_entries: Vec<_> = take_vector(entries, start, stop)
-                        .with_context(|| {
+                    let respected_entries =
+                        take_vector(entries, start, stop).with_context(|| {
                             format!(
                                 "Failed to take entries starting at: {start} and ending with {stop}"
                             )
@@ -123,11 +103,23 @@ pub(super) async fn add(
                         warn!("No entries found, after applying your start/stop limits.");
                     } else {
                         // Pre-warm the cache
-                        process_and_add(app, respected_entries.remove(0), &opts).await?;
+                        process_and_add(
+                            app,
+                            json_cast!(respected_entries[0], as_object).to_owned(),
+                            &yt_dlp,
+                        )
+                        .await?;
+                        let respected_entries = &respected_entries[1..];
 
                         let futures: Vec<_> = respected_entries
-                            .into_iter()
-                            .map(|entry| process_and_add(app, entry, &opts))
+                            .iter()
+                            .map(|entry| {
+                                process_and_add(
+                                    app,
+                                    json_cast!(entry, as_object).to_owned(),
+                                    &yt_dlp,
+                                )
+                            })
                             .collect();
 
                         for fut in futures {
@@ -148,7 +140,7 @@ pub(super) async fn add(
     Ok(())
 }
 
-fn take_vector<T>(vector: Vec<T>, start: usize, stop: usize) -> Result<Vec<T>> {
+fn take_vector<T>(vector: &[T], start: usize, stop: usize) -> Result<&[T]> {
     let length = vector.len();
 
     if stop >= length {
@@ -157,37 +149,18 @@ fn take_vector<T>(vector: Vec<T>, start: usize, stop: usize) -> Result<Vec<T>> {
         );
     }
 
-    let end_skip = {
-        let base = length
-            .checked_sub(stop)
-            .unreachable("The check above should have caught this case.");
-
-        base.checked_sub(1)
-            .unreachable("The check above should have caught this case.")
-    };
-
-    // NOTE: We're using this instead of the `vector[start..=stop]` notation, because I wanted to
-    // avoid the needed allocation to turn the slice into a vector. <2025-01-04>
-
-    // TODO: This function could also just return a slice, but oh well.. <2025-01-04>
-    Ok(vector
-        .into_iter()
-        .skip(start)
-        .rev()
-        .skip(end_skip)
-        .rev()
-        .collect())
+    Ok(&vector[start..=stop])
 }
 
 #[cfg(test)]
 mod test {
-    use crate::select::cmds::add::take_vector;
+    use super::take_vector;
 
     #[test]
     fn test_vector_take() {
         let vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
-        let new_vec = take_vector(vec, 2, 8).unwrap();
+        let new_vec = take_vector(&vec, 2, 8).unwrap();
 
         assert_eq!(new_vec, vec![2, 3, 4, 5, 6, 7, 8]);
     }
@@ -196,13 +169,13 @@ mod test {
     fn test_vector_take_overflow() {
         let vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
-        assert!(take_vector(vec, 0, 12).is_err());
+        assert!(take_vector(&vec, 0, 12).is_err());
     }
 
     #[test]
     fn test_vector_take_equal() {
         let vec = vec![0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
 
-        assert!(take_vector(vec, 0, 11).is_err());
+        assert!(take_vector(&vec, 0, 11).is_err());
     }
 }
diff --git a/crates/yt/src/commands/select/implm/standalone/mod.rs b/crates/yt/src/commands/select/implm/standalone/mod.rs
new file mode 100644
index 0000000..9512e32
--- /dev/null
+++ b/crates/yt/src/commands/select/implm/standalone/mod.rs
@@ -0,0 +1,132 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::io::{Write, stderr};
+
+use crate::{
+    ansi_escape_codes,
+    app::App,
+    commands::select::{SelectCommand, SharedSelectionCommandArgs},
+    storage::db::{
+        insert::{Operations, video::Operation},
+        video::{Priority, Video, VideoStatus},
+    },
+};
+
+use anyhow::{Context, Result, bail};
+
+pub(super) mod add;
+
+pub(crate) async fn handle_select_cmd(
+    app: &App,
+    cmd: SelectCommand,
+    video: &mut Video,
+    line_number: Option<i64>,
+    ops: &mut Operations<Operation>,
+) -> Result<()> {
+    let status = match cmd {
+        SelectCommand::Pick { shared } => Some((VideoStatus::Pick, shared)),
+        SelectCommand::Drop { shared } => Some((VideoStatus::Drop, shared)),
+        SelectCommand::Watched { shared } => Some((VideoStatus::Watched, shared)),
+        SelectCommand::Watch { shared } => {
+            if let VideoStatus::Cached {
+                cache_path,
+                is_focused,
+            } = &video.status
+            {
+                Some((
+                    VideoStatus::Cached {
+                        cache_path: cache_path.to_owned(),
+                        is_focused: *is_focused,
+                    },
+                    shared,
+                ))
+            } else {
+                Some((VideoStatus::Watch, shared))
+            }
+        }
+        SelectCommand::Url { shared } => {
+            let Some(url) = shared.url else {
+                bail!("You need to provide a url to `select url ..`")
+            };
+
+            let mut firefox = std::process::Command::new(app.config.commands.url_opener.first());
+            firefox.args(app.config.commands.url_opener.tail());
+            firefox.arg(url.as_str());
+            let _handle = firefox.spawn().context("Failed to run firefox")?;
+            None
+        }
+        SelectCommand::File { .. } | SelectCommand::Split { .. } | SelectCommand::Add { .. } => {
+            unreachable!("These should have been filtered out")
+        }
+    };
+
+    if let Some((status, shared)) = status {
+        handle_status_change(
+            app,
+            video,
+            shared,
+            line_number,
+            status,
+            line_number.is_none(),
+            ops,
+        )
+        .await?;
+    }
+
+    Ok(())
+}
+
+async fn handle_status_change(
+    app: &App,
+    video: &mut Video,
+    shared: SharedSelectionCommandArgs,
+    line_number: Option<i64>,
+    new_status: VideoStatus,
+    is_single: bool,
+    ops: &mut Operations<Operation>,
+) -> Result<()> {
+    let priority = compute_priority(line_number, shared.priority);
+
+    video.set_status(new_status, ops);
+    if let Some(priority) = priority {
+        video.set_priority(priority, ops);
+    }
+
+    if let Some(subtitle_langs) = shared.subtitle_langs {
+        video.set_subtitle_langs(subtitle_langs, ops);
+    }
+    if let Some(playback_speed) = shared.playback_speed {
+        video.set_playback_speed(playback_speed, ops);
+    }
+
+    if !is_single {
+        ansi_escape_codes::clear_whole_line();
+        ansi_escape_codes::move_to_col(1);
+    }
+
+    eprint!("{}", &video.to_line_display(app, None).await?);
+
+    if is_single {
+        eprintln!();
+    } else {
+        stderr().flush()?;
+    }
+
+    Ok(())
+}
+
+fn compute_priority(line_number: Option<i64>, priority: Option<i64>) -> Option<Priority> {
+    if let Some(pri) = priority {
+        Some(Priority::from(pri))
+    } else {
+        line_number.map(Priority::from)
+    }
+}
diff --git a/crates/yt/src/commands/select/mod.rs b/crates/yt/src/commands/select/mod.rs
new file mode 100644
index 0000000..db69238
--- /dev/null
+++ b/crates/yt/src/commands/select/mod.rs
@@ -0,0 +1,230 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    fmt::{self, Display, Formatter},
+    str::FromStr,
+};
+
+use chrono::NaiveDate;
+use clap::{Args, Subcommand, ValueEnum};
+use url::Url;
+
+use crate::{select::duration::MaybeDuration, storage::db::extractor_hash::LazyExtractorHash};
+
+mod implm;
+
+#[derive(Subcommand, Clone, Debug)]
+// NOTE: Keep this in sync with the [`constants::HELP_STR`] constant. <2024-08-20>
+// NOTE: Also keep this in sync with the `tree-sitter-yts/grammar.js`. <2024-11-04>
+#[allow(private_interfaces)] // Only the main `implm` method should be accessible.
+pub(super) enum SelectCommand {
+    /// Open a `git rebase` like file to select the videos to watch (the default)
+    File {
+        /// Include done (watched, dropped) videos
+        #[arg(long, short)]
+        done: bool,
+
+        /// Use the last selection file (useful if you've spend time on it and want to get it again)
+        #[arg(long, short, conflicts_with = "done")]
+        use_last_selection: bool,
+    },
+
+    /// Generate a directory, where each file contains only one subscription.
+    Split {
+        /// Include done (watched, dropped) videos
+        #[arg(long, short)]
+        done: bool,
+
+        /// Which key to use for sorting.
+        #[arg(default_value_t)]
+        sort_key: SelectSplitSortKey,
+
+        /// Which mode to use for sorting.
+        #[arg(default_value_t)]
+        sort_mode: SelectSplitSortMode,
+    },
+
+    /// Add a video to the database
+    ///
+    /// This optionally supports to add a playlist.
+    /// When a playlist is added, the `start` and `stop` arguments can be used to select which
+    /// playlist entries to include.
+    #[command(visible_alias = "a")]
+    Add {
+        urls: Vec<Url>,
+
+        /// Start adding playlist entries at this playlist index (zero based and inclusive)
+        #[arg(short = 's', long)]
+        start: Option<usize>,
+
+        /// Stop adding playlist entries at this playlist index (zero based and inclusive)
+        #[arg(short = 'e', long)]
+        stop: Option<usize>,
+    },
+
+    /// Mark the video given by the hash to be watched
+    #[command(visible_alias = "w")]
+    Watch {
+        #[command(flatten)]
+        shared: SharedSelectionCommandArgs,
+    },
+
+    /// Mark the video given by the hash to be dropped
+    #[command(visible_alias = "d")]
+    Drop {
+        #[command(flatten)]
+        shared: SharedSelectionCommandArgs,
+    },
+
+    /// Mark the video given by the hash as already watched
+    #[command(visible_alias = "wd")]
+    Watched {
+        #[command(flatten)]
+        shared: SharedSelectionCommandArgs,
+    },
+
+    /// Open the video URL in your specified command
+    #[command(visible_alias = "u")]
+    Url {
+        #[command(flatten)]
+        shared: SharedSelectionCommandArgs,
+    },
+
+    /// Reset the videos status to 'Pick'
+    #[command(visible_alias = "p")]
+    Pick {
+        #[command(flatten)]
+        shared: SharedSelectionCommandArgs,
+    },
+}
+impl Default for SelectCommand {
+    fn default() -> Self {
+        Self::File {
+            done: false,
+            use_last_selection: false,
+        }
+    }
+}
+
+#[derive(Clone, Debug, Args)]
+#[command(infer_subcommands = true)]
+/// Mark the video given by the hash to be watched
+struct SharedSelectionCommandArgs {
+    /// The ordering priority (higher means more at the top)
+    #[arg(short, long)]
+    priority: Option<i64>,
+
+    /// The subtitles to download (e.g. 'en,de,sv')
+    #[arg(short = 'l', long)]
+    subtitle_langs: Option<String>,
+
+    /// The speed to set mpv to
+    #[arg(short = 's', long)]
+    playback_speed: Option<f64>,
+
+    /// The short extractor hash
+    hash: LazyExtractorHash,
+
+    title: Option<String>,
+
+    date: Option<OptionalNaiveDate>,
+
+    publisher: Option<OptionalPublisher>,
+
+    duration: Option<MaybeDuration>,
+
+    url: Option<Url>,
+}
+
+impl SelectCommand {
+    fn into_shared(self) -> Option<SharedSelectionCommandArgs> {
+        match self {
+            SelectCommand::File { .. }
+            | SelectCommand::Split { .. }
+            | SelectCommand::Add { .. } => None,
+            SelectCommand::Watch { shared }
+            | SelectCommand::Drop { shared }
+            | SelectCommand::Watched { shared }
+            | SelectCommand::Url { shared }
+            | SelectCommand::Pick { shared } => Some(shared),
+        }
+    }
+}
+
+#[derive(Clone, Debug, Copy)]
+struct OptionalNaiveDate {
+    date: Option<NaiveDate>,
+}
+impl FromStr for OptionalNaiveDate {
+    type Err = anyhow::Error;
+    fn from_str(v: &str) -> Result<Self, Self::Err> {
+        if v == "[No release date]" {
+            Ok(Self { date: None })
+        } else {
+            Ok(Self {
+                date: Some(NaiveDate::from_str(v)?),
+            })
+        }
+    }
+}
+#[derive(Clone, Debug)]
+struct OptionalPublisher {
+    publisher: Option<String>,
+}
+impl FromStr for OptionalPublisher {
+    type Err = anyhow::Error;
+    fn from_str(v: &str) -> Result<Self, Self::Err> {
+        if v == "[No author]" {
+            Ok(Self { publisher: None })
+        } else {
+            Ok(Self {
+                publisher: Some(v.to_owned()),
+            })
+        }
+    }
+}
+
+#[derive(Default, ValueEnum, Clone, Copy, Debug)]
+enum SelectSplitSortKey {
+    /// Sort by the name of the publisher.
+    #[default]
+    Publisher,
+
+    /// Sort by the number of unselected videos per publisher.
+    Videos,
+}
+impl Display for SelectSplitSortKey {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            SelectSplitSortKey::Publisher => f.write_str("publisher"),
+            SelectSplitSortKey::Videos => f.write_str("videos"),
+        }
+    }
+}
+
+#[derive(Default, ValueEnum, Clone, Copy, Debug)]
+enum SelectSplitSortMode {
+    /// Sort in ascending order (small -> big)
+    #[default]
+    Asc,
+
+    /// Sort in descending order (big -> small)
+    Desc,
+}
+
+impl Display for SelectSplitSortMode {
+    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
+        match self {
+            SelectSplitSortMode::Asc => f.write_str("asc"),
+            SelectSplitSortMode::Desc => f.write_str("desc"),
+        }
+    }
+}
diff --git a/crates/yt/src/commands/show/implm/mod.rs b/crates/yt/src/commands/show/implm/mod.rs
new file mode 100644
index 0000000..a2e40fd
--- /dev/null
+++ b/crates/yt/src/commands/show/implm/mod.rs
@@ -0,0 +1,110 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    fs::{self, OpenOptions},
+    io,
+    process::Command,
+};
+
+use crate::{
+    app::App,
+    commands::ShowCommand,
+    output::{display_fmt_and_less, display_less},
+    storage::db::video::Video,
+};
+
+use anyhow::{Context, Result, anyhow, bail};
+use tempfile::Builder;
+use tokio_util::bytes::Buf;
+
+impl ShowCommand {
+    pub(in crate::commands) async fn implm(&self, app: &App) -> Result<()> {
+        match self {
+            ShowCommand::Description {} => {
+                let description = Video::get_current_description(app).await?;
+
+                display_fmt_and_less(&description)?;
+            }
+            ShowCommand::Comments {} => {
+                let comments = Video::get_current_comments(app).await?;
+
+                display_less(comments.render(app.config.global.display_colors))?;
+            }
+            ShowCommand::Thumbnail {} => {
+                let video = Video::currently_focused(app).await?.ok_or(anyhow!(
+                    "You need to have a current video to display its info"
+                ))?;
+
+                if let Some(url) = video.thumbnail_url {
+                    let response = reqwest::get(url.clone())
+                        .await
+                        .with_context(|| format!("Failed to download thumbnail from url: {url}"))?;
+                    let response = response
+                        .error_for_status()
+                        .context("Failed to download thumbnail")?;
+
+                    let (tmp_path, mut tmp) = {
+                        let file = Builder::new().prefix("yt-thumbnail-download").tempfile()?;
+                        let (_, path) = file.keep()?;
+                        let new_file = OpenOptions::new()
+                            .write(true)
+                            .read(false)
+                            .create(false)
+                            .truncate(true)
+                            .open(&path)?;
+
+                        (path, new_file)
+                    };
+
+                    let mut content = response.bytes().await?.reader();
+                    io::copy(&mut content, &mut tmp)?;
+
+                    let status = Command::new(app.config.commands.image_show.first())
+                        .args(app.config.commands.image_show.tail())
+                        .arg(tmp_path.as_os_str())
+                        .status()
+                        .context("Failed to spawn image show command")?;
+
+                    if !status.success() {
+                        bail!(
+                            "{:?} failed with status: {}",
+                            &app.config.commands.image_show.join(" "),
+                            status
+                        );
+                    }
+
+                    fs::remove_file(&tmp_path).with_context(|| {
+                        format!(
+                            "Failed to cleanup downloaded thumbnail image at: {}",
+                            tmp_path.display()
+                        )
+                    })?;
+                } else {
+                    eprintln!("Current video does not have a thumbnail.");
+                }
+            }
+            ShowCommand::Info {} => {
+                let video = Video::currently_focused(app).await?.ok_or(anyhow!(
+                    "You need to have a current video to display its info"
+                ))?;
+
+                display_less(
+                    video
+                        .to_info_display(app, None)
+                        .await
+                        .context("Failed to format video")?,
+                )?;
+            }
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/yt/src/commands/show/mod.rs b/crates/yt/src/commands/show/mod.rs
new file mode 100644
index 0000000..60f2e51
--- /dev/null
+++ b/crates/yt/src/commands/show/mod.rs
@@ -0,0 +1,30 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::Subcommand;
+
+mod implm;
+
+#[derive(Subcommand, Debug)]
+pub(super) enum ShowCommand {
+    /// Display the description of the currently playing video
+    Description {},
+
+    /// Display the comments of the currently playing video.
+    Comments {},
+
+    /// Display the thumbnail of the currently playing video.
+    Thumbnail {},
+
+    /// Display general info of the currently playing video.
+    ///
+    /// This is the same as running `yt videos info <hash of current video>`
+    Info {},
+}
diff --git a/crates/yt/src/commands/status/implm.rs b/crates/yt/src/commands/status/implm.rs
new file mode 100644
index 0000000..dabc5df
--- /dev/null
+++ b/crates/yt/src/commands/status/implm.rs
@@ -0,0 +1,157 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::time::Duration;
+
+use crate::{
+    app::App,
+    commands::status::StatusCommand,
+    select::duration::MaybeDuration,
+    shared::bytes::Bytes,
+    storage::db::{
+        subscription::Subscriptions,
+        video::{Video, VideoStatusMarker},
+    },
+    yt_dlp::get_current_cache_allocation,
+};
+
+use anyhow::{Context, Result};
+
+macro_rules! get {
+    ($videos:expr, $status:ident) => {
+        $videos
+            .iter()
+            .filter(|vid| vid.status.as_marker() == VideoStatusMarker::$status)
+            .count()
+    };
+
+    (@collect $videos:expr, $status:ident) => {
+        $videos
+            .iter()
+            .filter(|vid| vid.status.as_marker() == VideoStatusMarker::$status)
+            .collect()
+    };
+}
+
+impl StatusCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        let StatusCommand { format } = self;
+
+        let all_videos = Video::in_states(app, VideoStatusMarker::ALL).await?;
+
+        // lengths
+        let picked_videos_len = get!(all_videos, Pick);
+
+        let watch_videos_len = get!(all_videos, Watch);
+        let cached_videos_len = get!(all_videos, Cached);
+        let watched_videos_len = get!(all_videos, Watched);
+        let watched_videos: Vec<_> = get!(@collect all_videos, Watched);
+
+        let drop_videos_len = get!(all_videos, Drop);
+        let dropped_videos_len = get!(all_videos, Dropped);
+
+        let subscriptions = Subscriptions::get(app).await?;
+        let subscriptions_len = subscriptions.0.len();
+
+        let watchtime_status = {
+            let total_watch_time_raw = watched_videos
+                .iter()
+                .fold(Duration::default(), |acc, vid| acc + vid.watch_progress);
+
+            // Most things are watched at a speed of s (which is defined in the config file).
+            // Thus
+            //      y = x * s -> y / s = x
+            let total_watch_time = Duration::from_secs_f64(
+                (total_watch_time_raw.as_secs_f64()) / app.config.select.playback_speed,
+            );
+
+            let speed = app.config.select.playback_speed;
+
+            // Do not print the adjusted time, if the user has keep the speed level at 1.
+            #[allow(clippy::float_cmp)]
+            if speed == 1.0 {
+                format!(
+                    "Total Watchtime: {}\n",
+                    MaybeDuration::from_std(total_watch_time_raw)
+                )
+            } else {
+                format!(
+                    "Total Watchtime: {} (at {speed} speed: {})\n",
+                    MaybeDuration::from_std(total_watch_time_raw),
+                    MaybeDuration::from_std(total_watch_time),
+                )
+            }
+        };
+
+        let watch_rate: f64 = {
+            fn to_f64(input: usize) -> f64 {
+                f64::from(u32::try_from(input).expect("This should never exceed u32::MAX"))
+            }
+
+            let count =
+                to_f64(watched_videos_len) / (to_f64(drop_videos_len) + to_f64(dropped_videos_len));
+            count * 100.0
+        };
+
+        let cache_usage: Bytes = get_current_cache_allocation(app)
+            .await
+            .context("Failed to get current cache allocation")?;
+
+        if let Some(fmt) = format {
+            let output = fmt
+                .replace(
+                    "{picked_videos_len}",
+                    picked_videos_len.to_string().as_str(),
+                )
+                .replace("{watch_videos_len}", watch_videos_len.to_string().as_str())
+                .replace(
+                    "{cached_videos_len}",
+                    cached_videos_len.to_string().as_str(),
+                )
+                .replace(
+                    "{watched_videos_len}",
+                    watched_videos_len.to_string().as_str(),
+                )
+                .replace("{watch_rate}", watch_rate.to_string().as_str())
+                .replace("{drop_videos_len}", drop_videos_len.to_string().as_str())
+                .replace(
+                    "{dropped_videos_len}",
+                    dropped_videos_len.to_string().as_str(),
+                )
+                .replace("{watchtime_status}", watchtime_status.to_string().as_str())
+                .replace(
+                    "{subscriptions_len}",
+                    subscriptions_len.to_string().as_str(),
+                )
+                .replace("{cache_usage}", cache_usage.to_string().as_str());
+
+            print!("{output}");
+        } else {
+            println!(
+                "\
+Picked   Videos: {picked_videos_len}
+
+Watch    Videos: {watch_videos_len}
+Cached   Videos: {cached_videos_len}
+Watched  Videos: {watched_videos_len} (watch rate: {watch_rate:.2} %)
+
+Drop     Videos: {drop_videos_len}
+Dropped  Videos: {dropped_videos_len}
+
+{watchtime_status}
+
+  Subscriptions: {subscriptions_len}
+    Cache usage: {cache_usage}"
+            );
+        }
+
+        Ok(())
+    }
+}
diff --git a/crates/yt/src/commands/status/mod.rs b/crates/yt/src/commands/status/mod.rs
new file mode 100644
index 0000000..4a8dee7
--- /dev/null
+++ b/crates/yt/src/commands/status/mod.rs
@@ -0,0 +1,20 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::Parser;
+
+mod implm;
+
+#[derive(Parser, Debug)]
+pub(super) struct StatusCommand {
+    /// Which format to use
+    #[arg(short, long)]
+    format: Option<String>,
+}
diff --git a/crates/yt/src/commands/subscriptions/implm.rs b/crates/yt/src/commands/subscriptions/implm.rs
new file mode 100644
index 0000000..31b714e
--- /dev/null
+++ b/crates/yt/src/commands/subscriptions/implm.rs
@@ -0,0 +1,253 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::str::FromStr;
+
+use crate::{
+    app::App,
+    commands::subscriptions::SubscriptionCommand,
+    storage::db::{
+        insert::{Operations, subscription::Operation},
+        subscription::{Subscription, Subscriptions, check_url},
+    },
+};
+
+use anyhow::{Context, Result, bail};
+use log::{error, warn};
+use tokio::{
+    fs::File,
+    io::{AsyncBufRead, AsyncBufReadExt, BufReader, stdin},
+};
+use url::Url;
+use yt_dlp::{json_cast, json_get, json_try_get, options::YoutubeDLOptions};
+
+impl SubscriptionCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        match self {
+            SubscriptionCommand::Add {
+                name,
+                url,
+                no_check,
+            } => {
+                let mut ops = Operations::new("main: subscribe");
+                subscribe(app, name, url, no_check, &mut ops)
+                    .await
+                    .context("Failed to add a subscription")?;
+                ops.commit(app).await?;
+            }
+            SubscriptionCommand::Remove { name } => {
+                let mut present_subscriptions = Subscriptions::get(app).await?;
+
+                let mut ops = Operations::new("Subscribe: unsubscribe");
+                if let Some(subscription) = present_subscriptions.0.remove(&name) {
+                    subscription.remove(&mut ops);
+                } else {
+                    bail!("Couldn't find subscription: '{}'", &name);
+                }
+                ops.commit(app)
+                    .await
+                    .with_context(|| format!("Failed to unsubscribe from {name:?}"))?;
+            }
+            SubscriptionCommand::List {} => {
+                let all_subs = Subscriptions::get(app).await?;
+
+                for (key, val) in all_subs.0 {
+                    println!("{}: '{}'", key, val.url);
+                }
+            }
+            SubscriptionCommand::Export {} => {
+                let all_subs = Subscriptions::get(app).await?;
+                for val in all_subs.0.values() {
+                    println!("{}", val.url);
+                }
+            }
+            SubscriptionCommand::Import {
+                file,
+                force,
+                no_check,
+            } => {
+                if let Some(file) = file {
+                    let f = File::open(file).await?;
+
+                    import(app, BufReader::new(f), force, no_check).await?;
+                } else {
+                    import(app, BufReader::new(stdin()), force, no_check).await?;
+                }
+            }
+        }
+
+        Ok(())
+    }
+}
+
+async fn import<W: AsyncBufRead + AsyncBufReadExt + Unpin>(
+    app: &App,
+    reader: W,
+    force: bool,
+    no_check: bool,
+) -> Result<()> {
+    let mut ops = Operations::new("SubscribeImport: init");
+
+    let all = Subscriptions::get(app).await?;
+    if force {
+        all.remove(&mut ops);
+    }
+    ops.commit(app).await?;
+    let mut ops = Operations::new("SubscribeImport: after all subs remove");
+
+    let mut lines = reader.lines();
+    while let Some(line) = lines.next_line().await? {
+        let url =
+            Url::from_str(&line).with_context(|| format!("Failed to parse '{line}' as url"))?;
+
+        match subscribe(app, None, url, no_check, &mut ops)
+            .await
+            .with_context(|| format!("Failed to subscribe to: '{line}'"))
+        {
+            Ok(()) => (),
+            Err(err) => eprintln!(
+                "Error while subscribing to '{}': '{}'",
+                line,
+                err.source().expect("Should have a source")
+            ),
+        }
+    }
+    ops.commit(app).await?;
+
+    Ok(())
+}
+
+async fn subscribe(
+    app: &App,
+    name: Option<String>,
+    url: Url,
+    no_check: bool,
+    ops: &mut Operations<Operation>,
+) -> Result<()> {
+    if !(url.as_str().ends_with("videos")
+        || url.as_str().ends_with("streams")
+        || url.as_str().ends_with("shorts")
+        || url.as_str().ends_with("videos/")
+        || url.as_str().ends_with("streams/")
+        || url.as_str().ends_with("shorts/"))
+        && url.as_str().contains("youtube.com")
+    {
+        warn!(
+            "Your youtube url does not seem like it actually tracks a channels playlist \
+            (videos, streams, shorts). Adding subscriptions for each of them..."
+        );
+
+        let url = Url::parse(&(url.as_str().to_owned() + "/"))
+            .expect("This was an url, it should stay one");
+
+        let (videos, streams, shorts) = if let Some(name) = name {
+            (
+                Some(name.clone() + " {Videos}"),
+                Some(name.clone() + " {Streams}"),
+                Some(name.clone() + " {Shorts}"),
+            )
+        } else {
+            (None, None, None)
+        };
+
+        let _ = actual_subscribe(
+            app,
+            videos,
+            url.join("videos/").expect("See above."),
+            no_check,
+            ops,
+        )
+        .await
+        .map_err(|err| {
+            error!("Failed to subscribe to the '{}' variant: {err}", "{Videos}");
+        });
+
+        let _ = actual_subscribe(
+            app,
+            streams,
+            url.join("streams/").expect("See above."),
+            no_check,
+            ops,
+        )
+        .await
+        .map_err(|err| {
+            error!(
+                "Failed to subscribe to the '{}' variant: {err}",
+                "{Streams}"
+            );
+        });
+
+        let _ = actual_subscribe(
+            app,
+            shorts,
+            url.join("shorts/").expect("See above."),
+            no_check,
+            ops,
+        )
+        .await
+        .map_err(|err| {
+            error!("Failed to subscribe to the '{}' variant: {err}", "{Shorts}");
+        });
+    } else {
+        actual_subscribe(app, name, url, no_check, ops).await?;
+    }
+
+    Ok(())
+}
+
+async fn actual_subscribe(
+    app: &App,
+    name: Option<String>,
+    url: Url,
+    no_check: bool,
+    ops: &mut Operations<Operation>,
+) -> Result<()> {
+    if !no_check && !check_url(url.clone()).await? {
+        bail!("The url ('{}') does not represent a playlist!", &url)
+    }
+
+    let name = if let Some(name) = name {
+        name
+    } else {
+        let yt_dlp = YoutubeDLOptions::new()
+            .set("playliststart", 1)
+            .set("playlistend", 10)
+            .set("noplaylist", false)
+            .set("extract_flat", "in_playlist")
+            .build()?;
+
+        let info = yt_dlp.extract_info(&url, false, false)?;
+
+        if json_try_get!(info, "_type", as_str) == Some("playlist") {
+            json_get!(info, "title", as_str).to_owned()
+        } else {
+            bail!("The url ('{}') does not represent a playlist!", &url)
+        }
+    };
+
+    let present_subscriptions = Subscriptions::get(app).await?;
+
+    if let Some(subs) = present_subscriptions.0.get(&name) {
+        bail!(
+            "The subscription '{}' could not be added, \
+                as another one with the same name ('{}') already exists. \
+                It points to the Url: '{}'",
+            name,
+            name,
+            subs.url
+        );
+    }
+
+    let sub = Subscription { name, url };
+
+    sub.add(ops);
+
+    Ok(())
+}
diff --git a/crates/yt/src/commands/subscriptions/mod.rs b/crates/yt/src/commands/subscriptions/mod.rs
new file mode 100644
index 0000000..edd41c6
--- /dev/null
+++ b/crates/yt/src/commands/subscriptions/mod.rs
@@ -0,0 +1,62 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::path::PathBuf;
+
+use clap::Subcommand;
+use clap_complete::ArgValueCompleter;
+use url::Url;
+
+use crate::commands::complete_subscription;
+
+mod implm;
+
+#[derive(Subcommand, Clone, Debug)]
+pub(super) enum SubscriptionCommand {
+    /// Subscribe to an URL
+    Add {
+        #[arg(short, long)]
+        /// The human readable name of the subscription
+        name: Option<String>,
+
+        /// The URL to listen to
+        url: Url,
+
+        /// Don't check, whether the URL actually points to something yt understands.
+        #[arg(long, default_value_t = false)]
+        no_check: bool,
+    },
+
+    /// Unsubscribe from an URL
+    Remove {
+        /// The human readable name of the subscription
+        #[arg(add = ArgValueCompleter::new(complete_subscription))]
+        name: String,
+    },
+
+    /// Import a bunch of URLs as subscriptions.
+    Import {
+        /// The file containing the URLs. Will use Stdin otherwise.
+        file: Option<PathBuf>,
+
+        /// Remove any previous subscriptions
+        #[arg(short, long)]
+        force: bool,
+
+        /// Don't check, whether the URLs actually point to something yt understands.
+        #[arg(long, default_value_t = false)]
+        no_check: bool,
+    },
+    /// Write all subscriptions in an format understood by `import`
+    Export {},
+
+    /// List all subscriptions
+    List {},
+}
diff --git a/crates/yt/src/commands/update/implm/mod.rs b/crates/yt/src/commands/update/implm/mod.rs
new file mode 100644
index 0000000..53c7415
--- /dev/null
+++ b/crates/yt/src/commands/update/implm/mod.rs
@@ -0,0 +1,62 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    commands::update::{UpdateCommand, implm::updater::Updater},
+    storage::db::{
+        extractor_hash::ExtractorHash,
+        subscription::{Subscription, Subscriptions},
+    },
+};
+
+use anyhow::{Result, bail};
+
+mod updater;
+
+impl UpdateCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        let UpdateCommand {
+            max_backlog,
+            subscriptions: subscription_names_to_update,
+        } = self;
+
+        let mut all_subs = Subscriptions::get(app).await?;
+
+        let max_backlog = max_backlog.unwrap_or(app.config.update.max_backlog);
+
+        let subs: Vec<Subscription> = if subscription_names_to_update.is_empty() {
+            all_subs.0.into_values().collect()
+        } else {
+            subscription_names_to_update
+                .into_iter()
+                .map(|sub| {
+                    if let Some(val) = all_subs.0.remove(&sub) {
+                        Ok(val)
+                    } else {
+                        bail!(
+                            "Your specified subscription to update '{}' is not a subscription!",
+                            sub
+                        )
+                    }
+                })
+                .collect::<Result<_>>()?
+        };
+
+        // We can get away with not having to re-fetch the hashes every time, as the returned video
+        // should not contain duplicates.
+        let hashes = ExtractorHash::get_all(app).await?;
+
+        let updater = Updater::new(max_backlog, app.config.update.pool_size, hashes);
+        updater.update(app, subs).await?;
+
+        Ok(())
+    }
+}
diff --git a/crates/yt/src/commands/update/implm/updater.rs b/crates/yt/src/commands/update/implm/updater.rs
new file mode 100644
index 0000000..2b96bf2
--- /dev/null
+++ b/crates/yt/src/commands/update/implm/updater.rs
@@ -0,0 +1,205 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::sync::atomic::{AtomicUsize, Ordering};
+
+use anyhow::{Context, Result};
+use futures::{StreamExt, future::join_all, stream};
+use log::{Level, debug, error, log_enabled};
+use tokio::io::{AsyncWriteExt, stderr};
+use tokio_util::task::LocalPoolHandle;
+use yt_dlp::{
+    info_json::InfoJson, json_cast, json_try_get, options::YoutubeDLOptions, process_ie_result,
+    python_error::PythonError,
+};
+
+use crate::{
+    ansi_escape_codes,
+    app::App,
+    storage::db::{
+        extractor_hash::ExtractorHash, insert::Operations, subscription::Subscription, video::Video,
+    },
+    yt_dlp::yt_dlp_opts_updating,
+};
+
+pub(super) struct Updater {
+    max_backlog: usize,
+    hashes: Vec<ExtractorHash>,
+    pool: LocalPoolHandle,
+}
+
+static REACHED_NUMBER: AtomicUsize = const { AtomicUsize::new(1) };
+
+impl Updater {
+    pub(super) fn new(max_backlog: usize, max_threads: usize, hashes: Vec<ExtractorHash>) -> Self {
+        let pool = LocalPoolHandle::new(max_threads);
+
+        Self {
+            max_backlog,
+            hashes,
+            pool,
+        }
+    }
+
+    pub(super) async fn update(self, app: &App, subscriptions: Vec<Subscription>) -> Result<()> {
+        let total_number = subscriptions.len();
+
+        let mut stream = stream::iter(subscriptions)
+            .map(|sub| self.get_new_entries(sub, total_number))
+            .buffer_unordered(app.config.update.futures);
+
+        while let Some(output) = stream.next().await {
+            let mut entries = output?;
+
+            if let Some(next) = entries.next() {
+                let (sub, entry) = next;
+                process_subscription(app, sub, entry).await?;
+
+                join_all(entries.map(|(sub, entry)| process_subscription(app, sub, entry)))
+                    .await
+                    .into_iter()
+                    .collect::<Result<(), _>>()?;
+            }
+        }
+
+        Ok(())
+    }
+
+    #[allow(clippy::too_many_lines)]
+    async fn get_new_entries(
+        &self,
+        sub: Subscription,
+        total_number: usize,
+    ) -> Result<impl Iterator<Item = (Subscription, InfoJson)>> {
+        let max_backlog = self.max_backlog;
+        let hashes = self.hashes.clone();
+
+        let yt_dlp = yt_dlp_opts_updating(max_backlog)?;
+
+        self.pool
+            .spawn_pinned(move || {
+                async move {
+                    if !log_enabled!(Level::Debug) {
+                        ansi_escape_codes::clear_whole_line();
+                        ansi_escape_codes::move_to_col(1);
+                        eprint!(
+                            "({}/{total_number}) Checking playlist {}...",
+                            REACHED_NUMBER.fetch_add(1, Ordering::Relaxed),
+                            sub.name
+                        );
+                        ansi_escape_codes::move_to_col(1);
+                        stderr().flush().await?;
+                    }
+
+                    let info = yt_dlp
+                        .extract_info(&sub.url, false, false)
+                        .with_context(|| format!("Failed to get playlist '{}'.", sub.name))?;
+
+                    let empty = vec![];
+                    let entries = json_try_get!(info, "entries", as_array).unwrap_or(&empty);
+
+                    let valid_entries: Vec<(Subscription, InfoJson)> = entries
+                        .iter()
+                        .take(max_backlog)
+                        .filter_map(|entry| -> Option<(Subscription, InfoJson)> {
+                            let extractor_hash =
+                                ExtractorHash::from_info_json(json_cast!(entry, as_object));
+
+                            if hashes.contains(&extractor_hash) {
+                                debug!(
+                                    "Skipping entry, as it is \
+                                        already present: '{extractor_hash}'",
+                                );
+                                None
+                            } else {
+                                Some((sub.clone(), json_cast!(entry, as_object).to_owned()))
+                            }
+                        })
+                        .collect();
+
+                    Ok(valid_entries
+                        .into_iter()
+                        .map(|(sub, entry)| {
+                            let inner_yt_dlp = YoutubeDLOptions::new()
+                                .set("noplaylist", true)
+                                .build()
+                                .expect("Worked before, should work now");
+
+                            match inner_yt_dlp.process_ie_result(entry, false) {
+                                Ok(output) => Ok((sub, output)),
+                                Err(err) => Err(err),
+                            }
+                        })
+                        // Don't fail the whole update, if one of the entries fails to fetch.
+                        .filter_map(move |base| match base {
+                            Ok(ok) => Some(ok),
+                            Err(err) => {
+                                match err {
+                                    process_ie_result::Error::Python(PythonError(err)) => {
+                                        if err.contains(
+                                            "Join this channel to get access \
+                                            to members-only content ",
+                                        ) {
+                                            // Hide this error
+                                        } else {
+                                            // Show the error, but don't fail.
+                                            let error = err
+                                                .strip_prefix(
+                                                    "DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ",
+                                                )
+                                                .unwrap_or(&err);
+                                            error!("While fetching {:#?}: {error}", sub.name);
+                                        }
+
+                                        None
+                                    }
+                                    process_ie_result::Error::InfoJsonPrepare(error) => {
+                                        error!(
+                                            "While fetching {:#?}: Failed to prepare \
+                                            info json: {error}",
+                                            sub.name
+                                        );
+                                        None
+                                    }
+                                }
+                            }
+                        }))
+                }
+            })
+            .await?
+    }
+}
+
+async fn process_subscription(app: &App, sub: Subscription, entry: InfoJson) -> Result<()> {
+    let mut ops = Operations::new("Update: process subscription");
+    let video = Video::from_info_json(&entry, Some(&sub))
+        .context("Failed to parse search entry as Video")?;
+
+    let title = video.title.clone();
+    let url = video.url.clone();
+    let video = video.add(&mut ops).with_context(|| {
+        format!("Failed to add video to database: '{title}' (with url: '{url}')")
+    })?;
+
+    ops.commit(app).await.with_context(|| {
+        format!(
+            "Failed to add video to database: '{}' (with url: '{}')",
+            video.title, video.url
+        )
+    })?;
+    println!(
+        "{}",
+        &video
+            .to_line_display(app, None)
+            .await
+            .with_context(|| format!("Failed to format video: '{}'", video.title))?
+    );
+    Ok(())
+}
diff --git a/crates/yt/src/commands/update/mod.rs b/crates/yt/src/commands/update/mod.rs
new file mode 100644
index 0000000..cb29148
--- /dev/null
+++ b/crates/yt/src/commands/update/mod.rs
@@ -0,0 +1,27 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::Parser;
+use clap_complete::ArgValueCompleter;
+
+use crate::commands::complete_subscription;
+
+mod implm;
+
+#[derive(Parser, Debug)]
+pub(super) struct UpdateCommand {
+    /// The maximal number of videos to fetch for each subscription.
+    #[arg(short, long)]
+    max_backlog: Option<usize>,
+
+    /// The subscriptions to update
+    #[arg(add = ArgValueCompleter::new(complete_subscription))]
+    subscriptions: Vec<String>,
+}
diff --git a/crates/yt/src/commands/videos/implm.rs b/crates/yt/src/commands/videos/implm.rs
new file mode 100644
index 0000000..2a018c7
--- /dev/null
+++ b/crates/yt/src/commands/videos/implm.rs
@@ -0,0 +1,73 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    commands::videos::VideosCommand,
+    storage::db::video::{Video, VideoStatusMarker},
+};
+
+use anyhow::{Context, Result};
+use futures::{TryStreamExt, stream::FuturesUnordered};
+
+impl VideosCommand {
+    pub(in crate::commands) async fn implm(self, app: &App) -> Result<()> {
+        match self {
+            VideosCommand::List {
+                search_query,
+                limit,
+                format,
+            } => {
+                let all_videos = Video::in_states(app, VideoStatusMarker::ALL).await?;
+
+                // turn one video to a color display, to pre-warm the hash shrinking cache
+                if let Some(val) = all_videos.first() {
+                    val.to_line_display(app, format.clone()).await?;
+                }
+
+                let limit = limit.unwrap_or(all_videos.len());
+
+                let all_video_strings: Vec<String> = all_videos
+                    .into_iter()
+                    .take(limit)
+                    .map(|vid| to_line_display_owned(vid, app, format.clone()))
+                    .collect::<FuturesUnordered<_>>()
+                    .try_collect::<Vec<String>>()
+                    .await?;
+
+                if let Some(query) = search_query {
+                    all_video_strings
+                        .into_iter()
+                        .filter(|video| video.to_lowercase().contains(&query.to_lowercase()))
+                        .for_each(|video| println!("{video}"));
+                } else {
+                    println!("{}", all_video_strings.join("\n"));
+                }
+            }
+            VideosCommand::Info { hash, format } => {
+                let video = hash.realize(app, None).await?.get_with_app(app).await?;
+
+                print!(
+                    "{}",
+                    &video
+                        .to_info_display(app, format)
+                        .await
+                        .context("Failed to format video")?
+                );
+            }
+        }
+
+        Ok(())
+    }
+}
+
+async fn to_line_display_owned(video: Video, app: &App, format: Option<String>) -> Result<String> {
+    video.to_line_display(app, format).await
+}
diff --git a/crates/yt/src/commands/videos/mod.rs b/crates/yt/src/commands/videos/mod.rs
new file mode 100644
index 0000000..ca20715
--- /dev/null
+++ b/crates/yt/src/commands/videos/mod.rs
@@ -0,0 +1,46 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::{ArgAction, Subcommand};
+
+use crate::storage::db::extractor_hash::LazyExtractorHash;
+
+mod implm;
+
+#[derive(Subcommand, Clone, Debug)]
+pub(super) enum VideosCommand {
+    /// List the videos in the database
+    #[command(visible_alias = "ls")]
+    List {
+        /// An optional search query to limit the results
+        #[arg(action = ArgAction::Append)]
+        search_query: Option<String>,
+
+        /// The format string to use.
+        // TODO(@bpeetz): Encode the default format, as the default string here. <2025-07-04>
+        #[arg(short, long)]
+        format: Option<String>,
+
+        /// The number of videos to show
+        #[arg(short, long)]
+        limit: Option<usize>,
+    },
+
+    /// Get detailed information about a video
+    Info {
+        /// The short hash of the video
+        hash: LazyExtractorHash,
+
+        /// The format string to use.
+        // TODO(@bpeetz): Encode the default format, as the default string here. <2025-07-04>
+        #[arg(short, long)]
+        format: Option<String>,
+    },
+}
diff --git a/crates/yt/src/commands/watch/implm/mod.rs b/crates/yt/src/commands/watch/implm/mod.rs
new file mode 100644
index 0000000..8182216
--- /dev/null
+++ b/crates/yt/src/commands/watch/implm/mod.rs
@@ -0,0 +1,244 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    fs,
+    path::PathBuf,
+    sync::{
+        Arc,
+        atomic::{AtomicBool, Ordering},
+    },
+};
+
+use crate::{
+    app::App,
+    commands::watch::{WatchCommand, implm::playlist_handler::Status},
+    storage::{
+        db::{
+            insert::{Operations, maintenance::clear_stale_downloaded_paths},
+            playlist::Playlist,
+        },
+        notify::wait_for_db_write,
+    },
+};
+
+use anyhow::{Context, Result};
+use libmpv2::{Mpv, events::EventContext};
+use log::{debug, info, trace, warn};
+use tokio::{task, time};
+
+mod playlist_handler;
+
+impl WatchCommand {
+    #[allow(clippy::too_many_lines)]
+    pub(in crate::commands) async fn implm(self, app: Arc<App>) -> Result<()> {
+        let WatchCommand {
+            provide_ipc_socket,
+            headless,
+        } = self;
+
+        clear_stale_downloaded_paths(&app).await?;
+
+        let ipc_socket = if provide_ipc_socket {
+            Some(app.config.paths.mpv_ipc_socket_path.clone())
+        } else {
+            None
+        };
+
+        let (mpv, mut ev_ctx) =
+            init_mpv(&app, ipc_socket, headless).context("Failed to initialize mpv instance")?;
+        let mpv = Arc::new(mpv);
+
+        if provide_ipc_socket {
+            println!("{}", app.config.paths.mpv_ipc_socket_path.display());
+        }
+
+        let should_break = Arc::new(AtomicBool::new(false));
+        let local_app = Arc::clone(&app);
+        let local_mpv = Arc::clone(&mpv);
+        let local_should_break = Arc::clone(&should_break);
+        let progress_handle = task::spawn(async move {
+            loop {
+                if local_should_break.load(Ordering::Relaxed) {
+                    trace!("WatchProgressThread: Stopping, as we received exit signal.");
+                    break;
+                }
+
+                let mut playlist = Playlist::create(&local_app).await?;
+
+                if let Some(index) = playlist.current_index() {
+                    trace!("WatchProgressThread: Saving watch progress for current video");
+
+                    let mut ops =
+                        Operations::new("WatchProgressThread: save watch progress thread");
+                    playlist.save_watch_progress(&local_mpv, index, &mut ops);
+                    ops.commit(&local_app).await?;
+                } else {
+                    trace!(
+                        "WatchProgressThread: Tried to save current watch progress, but no video active."
+                    );
+                }
+
+                time::sleep(local_app.config.watch.progress_save_intervall).await;
+            }
+
+            Ok::<(), anyhow::Error>(())
+        });
+
+        let playlist = Playlist::create(&app).await?;
+        playlist.resync_with_mpv(&app, &mpv)?;
+
+        let mut have_warned = (false, 0);
+        'watchloop: loop {
+            'waitloop: while let Ok(value) = playlist_handler::status(&app).await {
+                match value {
+                    Status::NoMoreAvailable => {
+                        break 'watchloop;
+                    }
+                    Status::NoCached { marked_watch } => {
+                        // try again next time.
+                        if have_warned.0 {
+                            if have_warned.1 != marked_watch {
+                                warn!("Now {marked_watch} videos are marked as to be watched.");
+                                have_warned.1 = marked_watch;
+                            }
+                        } else {
+                            warn!(
+                                "There is nothing to watch yet, but still {marked_watch} videos marked as to be watched. \
+                                Will idle, until they become available"
+                            );
+                            have_warned = (true, marked_watch);
+                        }
+                        wait_for_db_write(&app).await?;
+
+                        // Add the new videos, if they are there.
+                        let playlist = Playlist::create(&app).await?;
+                        playlist.resync_with_mpv(&app, &mpv)?;
+                    }
+                    Status::Available { newly_available } => {
+                        debug!(
+                            "Checked for currently available videos and found {newly_available}!"
+                        );
+                        have_warned.0 = false;
+
+                        // Something just became available!
+                        break 'waitloop;
+                    }
+                }
+            }
+
+            // TODO(@bpeetz): Is the following assumption correct? <2025-07-10>
+            // We wait until forever for the next event, because we really don't need to do anything
+            // else.
+            if let Some(ev) = ev_ctx.wait_event(f64::MAX) {
+                match ev {
+                    Ok(event) => {
+                        trace!("Mpv event triggered: {event:#?}");
+                        if playlist_handler::handle_mpv_event(&app, &mpv, &event)
+                            .await
+                            .with_context(|| format!("Failed to handle mpv event: '{event:#?}'"))?
+                        {
+                            break;
+                        }
+                    }
+                    Err(e) => debug!("Mpv Event errored: {e}"),
+                }
+            }
+        }
+        should_break.store(true, Ordering::Relaxed);
+        progress_handle.await??;
+
+        if provide_ipc_socket {
+            fs::remove_file(&app.config.paths.mpv_ipc_socket_path).with_context(|| {
+                format!(
+                    "Failed to clean-up the mpv ipc socket at {}",
+                    app.config.paths.mpv_ipc_socket_path.display()
+                )
+            })?;
+        }
+
+        Ok(())
+    }
+}
+
+fn init_mpv(app: &App, ipc_socket: Option<PathBuf>, headless: bool) -> Result<(Mpv, EventContext)> {
+    // set some default values, to make things easier (these can be overridden by the config file,
+    // which we load later)
+    let mpv = Mpv::with_initializer(|mpv| {
+        if let Some(socket) = ipc_socket {
+            mpv.set_property(
+                "input-ipc-server",
+                socket
+                    .to_str()
+                    .expect("This path comes from us, it should never contain not-utf8"),
+            )?;
+        }
+
+        if headless {
+            // Do not provide video output.
+            mpv.set_property("vid", "no")?;
+        } else {
+            // Enable default key bindings, so the user can actually interact with
+            // the player (and e.g. close the window).
+            mpv.set_property("input-default-bindings", "yes")?;
+            mpv.set_property("input-vo-keyboard", "yes")?;
+
+            // Show the on screen controller.
+            mpv.set_property("osc", "yes")?;
+
+            // Don't automatically advance to the next video (or exit the player)
+            mpv.set_option("keep-open", "always")?;
+
+            // Always display an window, even for non-video playback.
+            // As mpv does not have cli access, no window means no control and no user feedback.
+            mpv.set_option("force-window", "yes")?;
+        }
+
+        Ok(())
+    })
+    .context("Failed to initialize mpv")?;
+
+    let config_path = &app.config.paths.mpv_config_path;
+    if config_path.try_exists()? {
+        info!("Found mpv.conf at '{}'!", config_path.display());
+        mpv.command(
+            "load-config-file",
+            &[config_path
+                .to_str()
+                .context("Failed to parse the config path is utf8-stringt")?],
+        )?;
+    } else {
+        warn!(
+            "Did not find a mpv.conf file at '{}'",
+            config_path.display()
+        );
+    }
+
+    let input_path = &app.config.paths.mpv_input_path;
+    if input_path.try_exists()? {
+        info!("Found mpv.input.conf at '{}'!", input_path.display());
+        mpv.command(
+            "load-input-conf",
+            &[input_path
+                .to_str()
+                .context("Failed to parse the input path as utf8 string")?],
+        )?;
+    } else {
+        warn!(
+            "Did not find a mpv.input.conf file at '{}'",
+            input_path.display()
+        );
+    }
+
+    let ev_ctx = EventContext::new(mpv.ctx);
+    ev_ctx.disable_deprecated_events()?;
+
+    Ok((mpv, ev_ctx))
+}
diff --git a/yt/src/watch/playlist_handler/client_messages/mod.rs b/crates/yt/src/commands/watch/implm/playlist_handler/client_messages.rs
index 6f7a59e..fd7e035 100644
--- a/yt/src/watch/playlist_handler/client_messages/mod.rs
+++ b/crates/yt/src/commands/watch/implm/playlist_handler/client_messages.rs
@@ -10,7 +10,7 @@
 
 use std::{env, time::Duration};
 
-use crate::{app::App, comments};
+use crate::{app::App, storage::db::video::Video};
 
 use anyhow::{Context, Result, bail};
 use libmpv2::Mpv;
@@ -19,22 +19,12 @@ use tokio::process::Command;
 use super::mpv_message;
 
 async fn run_self_in_external_command(app: &App, args: &[&str]) -> Result<()> {
+    // TODO(@bpeetz): Can we trust this value? <2025-06-15>
     let binary =
         env::current_exe().context("Failed to determine the current executable to re-execute")?;
 
-    let status = Command::new("riverctl")
-        .args(["focus-output", "next"])
-        .status()
-        .await?;
-    if !status.success() {
-        bail!("focusing the next output failed!");
-    }
-
     let arguments = [
         &[
-            "--title",
-            "floating please",
-            "--command",
             binary
                 .to_str()
                 .context("Failed to turn the executable path to a utf8-string")?,
@@ -49,29 +39,24 @@ async fn run_self_in_external_command(app: &App, args: &[&str]) -> Result<()> {
     ]
     .concat();
 
-    let status = Command::new("alacritty").args(arguments).status().await?;
-    if !status.success() {
-        bail!("Falied to start `yt comments`");
-    }
-
-    let status = Command::new("riverctl")
-        .args(["focus-output", "next"])
+    let status = Command::new(app.config.commands.external_spawn.first())
+        .args(app.config.commands.external_spawn.tail())
+        .args(arguments)
         .status()
         .await?;
-
     if !status.success() {
-        bail!("focusing the next output failed!");
+        bail!("Falied to start (external) `yt {}`", args.join(" "));
     }
 
     Ok(())
 }
 
 pub(super) async fn handle_yt_description_external(app: &App) -> Result<()> {
-    run_self_in_external_command(app, &["description"]).await?;
+    run_self_in_external_command(app, &["show", "description"]).await?;
     Ok(())
 }
 pub(super) async fn handle_yt_description_local(app: &App, mpv: &Mpv) -> Result<()> {
-    let description: String = comments::description::get(app)
+    let description: String = Video::get_current_description(app)
         .await?
         .chars()
         .take(app.config.watch.local_displays_length)
@@ -82,11 +67,11 @@ pub(super) async fn handle_yt_description_local(app: &App, mpv: &Mpv) -> Result<
 }
 
 pub(super) async fn handle_yt_comments_external(app: &App) -> Result<()> {
-    run_self_in_external_command(app, &["comments"]).await?;
+    run_self_in_external_command(app, &["show", "comments"]).await?;
     Ok(())
 }
 pub(super) async fn handle_yt_comments_local(app: &App, mpv: &Mpv) -> Result<()> {
-    let comments: String = comments::get(app)
+    let comments: String = Video::get_current_comments(app)
         .await?
         .render(false)
         .chars()
@@ -96,3 +81,13 @@ pub(super) async fn handle_yt_comments_local(app: &App, mpv: &Mpv) -> Result<()>
     mpv_message(mpv, &comments, Duration::from_secs(6))?;
     Ok(())
 }
+
+pub(super) async fn handle_yt_info_external(app: &App) -> Result<()> {
+    run_self_in_external_command(app, &["show", "info"]).await?;
+    Ok(())
+}
+
+pub(super) async fn handle_yt_thumbnail_external(app: &App) -> Result<()> {
+    run_self_in_external_command(app, &["show", "thumbnail"]).await?;
+    Ok(())
+}
diff --git a/crates/yt/src/commands/watch/implm/playlist_handler/mod.rs b/crates/yt/src/commands/watch/implm/playlist_handler/mod.rs
new file mode 100644
index 0000000..bdb77d2
--- /dev/null
+++ b/crates/yt/src/commands/watch/implm/playlist_handler/mod.rs
@@ -0,0 +1,225 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::time::Duration;
+
+use crate::{
+    app::App,
+    storage::db::{
+        insert::{Operations, playlist::VideoTransition},
+        playlist::{Playlist, PlaylistIndex},
+        video::{Video, VideoStatusMarker},
+    },
+};
+
+use anyhow::{Context, Result};
+use libmpv2::{EndFileReason, Mpv, events::Event};
+use log::{debug, info};
+
+mod client_messages;
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum Status {
+    /// There are no videos cached and no more marked to be watched.
+    /// Waiting is pointless.
+    NoMoreAvailable,
+
+    /// There are no videos cached, but some (> 0) are marked to be watched.
+    /// So we should wait for them to become available.
+    NoCached { marked_watch: usize },
+
+    /// There are videos cached and ready to be inserted into the playback queue.
+    Available { newly_available: usize },
+}
+
+fn mpv_message(mpv: &Mpv, message: &str, time: Duration) -> Result<()> {
+    mpv.command(
+        "show-text",
+        &[message, time.as_millis().to_string().as_str()],
+    )?;
+    Ok(())
+}
+
+/// Return the status of the playback queue
+pub(crate) async fn status(app: &App) -> Result<Status> {
+    let playlist = Playlist::create(app).await?;
+
+    let playlist_len = playlist.len();
+    let marked_watch_num = Video::in_states(app, &[VideoStatusMarker::Watch])
+        .await?
+        .len();
+
+    if playlist_len == 0 && marked_watch_num == 0 {
+        Ok(Status::NoMoreAvailable)
+    } else if playlist_len == 0 && marked_watch_num != 0 {
+        Ok(Status::NoCached {
+            marked_watch: marked_watch_num,
+        })
+    } else if playlist_len != 0 {
+        Ok(Status::Available {
+            newly_available: playlist_len,
+        })
+    } else {
+        unreachable!(
+            "The playlist length is {playlist_len}, but the number of marked watch videos is {marked_watch_num}! This is a bug."
+        );
+    }
+}
+
+/// # Returns
+/// This will return [`true`], if the event handling should be stopped
+///
+/// # Panics
+/// Only if internal assertions fail.
+#[allow(clippy::too_many_lines)]
+pub(crate) async fn handle_mpv_event(app: &App, mpv: &Mpv, event: &Event<'_>) -> Result<bool> {
+    let mut ops = Operations::new("PlaylistHandler: handle event");
+
+    // Construct the playlist lazily.
+    // This avoids unneeded db lookups.
+    // (We use the moved `call_once` as guard for this)
+    let call_once = String::new();
+    let playlist = move || {
+        drop(call_once);
+        Playlist::create(app)
+    };
+
+    let should_stop_event_handling = match event {
+        Event::EndFile(r) => match r.reason {
+            EndFileReason::Eof => {
+                info!("Mpv reached the end of the current video. Marking it watched.");
+                playlist().await?.resync_with_mpv(app, mpv)?;
+
+                false
+            }
+            EndFileReason::Stop => {
+                // This reason is incredibly ambiguous. It _both_ means actually pausing a
+                // video and going to the next one in the playlist.
+                // Oh, and it's also called, when a video is removed from the playlist (at
+                // least via "playlist-remove current")
+                info!("Paused video (or went to next playlist entry); Doing nothing");
+
+                false
+            }
+            EndFileReason::Quit => {
+                info!("Mpv quit. Exiting playback");
+
+                playlist().await?.save_current_watch_progress(mpv, &mut ops);
+
+                true
+            }
+            EndFileReason::Error => {
+                unreachable!("This should have been raised as a separate error")
+            }
+            EndFileReason::Redirect => {
+                // TODO: We probably need to handle this somehow <2025-02-17>
+                false
+            }
+        },
+        Event::StartFile(_) => {
+            let mut playlist = playlist().await?;
+
+            let mpv_pos = usize::try_from(mpv.get_property::<i64>("playlist-pos")?)
+                .expect("The value is strictly positive");
+
+            let yt_pos = playlist.current_index().map(usize::from);
+
+            if (Some(mpv_pos) != yt_pos) || yt_pos.is_none() {
+                debug!(
+                    "StartFileHandler: mpv pos {mpv_pos} and our pos {yt_pos:?} do not align. Reloading.."
+                );
+
+                if let Some((_, vid)) = playlist.get_focused_mut() {
+                    vid.set_focused(false, &mut ops);
+                    ops.commit(app)
+                        .await
+                        .context("Failed to commit video unfocusing")?;
+
+                    ops = Operations::new("PlaylistHandler: after set-focused");
+                }
+
+                let video = playlist
+                    .get_mut(PlaylistIndex::from(mpv_pos))
+                    .expect("The mpv pos should not be out of bounds");
+
+                video.set_focused(true, &mut ops);
+
+                playlist.resync_with_mpv(app, mpv)?;
+            }
+
+            false
+        }
+        Event::Seek => {
+            playlist().await?.save_current_watch_progress(mpv, &mut ops);
+
+            false
+        }
+        Event::ClientMessage(a) => {
+            debug!("Got Client Message event: '{}'", a.join(" "));
+
+            match a.as_slice() {
+                &["yt-comments-external"] => {
+                    client_messages::handle_yt_comments_external(app).await?;
+                }
+                &["yt-comments-local"] => {
+                    client_messages::handle_yt_comments_local(app, mpv).await?;
+                }
+
+                &["yt-description-external"] => {
+                    client_messages::handle_yt_description_external(app).await?;
+                }
+                &["yt-description-local"] => {
+                    client_messages::handle_yt_description_local(app, mpv).await?;
+                }
+
+                &["yt-info-external"] => {
+                    client_messages::handle_yt_info_external(app).await?;
+                }
+                &["yt-thumbnail-external"] => {
+                    client_messages::handle_yt_thumbnail_external(app).await?;
+                }
+
+                &["yt-mark-picked"] => {
+                    playlist().await?.mark_current_done(
+                        app,
+                        mpv,
+                        VideoTransition::Picked,
+                        &mut ops,
+                    )?;
+
+                    mpv_message(mpv, "Marked the video as picked", Duration::from_secs(3))?;
+                }
+                &["yt-mark-watched"] => {
+                    playlist().await?.mark_current_done(
+                        app,
+                        mpv,
+                        VideoTransition::Watched,
+                        &mut ops,
+                    )?;
+
+                    mpv_message(mpv, "Marked the video watched", Duration::from_secs(3))?;
+                }
+                &["yt-check-new-videos"] => {
+                    playlist().await?.resync_with_mpv(app, mpv)?;
+                }
+                other => {
+                    debug!("Unknown message: {}", other.join(" "));
+                }
+            }
+
+            false
+        }
+        _ => false,
+    };
+
+    ops.commit(app).await?;
+
+    Ok(should_stop_event_handling)
+}
diff --git a/crates/yt/src/commands/watch/mod.rs b/crates/yt/src/commands/watch/mod.rs
new file mode 100644
index 0000000..ea4c513
--- /dev/null
+++ b/crates/yt/src/commands/watch/mod.rs
@@ -0,0 +1,24 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use clap::Parser;
+
+mod implm;
+
+#[derive(Parser, Debug)]
+pub(super) struct WatchCommand {
+    /// Print the path to an ipc socket for mpv control to stdout at startup.
+    #[arg(long)]
+    provide_ipc_socket: bool,
+
+    /// Don't start an mpv window at all.
+    #[arg(long)]
+    headless: bool,
+}
diff --git a/crates/yt/src/config/mod.rs b/crates/yt/src/config/mod.rs
new file mode 100644
index 0000000..05bb4cf
--- /dev/null
+++ b/crates/yt/src/config/mod.rs
@@ -0,0 +1,138 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::sync::atomic::{AtomicBool, Ordering};
+
+use crate::config::support::mk_config;
+
+mod non_empty_vec;
+mod paths;
+mod support;
+
+pub(crate) static SHOULD_DISPLAY_COLOR: AtomicBool = AtomicBool::new(false);
+
+// We need to do both things to comply with what the config expects.
+#[allow(clippy::trivially_copy_pass_by_ref, clippy::unnecessary_wraps)]
+fn set_static_should_display_color(value: &bool) -> anyhow::Result<()> {
+    SHOULD_DISPLAY_COLOR.store(*value, Ordering::Relaxed);
+
+    Ok(())
+}
+
+mk_config! {
+    use std::path::PathBuf;
+    use std::io::IsTerminal;
+    use std::time::Duration;
+
+    use crate::shared::bytes::Bytes;
+
+    use super::set_static_should_display_color;
+
+    use super::paths::get_config_path;
+    use super::paths::get_runtime_path;
+    use super::paths::get_data_path;
+    use super::paths::ensure_parent_dir;
+    use super::paths::ensure_dir;
+    use super::paths::PREFIX;
+
+    use super::non_empty_vec::NonEmptyVec;
+    use super::non_empty_vec::non_empty_vec;
+
+    struct Config {
+        global: GlobalConfig = {
+            /// Whether to display colors.
+            display_colors: bool where display_color: Option<bool> =! {|config_value: Option<bool>|
+                Ok::<_, anyhow::Error>(
+                    display_color
+                    .unwrap_or(
+                        config_value
+                        .unwrap_or_else(|| std::io::stderr().is_terminal())
+                    )
+                )
+            } => set_static_should_display_color,
+        },
+        select: SelectConfig = {
+            /// The playback speed to use, when it is not overridden.
+            playback_speed: f64 =: 2.7,
+
+            /// The subtitle langs to download, when it is not overridden.
+            subtitle_langs: String =: String::new(),
+        },
+        watch: WatchConfig = {
+            /// How many chars to display at most, when displaying information on mpv's local on screen
+            /// display.
+            local_displays_length: usize =: 1000,
+
+            /// How long to wait between saving the video watch progress.
+            progress_save_intervall: Duration =: Duration::from_secs(10),
+        },
+        commands: CommandsConfig = {
+            /// Which command to execute, when showing the thumbnail.
+            ///
+            /// This command will be executed with the one argument, being the path to the image file to display.
+            image_show: NonEmptyVec<String> =: non_empty_vec!["imv".to_owned()],
+
+            /// Which command to use, when spawing one of the external commands (e.g.
+            /// `yt-comments-external` from mpv).
+            ///
+            /// The command will be called with a series of args that should be executed.
+            /// For example,
+            /// `<your_specified_command> <path_to_yt_binary> --db-path <path_to_current_db_path> comments`
+            external_spawn: NonEmptyVec<String> =: non_empty_vec!["alacritty".to_owned(), "-e".to_owned()],
+
+            /// Which command to use, when opening video urls (like in the `yt select url` case).
+            ///
+            /// This command will be called with one argument, being the url of the video to open.
+            url_opener: NonEmptyVec<String> =: non_empty_vec!["firefox".to_owned()],
+        },
+        paths: PathsConfig = {
+            /// Where to store downloaded files.
+            download_dir: PathBuf =: {
+                // We download to the temp dir to avoid taxing the disk
+                let temp_dir = std::env::temp_dir();
+
+                temp_dir.join(PREFIX)
+            } => ensure_dir,
+
+            /// Path to the mpv configuration file.
+            mpv_config_path: PathBuf =? get_config_path("mpv.conf") => ensure_parent_dir,
+
+            /// Path to the mpv input configuration file.
+            mpv_input_path: PathBuf =? get_config_path("mpv.input.conf") => ensure_parent_dir,
+
+            /// Which path to use for mpv ipc socket creation.
+            mpv_ipc_socket_path: PathBuf =? get_runtime_path("mpv.ipc.socket") => ensure_parent_dir,
+
+            /// Path to the video database.
+            database_path: PathBuf where db_path: Option<PathBuf> =! {|config_value: Option<PathBuf>| {
+                db_path.map_or_else(|| config_value.map_or_else(|| get_data_path("videos.sqlite"), Ok), Ok)
+            }} => ensure_parent_dir,
+
+            /// Where to store the selection file before applying it.
+            last_selection_path: PathBuf =? get_runtime_path("selected.yts") => ensure_parent_dir,
+        },
+        download: DownloadConfig = {
+            /// The maximum cache size.
+            max_cache_size: Bytes =? "3 GiB".parse(),
+        },
+        update: UpdateConfig = {
+            /// How many videos to download, when checking for new ones.
+            max_backlog: usize =: 20,
+
+            /// How many threads to use in the thread pool for fetching new videos.
+            pool_size: usize =: 16,
+
+            /// How many subscriptions to fetch at once.
+            ///
+            /// For example, 16 means, that we will fetch 16 subscriptions at the same time.
+            futures: usize =: 16 * 4,
+        },
+    }
+}
diff --git a/crates/yt/src/config/non_empty_vec.rs b/crates/yt/src/config/non_empty_vec.rs
new file mode 100644
index 0000000..bd2c5e3
--- /dev/null
+++ b/crates/yt/src/config/non_empty_vec.rs
@@ -0,0 +1,83 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    collections::VecDeque,
+    fmt::{Display, Write},
+};
+
+use anyhow::bail;
+use serde::{Deserialize, Serialize};
+
+macro_rules! non_empty_vec {
+    ($first:expr $(, $($others:expr),+ $(,)?)?) => {{
+        let inner: Vec<_> = vec![$first $(, $($others,)+)?];
+        inner.try_into().expect("Has a first arg")
+    }}
+}
+pub(crate) use non_empty_vec;
+
+/// A vector that is non-empty.
+#[derive(Deserialize, Serialize, Debug, Clone, PartialEq)]
+#[serde(try_from = "Vec<T>")]
+#[serde(into = "Vec<T>")]
+pub(crate) struct NonEmptyVec<T: Clone> {
+    first: T,
+    rest: Vec<T>,
+}
+
+impl<T: Clone> TryFrom<Vec<T>> for NonEmptyVec<T> {
+    type Error = anyhow::Error;
+
+    fn try_from(value: Vec<T>) -> Result<Self, Self::Error> {
+        let mut queue = VecDeque::from(value);
+
+        if let Some(first) = queue.pop_front() {
+            Ok(Self {
+                first,
+                rest: queue.into(),
+            })
+        } else {
+            bail!("You need to have at least one element in a non-empty vector.")
+        }
+    }
+}
+
+impl<T: Clone> From<NonEmptyVec<T>> for Vec<T> {
+    fn from(value: NonEmptyVec<T>) -> Self {
+        let mut base = vec![value.first];
+        base.extend(value.rest);
+        base
+    }
+}
+
+impl<T: Clone> NonEmptyVec<T> {
+    pub(crate) fn first(&self) -> &T {
+        &self.first
+    }
+
+    pub(crate) fn tail(&self) -> &[T] {
+        self.rest.as_ref()
+    }
+
+    pub(crate) fn join(&self, sep: &str) -> String
+    where
+        T: Display,
+    {
+        let mut output = String::new();
+        write!(output, "{}", self.first()).expect("In-memory, does not fail");
+
+        for elem in &self.rest {
+            write!(output, "{sep}{elem}").expect("In-memory, does not fail");
+        }
+
+        output
+    }
+}
diff --git a/crates/yt/src/config/paths.rs b/crates/yt/src/config/paths.rs
new file mode 100644
index 0000000..66975dd
--- /dev/null
+++ b/crates/yt/src/config/paths.rs
@@ -0,0 +1,58 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::path::{Path, PathBuf};
+
+use anyhow::{Context, Result};
+
+pub(super) fn get_runtime_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
+    xdg_dirs
+        .place_runtime_file(name)
+        .with_context(|| format!("Failed to place runtime file: '{name}'"))
+}
+pub(super) fn get_data_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
+    xdg_dirs
+        .place_data_file(name)
+        .with_context(|| format!("Failed to place data file: '{name}'"))
+}
+pub(super) fn get_config_path(name: &'static str) -> Result<PathBuf> {
+    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX);
+    xdg_dirs
+        .place_config_file(name)
+        .with_context(|| format!("Failed to place config file: '{name}'"))
+}
+
+pub(super) fn ensure_parent_dir(path: &Path) -> Result<()> {
+    if !path.exists() {
+        if let Some(parent) = path.parent() {
+            std::fs::create_dir_all(parent)
+                .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
+        }
+    }
+
+    Ok(())
+}
+pub(super) fn ensure_dir(path: &Path) -> Result<()> {
+    if !path.exists() {
+        std::fs::create_dir_all(path)
+            .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
+    }
+
+    Ok(())
+}
+
+pub(super) fn config_path() -> Result<PathBuf> {
+    get_config_path("config.toml")
+}
+
+pub(crate) const PREFIX: &str = "yt";
diff --git a/crates/yt/src/config/support.rs b/crates/yt/src/config/support.rs
new file mode 100644
index 0000000..96e7ba4
--- /dev/null
+++ b/crates/yt/src/config/support.rs
@@ -0,0 +1,161 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+macro_rules! mk_config {
+    (
+        $(use $usage_path:path;)*
+
+        struct $name:ident {
+            $(
+              $(#[$attr0:meta])*
+              $subconfig_name:ident : $subconfig_type:ident = {
+                $(
+                    $(#[$attr1:meta])*
+                    $field_name:ident : $field_type:ty $(
+                        where $extra_input:ident: $extra_input_type:ty
+                    ),* = $errors:tt  $default:expr $(=> $finalizer:ident)?
+                ),*
+                $(,)?
+              }
+            ),*
+            $(,)?
+        }
+    ) => {
+        mod _inner {
+            #![allow(non_snake_case)]
+
+            $(use $usage_path;)*
+
+            #[derive(serde::Serialize, Debug)]
+            pub(crate) struct $name {
+                $(
+                    $(#[$attr0])*
+                    pub(crate) $subconfig_name: $subconfig_type
+                ),*
+            }
+
+            #[derive(Debug, serde::Deserialize, PartialEq)]
+            #[serde(deny_unknown_fields)]
+            #[allow(non_camel_case_types)]
+            struct config {
+                $(
+                    $subconfig_name: Option<$subconfig_name>
+                ),*
+            }
+
+            impl $name {
+                pub(crate) fn from_config_file(
+                    config_file_path: Option<std::path::PathBuf>,
+                    $(
+                        $(
+                            $(
+                                $extra_input: $extra_input_type,
+                            )*
+                        )*
+                    )*
+                ) -> anyhow::Result<Self> {
+                    use anyhow::Context;
+
+                    let config_file_path =
+                        config_file_path.map_or_else(|| -> anyhow::Result<_> { super::paths::config_path() }, Ok)?;
+
+                    let config: config =
+                        toml::from_str(&std::fs::read_to_string(config_file_path).unwrap_or(String::new()))
+                            .context("Failed to parse the config file as toml")?;
+
+                    Ok(Self {
+                        $(
+                            $subconfig_name: {
+                                let toplevel = config.$subconfig_name.unwrap_or_default();
+                                $subconfig_type {
+                                    $(
+                                        $field_name: $field_name(toplevel.$field_name, $($extra_input),*)?
+                                    ),*
+                                }
+                            }
+                        ),*
+                    })
+                }
+
+                pub(crate) fn run_finalizers(&self) -> anyhow::Result<()> {
+                    #[allow(unused_imports)]
+                    use anyhow::Context;
+
+                    $(
+                        $(
+                            $(
+                                $finalizer(&self.$subconfig_name.$field_name)
+                                  .context(
+                                        concat!(
+                                            "While running the finalizer for config value '",
+                                            stringify!($subconfig_name),
+                                            ".",
+                                            stringify!($field_name),
+                                            "'"
+                                        )
+                                  )?;
+                            )?
+                        )*
+                    )*
+
+                    Ok(())
+                }
+            }
+
+            $(
+                #[derive(serde::Serialize, Debug)]
+                pub(crate) struct $subconfig_type {
+                    $(
+                        $(#[$attr1])*
+                        pub(crate) $field_name: $field_type
+                    ),*
+                }
+
+                #[derive(Debug, Default, serde::Deserialize, PartialEq)]
+                #[serde(deny_unknown_fields)]
+                #[allow(non_camel_case_types)]
+                struct $subconfig_name {
+                    $(
+                        $field_name: Option<$field_type>
+                    ),*
+                }
+
+                $(
+                    fn $field_name(
+                        config_value: Option<$field_type>,
+                        $($extra_input: $extra_input_type),*
+                    ) -> anyhow::Result<$field_type> {
+                        use anyhow::Context;
+
+                        let expr = $crate::config::support::maybe_wrap_type!($field_type =$errors $default)(config_value);
+
+                        expr.context(concat!("Failed to generate default config value for '", stringify!($field_name),"'"))
+                    }
+                )*
+            )*
+        }
+        pub(crate) use self::_inner::*;
+    };
+}
+
+macro_rules! maybe_wrap_type {
+    ($ty:ty =! $val:expr) => {
+        (|config_value: Option<$ty>| $val(config_value))
+    };
+    ($ty:ty =? $val:expr) => {
+        (|config_value: Option<$ty>| config_value.map_or_else(|| $val, Ok))
+    };
+    ($ty:ty =: $val:expr) => {
+        (|config_value: Option<$ty>| Ok::<_, anyhow::Error>(config_value.unwrap_or_else(|| $val)))
+    };
+}
+
+pub(crate) use maybe_wrap_type;
+pub(crate) use mk_config;
diff --git a/crates/yt/src/main.rs b/crates/yt/src/main.rs
new file mode 100644
index 0000000..705e642
--- /dev/null
+++ b/crates/yt/src/main.rs
@@ -0,0 +1,89 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+// `yt` is not a library. Besides, the `anyhow::Result` type is really useless, if you're not going
+// to print it anyways.
+#![allow(clippy::missing_errors_doc, clippy::missing_panics_doc)]
+
+use anyhow::{Context, Result};
+use app::App;
+use clap::{CommandFactory, Parser};
+use config::Config;
+use log::info;
+
+use crate::commands::Command;
+
+pub(crate) mod output;
+pub(crate) mod yt_dlp;
+
+pub(crate) mod ansi_escape_codes;
+pub(crate) mod app;
+pub(crate) mod cli;
+pub(crate) mod commands;
+pub(crate) mod shared;
+
+pub(crate) mod config;
+pub(crate) mod select;
+pub(crate) mod storage;
+pub(crate) mod version;
+pub(crate) mod videos;
+
+#[tokio::main]
+async fn main() -> Result<()> {
+    clap_complete::CompleteEnv::with_factory(cli::CliArgs::command).complete();
+
+    let args = cli::CliArgs::parse();
+
+    // The default verbosity is 1 (Warn)
+    let verbosity: u8 = args.verbosity + 1;
+
+    stderrlog::new()
+        .module(module_path!())
+        .modules(&["yt_dlp".to_owned(), "libmpv2".to_owned()])
+        .quiet(args.quiet)
+        .show_module_names(false)
+        .color(stderrlog::ColorChoice::Auto)
+        .verbosity(verbosity as usize)
+        .timestamp(stderrlog::Timestamp::Off)
+        .init()
+        .expect("Let's just hope that this does not panic");
+
+    info!("Using verbosity level: '{} ({})'", verbosity, {
+        match verbosity {
+            0 => "Error",
+            1 => "Warn",
+            2 => "Info",
+            3 => "Debug",
+            4.. => "Trace",
+        }
+    });
+
+    let config = Config::from_config_file(args.config_path, args.color, args.db_path)?;
+    if args.version {
+        version::show(&config).await?;
+        return Ok(());
+    }
+
+    // Perform config finalization _after_ checking for the version
+    // so that version always works.
+    config
+        .run_finalizers()
+        .context("Failed to finalize config for usage")?;
+
+    let app = App::new(config, !args.no_migrate_db).await?;
+
+    args.command
+        .unwrap_or(Command::default())
+        .implm(app)
+        .await?;
+
+    Ok(())
+}
diff --git a/yt/src/comments/output.rs b/crates/yt/src/output/mod.rs
index cb3a9c4..2f74519 100644
--- a/yt/src/comments/output.rs
+++ b/crates/yt/src/output/mod.rs
@@ -17,9 +17,7 @@ use std::{
 use anyhow::{Context, Result};
 use uu_fmt::{FmtOptions, process_text};
 
-use crate::unreachable::Unreachable;
-
-pub async fn display_fmt_and_less(input: String) -> Result<()> {
+pub(crate) fn display_less(input: String) -> Result<()> {
     let mut less = Command::new("less")
         .args(["--raw-control-chars"])
         .stdin(Stdio::piped())
@@ -27,12 +25,11 @@ pub async fn display_fmt_and_less(input: String) -> Result<()> {
         .spawn()
         .context("Failed to run less")?;
 
-    let input = format_text(&input);
     let mut stdin = less.stdin.take().context("Failed to open stdin")?;
     std::thread::spawn(move || {
         stdin
             .write_all(input.as_bytes())
-            .unreachable("Should be able to write to the stdin of less");
+            .expect("Should be able to write to the stdin of less");
     });
 
     let _ = less.wait().context("Failed to await less")?;
@@ -40,9 +37,15 @@ pub async fn display_fmt_and_less(input: String) -> Result<()> {
     Ok(())
 }
 
+pub(crate) fn display_fmt_and_less(input: &str) -> Result<()> {
+    display_less(format_text(&input, None))
+}
+
 #[must_use]
-pub fn format_text(input: &str) -> String {
-    let width = termsize::get().map_or(90, |size| size.cols);
+pub(crate) fn format_text(input: &str, termsize: Option<u16>) -> String {
+    let input = input.trim();
+
+    let width = termsize.unwrap_or_else(|| termsize::get().map_or(90, |size| size.cols));
     let fmt_opts = FmtOptions {
         uniform: true,
         split_only: true,
diff --git a/crates/yt/src/select/duration.rs b/crates/yt/src/select/duration.rs
new file mode 100644
index 0000000..f1de2ea
--- /dev/null
+++ b/crates/yt/src/select/duration.rs
@@ -0,0 +1,240 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::str::FromStr;
+use std::time::Duration;
+
+use anyhow::{Result, bail};
+
+const SECOND: u64 = 1;
+const MINUTE: u64 = 60 * SECOND;
+const HOUR: u64 = 60 * MINUTE;
+const DAY: u64 = 24 * HOUR;
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
+pub(crate) struct MaybeDuration {
+    time: Option<Duration>,
+}
+
+impl MaybeDuration {
+    #[must_use]
+    pub(crate) fn from_std(d: Duration) -> Self {
+        Self { time: Some(d) }
+    }
+
+    #[must_use]
+    pub(crate) fn from_secs_f64(d: f64) -> Self {
+        Self {
+            time: Some(Duration::from_secs_f64(d)),
+        }
+    }
+    #[must_use]
+    pub(crate) fn from_maybe_secs_f64(d: Option<f64>) -> Self {
+        Self {
+            time: d.map(Duration::from_secs_f64),
+        }
+    }
+    #[must_use]
+    #[cfg(test)]
+    pub(crate) fn from_secs(d: u64) -> Self {
+        Self {
+            time: Some(Duration::from_secs(d)),
+        }
+    }
+
+    /// Try to return the current duration encoded as seconds.
+    #[must_use]
+    pub(crate) fn as_secs(&self) -> Option<u64> {
+        self.time.map(|v| v.as_secs())
+    }
+
+    /// Try to return the current duration encoded as seconds and nanoseconds.
+    #[must_use]
+    pub(crate) fn as_secs_f64(&self) -> Option<f64> {
+        self.time.map(|v| v.as_secs_f64())
+    }
+}
+
+impl FromStr for MaybeDuration {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> Result<Self, Self::Err> {
+        #[derive(Debug, Clone, Copy)]
+        enum Token {
+            Number(u64),
+            UnitConstant((char, u64)),
+        }
+
+        struct Tokenizer<'a> {
+            input: &'a str,
+        }
+
+        impl Tokenizer<'_> {
+            fn next(&mut self) -> Result<Option<Token>> {
+                loop {
+                    if let Some(next) = self.peek() {
+                        match next {
+                            '0'..='9' => {
+                                let mut number = self.expect_num();
+                                while matches!(self.peek(), Some('0'..='9')) {
+                                    number *= 10;
+                                    number += self.expect_num();
+                                }
+                                break Ok(Some(Token::Number(number)));
+                            }
+                            's' => {
+                                self.chomp();
+                                break Ok(Some(Token::UnitConstant(('s', SECOND))));
+                            }
+                            'm' => {
+                                self.chomp();
+                                break Ok(Some(Token::UnitConstant(('m', MINUTE))));
+                            }
+                            'h' => {
+                                self.chomp();
+                                break Ok(Some(Token::UnitConstant(('h', HOUR))));
+                            }
+                            'd' => {
+                                self.chomp();
+                                break Ok(Some(Token::UnitConstant(('d', DAY))));
+                            }
+                            ' ' => {
+                                // Simply ignore white space
+                                self.chomp();
+                            }
+                            other => bail!("Unknown unit: {other:#?}"),
+                        }
+                    } else {
+                        break Ok(None);
+                    }
+                }
+            }
+
+            fn chomp(&mut self) {
+                self.input = &self.input[1..];
+            }
+
+            fn peek(&self) -> Option<char> {
+                self.input.chars().next()
+            }
+
+            fn expect_num(&mut self) -> u64 {
+                let next = self.peek().expect("Should be some at this point");
+                self.chomp();
+                assert!(next.is_ascii_digit());
+                (next as u64) - ('0' as u64)
+            }
+        }
+
+        if s == "[No duration]" {
+            return Ok(Self { time: None });
+        }
+
+        let mut tokenizer = Tokenizer { input: s };
+
+        let mut value = 0;
+        let mut current_val = None;
+        while let Some(token) = tokenizer.next()? {
+            match token {
+                Token::Number(number) => {
+                    if let Some(current_val) = current_val {
+                        bail!("Failed to find unit for number: {current_val}");
+                    }
+
+                    {
+                        current_val = Some(number);
+                    }
+                }
+                Token::UnitConstant((name, unit)) => {
+                    if let Some(cval) = current_val {
+                        value += cval * unit;
+                        current_val = None;
+                    } else {
+                        bail!("Found unit without number: {name:#?}");
+                    }
+                }
+            }
+        }
+
+        if let Some(current_val) = current_val {
+            bail!("Duration endet without unit, number was: {current_val}");
+        }
+
+        Ok(Self {
+            time: Some(Duration::from_secs(value)),
+        })
+    }
+}
+
+impl std::fmt::Display for MaybeDuration {
+    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
+        if let Some(self_seconds) = self.as_secs() {
+            let base_day = self_seconds - (self_seconds % DAY);
+            let base_hour = (self_seconds % DAY) - ((self_seconds % DAY) % HOUR);
+            let base_min = (self_seconds % HOUR) - (((self_seconds % DAY) % HOUR) % MINUTE);
+            let base_sec = ((self_seconds % DAY) % HOUR) % MINUTE;
+
+            let d = base_day / DAY;
+            let h = base_hour / HOUR;
+            let m = base_min / MINUTE;
+            let s = base_sec / SECOND;
+
+            if d > 0 {
+                write!(fmt, "{d}d {h}h {m}m")
+            } else if h > 0 {
+                write!(fmt, "{h}h {m}m")
+            } else {
+                write!(fmt, "{m}m {s}s")
+            }
+        } else {
+            write!(fmt, "[No duration]")
+        }
+    }
+}
+#[cfg(test)]
+mod test {
+    use std::str::FromStr;
+
+    use crate::select::duration::{DAY, HOUR, MINUTE};
+
+    use super::MaybeDuration;
+
+    fn mk_roundtrip(input: MaybeDuration, expected: &str) {
+        let output = MaybeDuration::from_str(expected).unwrap();
+
+        assert_eq!(input.to_string(), output.to_string());
+        assert_eq!(input.to_string(), expected);
+        assert_eq!(
+            MaybeDuration::from_str(input.to_string().as_str()).unwrap(),
+            output
+        );
+    }
+
+    #[test]
+    fn test_roundtrip_duration_1h() {
+        mk_roundtrip(MaybeDuration::from_secs(HOUR), "1h 0m");
+    }
+    #[test]
+    fn test_roundtrip_duration_30min() {
+        mk_roundtrip(MaybeDuration::from_secs(MINUTE * 30), "30m 0s");
+    }
+    #[test]
+    fn test_roundtrip_duration_1d() {
+        mk_roundtrip(
+            MaybeDuration::from_secs(DAY + MINUTE * 30 + HOUR * 2),
+            "1d 2h 30m",
+        );
+    }
+    #[test]
+    fn test_roundtrip_duration_none() {
+        mk_roundtrip(MaybeDuration::from_maybe_secs_f64(None), "[No duration]");
+    }
+}
diff --git a/crates/yt/src/select/mod.rs b/crates/yt/src/select/mod.rs
new file mode 100644
index 0000000..b02677f
--- /dev/null
+++ b/crates/yt/src/select/mod.rs
@@ -0,0 +1,35 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+pub(crate) mod duration;
+
+// // FIXME: There should be no reason why we need to re-run yt, just to get the help string. But I've
+// // yet to find a way to do it without the extra exec <2024-08-20>
+// async fn get_help() -> Result<String> {
+//     let binary_name = current_exe()?;
+//     let cmd = Command::new(binary_name)
+//         .args(&["select", "--help"])
+//         .output()
+//         .await?;
+//
+//     assert_eq!(cmd.status.code(), Some(0));
+//
+//     let output = String::from_utf8(cmd.stdout).expect("Our help output was not utf8?");
+//
+//     let out = output
+//         .lines()
+//         .map(|line| format!("# {}\n", line))
+//         .collect::<String>();
+//
+//     debug!("Returning help: '{}'", &out);
+//
+//     Ok(out)
+// }
diff --git a/crates/bytes/src/error.rs b/crates/yt/src/shared/bytes/error.rs
index c9783d8..c9783d8 100644
--- a/crates/bytes/src/error.rs
+++ b/crates/yt/src/shared/bytes/error.rs
diff --git a/crates/bytes/src/lib.rs b/crates/yt/src/shared/bytes/mod.rs
index 2a9248d..31e782e 100644
--- a/crates/bytes/src/lib.rs
+++ b/crates/yt/src/shared/bytes/mod.rs
@@ -16,6 +16,7 @@
 )]
 use std::{fmt::Display, str::FromStr};
 
+use ::serde::{Deserialize, Serialize};
 use error::BytesError;
 
 const B: u64 = 1;
@@ -31,10 +32,11 @@ const MB: u64 = 1000 * KB;
 const GB: u64 = 1000 * MB;
 const TB: u64 = 1000 * GB;
 
-pub mod error;
-pub mod serde;
+pub(crate) mod error;
 
-#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq)]
+#[derive(Clone, Copy, Debug, PartialEq, PartialOrd, Ord, Eq, Deserialize, Serialize)]
+#[serde(try_from = "String")]
+#[serde(into = "String")]
 pub struct Bytes(u64);
 
 impl Bytes {
@@ -131,6 +133,20 @@ impl Display for Bytes {
     }
 }
 
+impl From<Bytes> for String {
+    fn from(value: Bytes) -> Self {
+        value.to_string()
+    }
+}
+
+impl TryFrom<String> for Bytes {
+    type Error = BytesError;
+
+    fn try_from(value: String) -> Result<Self, Self::Error> {
+        value.as_str().parse()
+    }
+}
+
 // taken from this stack overflow question: https://stackoverflow.com/a/76572321
 /// Round to significant digits (rather than digits after the decimal).
 ///
@@ -149,7 +165,7 @@ impl Display for Bytes {
 ///# }
 /// ```
 #[must_use]
-pub fn precision_f64(x: f64, decimals: u32) -> f64 {
+pub(crate) fn precision_f64(x: f64, decimals: u32) -> f64 {
     if x == 0. || decimals == 0 {
         0.
     } else {
diff --git a/crates/yt_dlp/src/wrapper/mod.rs b/crates/yt/src/shared/mod.rs
index 3fe3247..d3cc563 100644
--- a/crates/yt_dlp/src/wrapper/mod.rs
+++ b/crates/yt/src/shared/mod.rs
@@ -1,6 +1,6 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
 // This file is part of Yt.
@@ -8,5 +8,4 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-pub mod info_json;
-// pub mod yt_dlp_options;
+pub(crate) mod bytes;
diff --git a/crates/yt/src/storage/db/extractor_hash.rs b/crates/yt/src/storage/db/extractor_hash.rs
new file mode 100644
index 0000000..3ad8273
--- /dev/null
+++ b/crates/yt/src/storage/db/extractor_hash.rs
@@ -0,0 +1,220 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{collections::HashSet, fmt::Display, str::FromStr};
+
+use anyhow::{Context, Result, bail};
+use blake3::Hash;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use tokio::sync::OnceCell;
+use yt_dlp::{info_json::InfoJson, json_cast, json_get, json_try_get};
+
+use crate::app::App;
+
+static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new();
+
+#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy, Serialize, Deserialize)]
+pub(crate) struct ExtractorHash {
+    hash: Hash,
+}
+
+impl Display for ExtractorHash {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.hash.fmt(f)
+    }
+}
+
+#[derive(Debug, Clone)]
+pub(crate) struct ShortHash(String);
+
+impl Display for ShortHash {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        self.0.fmt(f)
+    }
+}
+
+#[derive(Debug, Clone)]
+#[allow(clippy::module_name_repetitions)]
+pub(crate) struct LazyExtractorHash {
+    value: ShortHash,
+}
+
+impl FromStr for LazyExtractorHash {
+    type Err = anyhow::Error;
+
+    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
+        // perform some cheap validation
+        if s.len() > 64 {
+            bail!("A hash can only contain 64 bytes!");
+        }
+
+        Ok(Self {
+            value: ShortHash(s.to_owned()),
+        })
+    }
+}
+
+impl LazyExtractorHash {
+    /// Turn the [`LazyExtractorHash`] into the [`ExtractorHash`]
+    pub(crate) async fn realize(
+        self,
+        app: &App,
+        all_hashes: Option<&[ExtractorHash]>,
+    ) -> Result<ExtractorHash> {
+        ExtractorHash::from_short_hash(app, &self.value, all_hashes).await
+    }
+}
+
+impl ExtractorHash {
+    #[must_use]
+    pub(crate) fn from_hash(hash: Hash) -> Self {
+        Self { hash }
+    }
+
+    pub(crate) async fn from_short_hash(
+        app: &App,
+        s: &ShortHash,
+        all_hashes: Option<&[Self]>,
+    ) -> Result<Self> {
+        let all_hashes = if let Some(all) = all_hashes {
+            all
+        } else {
+            &Self::get_all(app)
+                .await
+                .context("Failed to fetch all extractor-hashes from the database")?
+        };
+        let needed_chars = s.0.len();
+        for hash in all_hashes {
+            // PERFORMANCE(@bpeetz): This could avoid the string construction and just use a
+            // numeric equality check instead. <2025-07-15>
+            if hash.hash().to_hex()[..needed_chars] == s.0 {
+                return Ok(*hash);
+            }
+        }
+        bail!("Your shortend hash, does not match a real hash (this is probably a bug)!");
+    }
+
+    pub(crate) fn from_info_json(entry: &InfoJson) -> Self {
+        // HACK(@bpeetz): The code that follows is a gross hack.
+        // One would expect the `id` to be unique _and_ constant for each and every possible info JSON.
+        // But .. it's just not. The `ARDMediathek` extractor, will sometimes return different `id`s for the same
+        // video, effectively causing us to insert the same video again into the db (which fails,
+        // because the URL is still unique).
+        //
+        // As such we _should_ probably find a constant value for all extractors, but that just does
+        // not exist currently, without processing each entry (which is expensive and which I would
+        // like to avoid).
+        //
+        // Therefor, we simply special case the `ARDBetaMediathek` extractor. <2025-07-04>
+
+        // NOTE(@bpeetz): `yt-dlp` apparently uses these two different names for the same thing <2025-07-04>
+        let ie_key = {
+            if let Some(ie_key) = json_try_get!(entry, "ie_key", as_str) {
+                ie_key
+            } else if let Some(extractor_key) = json_try_get!(entry, "extractor_key", as_str) {
+                extractor_key
+            } else {
+                unreachable!(
+                    "Either `ie_key` or `extractor_key` \
+                should be set on every entry info json"
+                )
+            }
+        };
+
+        if ie_key == "ARDBetaMediathek" {
+            // NOTE(@bpeetz): The mediathek is changing their Id scheme, from an `short` old Id to the
+            // new id. As the new id is too long for some people, yt-dlp will be default return the old
+            // one (when it is still available!). The new one is called `display_id`.
+            // Therefore, we simply check if the new one is explicitly returned, and otherwise use the
+            // normal `id` value, as these are cases where the old one is no longer available. <2025-07-04>
+            let id = if let Some(display_id) = json_try_get!(entry, "display_id", as_str) {
+                display_id.as_bytes()
+            } else {
+                json_get!(entry, "id", as_str).as_bytes()
+            };
+
+            Self {
+                hash: blake3::hash(id),
+            }
+        } else {
+            Self {
+                hash: blake3::hash(json_get!(entry, "id", as_str).as_bytes()),
+            }
+        }
+    }
+
+    #[must_use]
+    pub(crate) fn hash(&self) -> &Hash {
+        &self.hash
+    }
+
+    pub(crate) async fn as_short_hash(&self, app: &App) -> Result<ShortHash> {
+        let needed_chars = if let Some(needed_chars) = EXTRACTOR_HASH_LENGTH.get() {
+            *needed_chars
+        } else {
+            let needed_chars = self
+                .get_needed_char_len(app)
+                .await
+                .context("Failed to calculate needed char length")?;
+            EXTRACTOR_HASH_LENGTH
+                .set(needed_chars)
+                .expect("This should work at this stage, as we checked above that it is empty.");
+
+            needed_chars
+        };
+
+        Ok(ShortHash(
+            self.hash()
+                .to_hex()
+                .chars()
+                .take(needed_chars)
+                .collect::<String>(),
+        ))
+    }
+
+    async fn get_needed_char_len(&self, app: &App) -> Result<usize> {
+        debug!("Calculating the needed hash char length");
+        let all_hashes = Self::get_all(app)
+            .await
+            .context("Failed to fetch all extractor -hashesh from database")?;
+
+        let all_char_vec_hashes = all_hashes
+            .into_iter()
+            .map(|hash| hash.hash().to_hex().chars().collect::<Vec<char>>())
+            .collect::<Vec<Vec<_>>>();
+
+        // This value should be updated later, if not rust will panic in the assertion.
+        let mut needed_chars: usize = 1000;
+        'outer: for i in 1..64 {
+            let i_chars: Vec<String> = all_char_vec_hashes
+                .iter()
+                .map(|vec| vec.iter().take(i).collect::<String>())
+                .collect();
+
+            let mut uniqnes_hashmap: HashSet<String> = HashSet::new();
+            for ch in i_chars {
+                if !uniqnes_hashmap.insert(ch) {
+                    // The key was already in the hash map, thus we have a duplicated char and need
+                    // at least one char more
+                    continue 'outer;
+                }
+            }
+
+            needed_chars = i;
+            break 'outer;
+        }
+
+        assert!(needed_chars <= 64, "Hashes are only 64 bytes long");
+
+        Ok(needed_chars)
+    }
+}
diff --git a/crates/yt/src/storage/db/get/extractor_hash.rs b/crates/yt/src/storage/db/get/extractor_hash.rs
new file mode 100644
index 0000000..c8e150a
--- /dev/null
+++ b/crates/yt/src/storage/db/get/extractor_hash.rs
@@ -0,0 +1,68 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use anyhow::Result;
+use blake3::Hash;
+use sqlx::{SqliteConnection, query};
+
+use crate::{
+    app::App,
+    storage::db::{
+        extractor_hash::ExtractorHash,
+        video::{Video, video_from_record},
+    },
+};
+
+impl ExtractorHash {
+    pub(crate) async fn get(&self, txn: &mut SqliteConnection) -> Result<Video> {
+        let extractor_hash = self.hash().to_string();
+
+        let base = query!(
+            r#"
+            SELECT *
+            FROM videos
+            WHERE extractor_hash = ?
+            "#,
+            extractor_hash
+        )
+        .fetch_one(txn)
+        .await?;
+
+        Ok(video_from_record!(base))
+    }
+
+    pub(crate) async fn get_with_app(&self, app: &App) -> Result<Video> {
+        let mut txn = app.database.begin().await?;
+        let out = self.get(&mut txn).await?;
+        txn.commit().await?;
+
+        Ok(out)
+    }
+
+    pub(crate) async fn get_all(app: &App) -> Result<Vec<Self>> {
+        let hashes_hex = query!(
+            r#"
+        SELECT extractor_hash
+        FROM videos;
+        "#
+        )
+        .fetch_all(&app.database)
+        .await?;
+
+        Ok(hashes_hex
+            .iter()
+            .map(|hash| {
+                Self::from_hash(Hash::from_hex(&hash.extractor_hash).expect(
+                    "These values started as blake3 hashes, they should stay blake3 hashes",
+                ))
+            })
+            .collect())
+    }
+}
diff --git a/crates/yt/src/storage/db/get/mod.rs b/crates/yt/src/storage/db/get/mod.rs
new file mode 100644
index 0000000..4bcd066
--- /dev/null
+++ b/crates/yt/src/storage/db/get/mod.rs
@@ -0,0 +1,15 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+pub(crate) mod extractor_hash;
+pub(crate) mod playlist;
+pub(crate) mod subscription;
+pub(crate) mod txn_log;
+pub(crate) mod video;
diff --git a/crates/yt/src/storage/db/get/playlist.rs b/crates/yt/src/storage/db/get/playlist.rs
new file mode 100644
index 0000000..5094523
--- /dev/null
+++ b/crates/yt/src/storage/db/get/playlist.rs
@@ -0,0 +1,68 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    storage::db::{
+        playlist::{Playlist, PlaylistIndex},
+        video::{Video, VideoStatusMarker},
+    },
+};
+
+use anyhow::Result;
+
+impl Playlist {
+    /// Get an video based in its index.
+    #[must_use]
+    pub(crate) fn get_mut(&mut self, index: PlaylistIndex) -> Option<&mut Video> {
+        self.videos.get_mut(Into::<usize>::into(index))
+    }
+
+    /// Create a playlist, by loading it from the database.
+    pub(crate) async fn create(app: &App) -> Result<Self> {
+        let videos = Video::in_states(app, &[VideoStatusMarker::Cached]).await?;
+
+        Ok(Self { videos })
+    }
+
+    /// Return the current playlist index.
+    ///
+    /// This effectively looks for the currently focused video and returns it's index.
+    ///
+    /// # Panics
+    /// Only if internal assertions fail.
+    pub(crate) fn current_index(&self) -> Option<PlaylistIndex> {
+        if let Some((index, _)) = self.get_focused() {
+            Some(index)
+        } else {
+            None
+        }
+    }
+
+    /// Get the currently focused video, if it exists.
+    #[must_use]
+    pub(crate) fn get_focused_mut(&mut self) -> Option<(PlaylistIndex, &mut Video)> {
+        self.videos
+            .iter_mut()
+            .enumerate()
+            .find(|(_, v)| v.is_focused())
+            .map(|(index, video)| (PlaylistIndex::from(index), video))
+    }
+
+    /// Get the currently focused video, if it exists.
+    #[must_use]
+    pub(crate) fn get_focused(&self) -> Option<(PlaylistIndex, &Video)> {
+        self.videos
+            .iter()
+            .enumerate()
+            .find(|(_, v)| v.is_focused())
+            .map(|(index, video)| (PlaylistIndex::from(index), video))
+    }
+}
diff --git a/crates/yt/src/storage/db/get/subscription.rs b/crates/yt/src/storage/db/get/subscription.rs
new file mode 100644
index 0000000..16a6e8b
--- /dev/null
+++ b/crates/yt/src/storage/db/get/subscription.rs
@@ -0,0 +1,49 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::collections::HashMap;
+
+use crate::{
+    app::App,
+    storage::db::subscription::{Subscription, Subscriptions},
+};
+
+use anyhow::Result;
+use sqlx::query;
+use url::Url;
+
+impl Subscriptions {
+    /// Get a list of subscriptions
+    pub(crate) async fn get(app: &App) -> Result<Self> {
+        let raw_subs = query!(
+            "
+        SELECT *
+        FROM subscriptions;
+        "
+        )
+        .fetch_all(&app.database)
+        .await?;
+
+        let subscriptions: HashMap<String, Subscription> = raw_subs
+            .into_iter()
+            .map(|sub| {
+                (
+                    sub.name.clone(),
+                    Subscription::new(
+                        sub.name,
+                        Url::parse(&sub.url).expect("It was an URL, when we inserted it."),
+                    ),
+                )
+            })
+            .collect();
+
+        Ok(Subscriptions(subscriptions))
+    }
+}
diff --git a/crates/yt/src/storage/db/get/txn_log.rs b/crates/yt/src/storage/db/get/txn_log.rs
new file mode 100644
index 0000000..1a6df2c
--- /dev/null
+++ b/crates/yt/src/storage/db/get/txn_log.rs
@@ -0,0 +1,43 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    storage::db::{insert::Committable, txn_log::TxnLog, video::TimeStamp},
+};
+
+use anyhow::Result;
+use sqlx::query;
+
+impl<O: Committable> TxnLog<O> {
+    /// Get the log of all operations that have been performed.
+    pub(crate) async fn get(app: &App) -> Result<Self> {
+        let raw_ops = query!(
+            "
+        SELECT *
+        FROM txn_log
+        ORDER BY timestamp ASC;
+        "
+        )
+        .fetch_all(&app.database)
+        .await?;
+
+        let inner = raw_ops
+            .into_iter()
+            .filter_map(|raw_op| {
+                serde_json::from_str(&raw_op.operation)
+                    .map(|parsed_op| (TimeStamp::from_secs(raw_op.timestamp), parsed_op))
+                    .ok()
+            })
+            .collect();
+
+        Ok(TxnLog::new(inner))
+    }
+}
diff --git a/crates/yt/src/storage/db/get/video/mod.rs b/crates/yt/src/storage/db/get/video/mod.rs
new file mode 100644
index 0000000..69adb6b
--- /dev/null
+++ b/crates/yt/src/storage/db/get/video/mod.rs
@@ -0,0 +1,261 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{fs::File, path::PathBuf};
+
+use anyhow::{Context, Result, bail};
+use log::debug;
+use sqlx::query;
+use yt_dlp::{info_json::InfoJson, json_cast, json_try_get};
+
+use crate::{
+    app::App,
+    storage::db::video::{
+        Video, VideoStatus, VideoStatusMarker,
+        comments::{Comments, raw::RawComment},
+        video_from_record,
+    },
+};
+
+impl Video {
+    /// Returns to next video which should be downloaded. This respects the priority assigned by select.
+    /// It does not return videos, which are already downloaded.
+    ///
+    /// # Panics
+    /// Only if assertions fail.
+    pub(crate) async fn next_to_download(app: &App) -> Result<Option<Self>> {
+        let status = VideoStatus::Watch.as_marker().as_db_integer();
+
+        // NOTE: The ORDER BY statement should be the same as the one in [`in_states`]. <2024-08-22>
+        let result = query!(
+            r#"
+        SELECT *
+        FROM videos
+        WHERE status = ? AND cache_path IS NULL
+        ORDER BY priority DESC, publish_date DESC
+        LIMIT 1;
+    "#,
+            status
+        )
+        .fetch_one(&app.database)
+        .await;
+
+        if let Err(sqlx::Error::RowNotFound) = result {
+            Ok(None)
+        } else {
+            let base = result?;
+
+            Ok(Some(video_from_record!(base)))
+        }
+    }
+
+    /// Returns the description of the current video.
+    /// The returned description will be set to `<No description>` in the absence of one.
+    ///
+    /// # Errors
+    /// If no current video exists.
+    ///
+    /// # Panics
+    /// If the current video lacks the `info.json` file.
+    pub(crate) async fn get_current_description(app: &App) -> Result<String> {
+        let Some(currently_playing_video) = Video::currently_focused(app).await? else {
+            bail!("Could not find a currently playing video!");
+        };
+
+        let info_json = &currently_playing_video.get_info_json()?.expect(
+            "A currently *playing* must be cached. \
+                And thus the info.json should be available.",
+        );
+
+        let description = json_try_get!(info_json, "description", as_str)
+            .unwrap_or("<No description>")
+            .to_owned();
+
+        Ok(description)
+    }
+
+    /// Returns the comments of the current video.
+    /// The returned [`Comments`] will be empty in the absence of comments.
+    ///
+    /// # Errors
+    /// If no current video exists.
+    ///
+    /// # Panics
+    /// If the current video lacks the `info.json` file.
+    pub(crate) async fn get_current_comments(app: &App) -> Result<Comments> {
+        let Some(currently_playing_video) = Video::currently_focused(app).await? else {
+            bail!("Could not find a currently playing video!");
+        };
+
+        let info_json = &currently_playing_video.get_info_json()?.expect(
+            "A currently *playing* video must be cached. \
+                And thus the info.json should be available.",
+        );
+
+        let raw_comments = if let Some(comments) = json_try_get!(info_json, "comments", as_array) {
+            comments
+                .iter()
+                .cloned()
+                .map(serde_json::from_value)
+                .collect::<Result<Vec<RawComment>, _>>()?
+        } else {
+            // TODO(@bpeetz): We could display a `<No-comments>` here. <2025-07-15>
+
+            bail!(
+                "The video ('{}') does not have comments!",
+                json_try_get!(info_json, "title", as_str).unwrap_or("<No Title>")
+            )
+        };
+
+        Ok(Comments::from_raw(raw_comments))
+    }
+
+    /// Optionally returns the video that is currently focused.
+    ///
+    /// # Panics
+    /// Only if assertions fail.
+    pub(crate) async fn currently_focused(app: &App) -> Result<Option<Self>> {
+        let status = VideoStatusMarker::Cached.as_db_integer();
+
+        let result = query!(
+            r#"
+            SELECT *
+            FROM videos
+            WHERE status = ? AND is_focused = 1
+            "#,
+            status
+        )
+        .fetch_one(&app.database)
+        .await;
+
+        if let Err(sqlx::Error::RowNotFound) = result {
+            Ok(None)
+        } else {
+            let base = result?;
+
+            Ok(Some(video_from_record!(base)))
+        }
+    }
+
+    /// Calculate the [`info_json`] location on-disk for this video.
+    ///
+    /// Will return [`None`], if the video does not have an downloaded [`info_json`]
+    pub(crate) fn info_json_path(&self) -> Result<Option<PathBuf>> {
+        if let VideoStatus::Cached { mut cache_path, .. } = self.status.clone() {
+            if !cache_path.set_extension("info.json") {
+                bail!(
+                    "Failed to change path extension to 'info.json': {}",
+                    cache_path.display()
+                );
+            }
+
+            Ok(Some(cache_path))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Fetch the [`info_json`], downloaded on-disk for this video.
+    ///
+    /// Will return [`None`], if the video does not have an downloaded [`info_json`]
+    pub(crate) fn get_info_json(&self) -> Result<Option<InfoJson>> {
+        if let Some(path) = self.info_json_path()? {
+            let info_json_string = File::open(path)?;
+            let info_json = serde_json::from_reader(&info_json_string)?;
+
+            Ok(Some(info_json))
+        } else {
+            Ok(None)
+        }
+    }
+
+    /// Returns this videos `is_focused` flag if it is set.
+    ///
+    /// Will return `false` for not-downloaded videos.
+    pub(crate) fn is_focused(&self) -> bool {
+        if let VideoStatus::Cached { is_focused, .. } = &self.status {
+            *is_focused
+        } else {
+            false
+        }
+    }
+
+    /// Returns the videos that are in the `allowed_states`.
+    ///
+    /// # Panics
+    /// Only, if assertions fail.
+    pub(crate) async fn in_states(
+        app: &App,
+        allowed_states: &[VideoStatusMarker],
+    ) -> Result<Vec<Video>> {
+        fn test(all_states: &[VideoStatusMarker], check: VideoStatusMarker) -> Option<i64> {
+            if all_states.contains(&check) {
+                Some(check.as_db_integer())
+            } else {
+                None
+            }
+        }
+        fn states_to_string(allowed_states: &[VideoStatusMarker]) -> String {
+            let mut states = allowed_states
+                .iter()
+                .fold(String::from("&["), |mut acc, state| {
+                    acc.push_str(state.as_str());
+                    acc.push_str(", ");
+                    acc
+                });
+            states = states.trim().to_owned();
+            states = states.trim_end_matches(',').to_owned();
+            states.push(']');
+            states
+        }
+
+        debug!(
+            "Fetching videos in the states: '{}'",
+            states_to_string(allowed_states)
+        );
+        let active_pick: Option<i64> = test(allowed_states, VideoStatusMarker::Pick);
+        let active_watch: Option<i64> = test(allowed_states, VideoStatusMarker::Watch);
+        let active_cached: Option<i64> = test(allowed_states, VideoStatusMarker::Cached);
+        let active_watched: Option<i64> = test(allowed_states, VideoStatusMarker::Watched);
+        let active_drop: Option<i64> = test(allowed_states, VideoStatusMarker::Drop);
+        let active_dropped: Option<i64> = test(allowed_states, VideoStatusMarker::Dropped);
+
+        // NOTE: The ORDER BY statement should be the same as the one in [`next_to_download`]. <2024-08-22>
+        let videos = query!(
+            r"
+          SELECT *
+          FROM videos
+          WHERE status IN (?,?,?,?,?,?)
+          ORDER BY priority DESC, publish_date DESC;
+          ",
+            active_pick,
+            active_watch,
+            active_cached,
+            active_watched,
+            active_drop,
+            active_dropped,
+        )
+        .fetch_all(&app.database)
+        .await
+        .with_context(|| {
+            format!(
+                "Failed to query videos with states: '{}'",
+                states_to_string(allowed_states)
+            )
+        })?;
+
+        let real_videos: Vec<Video> = videos
+            .iter()
+            .map(|base| -> Video { video_from_record!(base) })
+            .collect();
+
+        Ok(real_videos)
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/maintenance.rs b/crates/yt/src/storage/db/insert/maintenance.rs
new file mode 100644
index 0000000..d87c1ae
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/maintenance.rs
@@ -0,0 +1,38 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    app::App,
+    storage::db::{
+        insert::Operations,
+        video::{Video, VideoStatus, VideoStatusMarker},
+    },
+};
+
+use anyhow::Result;
+
+/// Remove the downloaded paths from videos in the db, that no longer exist on the file system.
+pub(crate) async fn clear_stale_downloaded_paths(app: &App) -> Result<()> {
+    let mut cached_videos = Video::in_states(app, &[VideoStatusMarker::Cached]).await?;
+
+    let mut ops = Operations::new("DbMaintain: init");
+    for vid in &mut cached_videos {
+        if let VideoStatus::Cached { cache_path, .. } = &vid.status {
+            if !cache_path.exists() {
+                vid.remove_download_path(&mut ops);
+            }
+        } else {
+            unreachable!("We only asked for cached videos.")
+        }
+    }
+    ops.commit(app).await?;
+
+    Ok(())
+}
diff --git a/crates/yt/src/storage/db/insert/mod.rs b/crates/yt/src/storage/db/insert/mod.rs
new file mode 100644
index 0000000..3458608
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/mod.rs
@@ -0,0 +1,115 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::mem;
+
+use crate::app::App;
+
+use anyhow::Result;
+use chrono::Utc;
+use log::{debug, trace};
+use serde::{Serialize, de::DeserializeOwned};
+use sqlx::{SqliteConnection, query};
+
+pub(crate) mod maintenance;
+pub(crate) mod playlist;
+pub(crate) mod subscription;
+pub(crate) mod video;
+
+pub(crate) trait Committable:
+    Sized + std::fmt::Debug + Serialize + DeserializeOwned
+{
+    async fn commit(self, txn: &mut SqliteConnection) -> Result<()>;
+}
+
+#[derive(Debug)]
+pub(crate) struct Operations<O: Committable> {
+    name: &'static str,
+    ops: Vec<O>,
+}
+
+impl<O: Committable> Default for Operations<O> {
+    fn default() -> Self {
+        Self::new("<default impl>")
+    }
+}
+
+impl<O: Committable> Operations<O> {
+    #[must_use]
+    pub(crate) fn new(name: &'static str) -> Self {
+        Self {
+            name,
+            ops: Vec::new(),
+        }
+    }
+
+    pub(crate) async fn commit(mut self, app: &App) -> Result<()> {
+        let ops = mem::take(&mut self.ops);
+
+        if ops.is_empty() {
+            return Ok(());
+        }
+
+        trace!("Begin commit of {}", self.name);
+        let mut txn = app.database.begin().await?;
+
+        for op in ops {
+            trace!("Commiting operation: {op:?}");
+            add_operation_to_txn_log(&op, &mut txn).await?;
+            op.commit(&mut txn).await?;
+        }
+
+        txn.commit().await?;
+        trace!("End commit of {}", self.name);
+
+        Ok(())
+    }
+
+    pub(crate) fn push(&mut self, op: O) {
+        self.ops.push(op);
+    }
+}
+
+impl<O: Committable> Drop for Operations<O> {
+    fn drop(&mut self) {
+        assert!(
+            self.ops.is_empty(),
+            "Trying to drop uncommitted operations (name: {}) ({:#?}). This is a bug.",
+            self.name,
+            self.ops
+        );
+    }
+}
+
+async fn add_operation_to_txn_log<O: Committable>(
+    operation: &O,
+    txn: &mut SqliteConnection,
+) -> Result<()> {
+    debug!("Adding operation to txn log: {operation:?}");
+
+    let now = Utc::now().timestamp();
+    let operation = serde_json::to_string(&operation).expect("should be serializable");
+
+    query!(
+        r#"
+        INSERT INTO txn_log (
+            timestamp,
+            operation
+        )
+        VALUES (?, ?);
+        "#,
+        now,
+        operation,
+    )
+    .execute(txn)
+    .await?;
+
+    Ok(())
+}
diff --git a/crates/yt/src/storage/db/insert/playlist.rs b/crates/yt/src/storage/db/insert/playlist.rs
new file mode 100644
index 0000000..4d3e140
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/playlist.rs
@@ -0,0 +1,222 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{cmp::Ordering, time::Duration};
+
+use anyhow::{Context, Result};
+use colors::Colorize;
+use libmpv2::Mpv;
+use log::{debug, trace};
+
+use crate::{
+    app::App,
+    storage::db::{
+        insert::{Operations, video::Operation},
+        playlist::{Playlist, PlaylistIndex},
+        video::VideoStatus,
+    },
+};
+
+#[derive(Debug, Clone, Copy)]
+pub(crate) enum VideoTransition {
+    Watched,
+    Picked,
+}
+
+impl Playlist {
+    pub(crate) fn mark_current_done(
+        &mut self,
+        app: &App,
+        mpv: &Mpv,
+        new_state: VideoTransition,
+        ops: &mut Operations<Operation>,
+    ) -> Result<()> {
+        let (current_index, current_video) = self
+            .get_focused_mut()
+            .expect("This should be some at this point");
+
+        debug!(
+            "Playlist handler will mark video '{}' {:?}.",
+            current_video.title, new_state
+        );
+
+        match new_state {
+            VideoTransition::Watched => current_video.set_watched(ops),
+            VideoTransition::Picked => current_video.set_status(VideoStatus::Pick, ops),
+        }
+
+        self.save_watch_progress(mpv, current_index, ops);
+
+        self.videos.remove(Into::<usize>::into(current_index));
+
+        {
+            // Decide which video to mark focused now.
+            let index = usize::from(current_index);
+            let playlist_length = self.len();
+
+            if playlist_length == 0 {
+                // There are no new videos to mark focused.
+            } else {
+                let index = match index.cmp(&playlist_length) {
+                    Ordering::Greater => {
+                        unreachable!(
+                            "The index '{index}' cannot exceed the \
+                        playlist length '{playlist_length}' as indices are 0 based."
+                        );
+                    }
+                    Ordering::Less => {
+                        // The index is still valid.
+                        // Therefore, we keep the user at this position.
+                        index
+                    }
+                    Ordering::Equal => {
+                        // The index is pointing to the end of the playlist. We could either go the second
+                        // to last entry (i.e., one entry back) or wrap around to the start.
+                        // We wrap around.
+                        0
+                    }
+                };
+
+                let next = self
+                    .get_mut(PlaylistIndex::from(index))
+                    .expect("We checked that the index is still good");
+                next.set_focused(true, ops);
+            }
+
+            // Tell mpv about our decision.
+            self.resync_with_mpv(app, mpv)?;
+        }
+
+        Ok(())
+    }
+
+    /// Sync the mpv playlist with this playlist.
+    pub(crate) fn resync_with_mpv(&self, app: &App, mpv: &Mpv) -> Result<()> {
+        fn get_playlist_count(mpv: &Mpv) -> Result<usize> {
+            mpv.get_property::<i64>("playlist/count")
+                .context("Failed to get mpv playlist len")
+                .map(|count| {
+                    usize::try_from(count).expect("The playlist_count should always be positive")
+                })
+        }
+
+        if get_playlist_count(mpv)? != 0 {
+            // We could also use `loadlist`, but that would require use to start a unix socket or even
+            // write all the video paths to a file beforehand
+            mpv.command("playlist-clear", &[])?;
+            mpv.command("playlist-remove", &["current"])?;
+        }
+
+        assert_eq!(
+            get_playlist_count(mpv)?,
+            0,
+            "The playlist should be empty at this point."
+        );
+
+        debug!("MpvReload: Adding {} videos to playlist.", self.len());
+
+        self.videos
+            .iter()
+            .enumerate()
+            .try_for_each(|(index, video)| {
+                let VideoStatus::Cached {
+                    cache_path,
+                    is_focused,
+                } = &video.status
+                else {
+                    unreachable!("All of the videos in a playlist are cached");
+                };
+
+                let options = format!(
+                    "speed={},start={}",
+                    video
+                        .playback_speed
+                        .unwrap_or(app.config.select.playback_speed),
+                    i64::try_from(video.watch_progress.as_secs())
+                        .expect("This should not overflow"),
+                );
+
+                mpv.command(
+                    "loadfile",
+                    &[
+                        cache_path.to_str().with_context(|| {
+                            format!(
+                                "Failed to parse the video cache path ('{}') as valid utf8",
+                                cache_path.display()
+                            )
+                        })?,
+                        "append-play",
+                        "-1", // Not used for `append-play`, but needed for the next args to take effect.
+                        options.as_str(),
+                    ],
+                )?;
+
+                if *is_focused {
+                    debug!("MpvReload: Setting playlist position to {index}");
+                    mpv.set_property("playlist-pos", index.to_string().as_str())?;
+                }
+
+                Ok::<(), anyhow::Error>(())
+            })?;
+
+        Ok(())
+    }
+
+    pub(crate) fn save_current_watch_progress(
+        &mut self,
+        mpv: &Mpv,
+        ops: &mut Operations<Operation>,
+    ) {
+        let (index, _) = self
+            .get_focused_mut()
+            .expect("This should be some at this point");
+
+        self.save_watch_progress(mpv, index, ops);
+    }
+
+    /// Saves the `watch_progress` of a video at the index.
+    pub(crate) fn save_watch_progress(
+        &mut self,
+        mpv: &Mpv,
+        at: PlaylistIndex,
+        ops: &mut Operations<Operation>,
+    ) {
+        let current_video = self
+            .get_mut(at)
+            .expect("We should never produce invalid playlist indices");
+
+        let watch_progress = match mpv.get_property::<i64>("time-pos") {
+            Ok(time) => u64::try_from(time)
+                .expect("This conversion should never fail as the `time-pos` property is positive"),
+            Err(err) => {
+                // We cannot hard error here, as we would open us to an race condition between mpv
+                // changing the current video and we saving it.
+                trace!(
+                    "While trying to save the watch progress for the current video: \
+                    Failed to get the watchprogress of the currently playling video: \
+                    (This is probably expected, nevertheless showing the raw error) \
+                    {err}"
+                );
+
+                return;
+            }
+        };
+
+        let watch_progress = Duration::from_secs(watch_progress);
+
+        debug!(
+            "Setting the watch progress for the current_video '{}' to {}s",
+            current_video.title_fmt().render(false),
+            watch_progress.as_secs(),
+        );
+
+        current_video.set_watch_progress(watch_progress, ops);
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/subscription.rs b/crates/yt/src/storage/db/insert/subscription.rs
new file mode 100644
index 0000000..d25a209
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/subscription.rs
@@ -0,0 +1,95 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::storage::db::{
+    insert::{Committable, Operations},
+    subscription::{Subscription, Subscriptions},
+};
+
+use anyhow::Result;
+use serde::{Deserialize, Serialize};
+use sqlx::query;
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) enum Operation {
+    Add(Subscription),
+    Remove(Subscription),
+}
+
+impl Committable for Operation {
+    async fn commit(self, txn: &mut sqlx::SqliteConnection) -> Result<()> {
+        match self {
+            Operation::Add(subscription) => {
+                let url = subscription.url.as_str();
+
+                query!(
+                    "
+                    INSERT INTO subscriptions (
+                        name,
+                        url
+                    ) VALUES (?, ?);
+                    ",
+                    subscription.name,
+                    url
+                )
+                .execute(txn)
+                .await?;
+
+                println!(
+                    "Subscribed to '{}' at '{}'",
+                    subscription.name, subscription.url
+                );
+                Ok(())
+            }
+            Operation::Remove(subscription) => {
+                let output = query!(
+                    "
+                    DELETE FROM subscriptions
+                    WHERE name = ?
+                    ",
+                    subscription.name,
+                )
+                .execute(txn)
+                .await?;
+
+                assert_eq!(
+                    output.rows_affected(),
+                    1,
+                    "The removed subscription query did effect more (or less) than one row. This is a bug."
+                );
+
+                println!(
+                    "Unsubscribed from '{}' at '{}'",
+                    subscription.name, subscription.url
+                );
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl Subscription {
+    pub(crate) fn add(self, ops: &mut Operations<Operation>) {
+        ops.push(Operation::Add(self));
+    }
+
+    pub(crate) fn remove(self, ops: &mut Operations<Operation>) {
+        ops.push(Operation::Remove(self));
+    }
+}
+
+impl Subscriptions {
+    pub(crate) fn remove(self, ops: &mut Operations<Operation>) {
+        for sub in self.0.into_values() {
+            ops.push(Operation::Remove(sub));
+        }
+    }
+}
diff --git a/crates/yt/src/storage/db/insert/video/mod.rs b/crates/yt/src/storage/db/insert/video/mod.rs
new file mode 100644
index 0000000..da62e37
--- /dev/null
+++ b/crates/yt/src/storage/db/insert/video/mod.rs
@@ -0,0 +1,610 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    path::{Path, PathBuf},
+    time,
+};
+
+use crate::storage::db::{
+    extractor_hash::ExtractorHash,
+    insert::{Committable, Operations},
+    video::{Priority, Video, VideoStatus, VideoStatusMarker},
+};
+
+use anyhow::{Context, Result};
+use chrono::Utc;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use sqlx::query;
+use tokio::fs;
+
+use super::super::video::TimeStamp;
+
+const fn is_focused_to_value(is_focused: bool) -> Option<i8> {
+    if is_focused { Some(1) } else { None }
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub(crate) enum Operation {
+    Add {
+        description: Option<String>,
+        title: String,
+        parent_subscription_name: Option<String>,
+        thumbnail_url: Option<String>,
+        url: String,
+        extractor_hash: String,
+        status: i64,
+        cache_path: Option<String>,
+        is_focused: Option<i8>,
+        duration: Option<f64>,
+        last_status_change: i64,
+        publish_date: Option<i64>,
+        watch_progress: i64,
+    },
+    // TODO(@bpeetz): Could both the {`Set`,`Remove`}`DownloadPath` ops, be merged into SetStatus
+    // {`Cached`,`Watch`}? <2025-07-14>
+    SetDownloadPath {
+        video: ExtractorHash,
+        path: PathBuf,
+    },
+    RemoveDownloadPath {
+        video: ExtractorHash,
+    },
+    SetStatus {
+        video: ExtractorHash,
+        status: VideoStatus,
+    },
+    SetPriority {
+        video: ExtractorHash,
+        priority: Priority,
+    },
+    SetPlaybackSpeed {
+        video: ExtractorHash,
+        playback_speed: f64,
+    },
+    SetSubtitleLangs {
+        video: ExtractorHash,
+        subtitle_langs: String,
+    },
+    SetWatchProgress {
+        video: ExtractorHash,
+        watch_progress: time::Duration,
+    },
+    SetIsFocused {
+        video: ExtractorHash,
+        is_focused: bool,
+    },
+}
+
+impl Committable for Operation {
+    #[allow(clippy::too_many_lines)]
+    async fn commit(self, txn: &mut sqlx::SqliteConnection) -> Result<()> {
+        match self {
+            Operation::SetDownloadPath { video, path } => {
+                debug!("Setting cache path from '{video}' to '{}'", path.display());
+
+                let path_str = path.display().to_string();
+                let extractor_hash = video.hash().to_string();
+                let status = VideoStatusMarker::Cached.as_db_integer();
+
+                query!(
+                    r#"
+                UPDATE videos
+                SET cache_path = ?, status = ?
+                WHERE extractor_hash = ?;
+                "#,
+                    path_str,
+                    status,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::RemoveDownloadPath { video } => {
+                let extractor_hash = video.hash().to_string();
+                let status = VideoStatus::Watch.as_marker().as_db_integer();
+
+                let old = video.get(&mut *txn).await?;
+
+                debug!("Deleting download path of '{video}' ({}).", old.title);
+
+                if let VideoStatus::Cached { cache_path, .. } = &old.status {
+                    if let Ok(true) = cache_path.try_exists() {
+                        fs::remove_file(cache_path).await?;
+                    }
+
+                    {
+                        let info_json_path = old.info_json_path()?.expect("Is downloaded");
+
+                        if let Ok(true) = info_json_path.try_exists() {
+                            fs::remove_file(info_json_path).await?;
+                        }
+                    }
+
+                    {
+                        if old.subtitle_langs.is_some() {
+                            // TODO(@bpeetz): Also clean-up the downloaded subtitle files. <2025-07-05>
+                        }
+                    }
+                } else {
+                    unreachable!(
+                        "A video cannot have a download path deletion \
+                        queued without being marked as Cached."
+                    );
+                }
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET cache_path = NULL, status = ?, is_focused = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    status,
+                    None::<i32>,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetStatus { video, status } => {
+                let extractor_hash = video.hash().to_string();
+
+                let old = video.get(&mut *txn).await?;
+
+                let old_marker = old.status.as_marker();
+
+                let (cache_path, is_focused) = {
+                    fn cache_path_to_string(path: &Path) -> Result<String> {
+                        Ok(path
+                            .to_str()
+                            .with_context(|| {
+                                format!(
+                                    "Failed to parse cache path ('{}') as utf8 string",
+                                    path.display()
+                                )
+                            })?
+                            .to_owned())
+                    }
+
+                    if let VideoStatus::Cached {
+                        cache_path,
+                        is_focused,
+                    } = &status
+                    {
+                        (
+                            Some(cache_path_to_string(cache_path)?),
+                            is_focused_to_value(*is_focused),
+                        )
+                    } else {
+                        (None, None)
+                    }
+                };
+
+                let new_status = status.as_marker();
+
+                assert_ne!(
+                    old_marker, new_status,
+                    "We should have never generated this operation"
+                );
+
+                let now = Utc::now().timestamp();
+
+                debug!(
+                    "Changing status of video ('{}' {extractor_hash}) \
+                        from {old_marker:#?} to {new_status:#?}.",
+                    old.title
+                );
+
+                let new_status = new_status.as_db_integer();
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET status = ?, last_status_change = ?, cache_path = ?, is_focused = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    new_status,
+                    now,
+                    cache_path,
+                    is_focused,
+                    extractor_hash,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetPriority { video, priority } => {
+                let extractor_hash = video.hash().to_string();
+
+                let new_priority = priority.as_db_integer();
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET priority = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    new_priority,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetWatchProgress {
+                video,
+                watch_progress,
+            } => {
+                let video_extractor_hash = video.hash().to_string();
+                let watch_progress = i64::try_from(watch_progress.as_secs())
+                    .expect("This should never exceed its bounds");
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET watch_progress = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    watch_progress,
+                    video_extractor_hash,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetPlaybackSpeed {
+                video,
+                playback_speed,
+            } => {
+                let extractor_hash = video.hash().to_string();
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET playback_speed = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    playback_speed,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetSubtitleLangs {
+                video,
+                subtitle_langs,
+            } => {
+                let extractor_hash = video.hash().to_string();
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET subtitle_langs = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    subtitle_langs,
+                    extractor_hash
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::SetIsFocused { video, is_focused } => {
+                debug!("Set is_focused of video: '{video}' to {is_focused}");
+                let new_hash = video.hash().to_string();
+                let new_is_focused = is_focused_to_value(is_focused);
+
+                query!(
+                    r#"
+                    UPDATE videos
+                    SET is_focused = ?
+                    WHERE extractor_hash = ?;
+                    "#,
+                    new_is_focused,
+                    new_hash,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+            Operation::Add {
+                parent_subscription_name,
+                thumbnail_url,
+                url,
+                extractor_hash,
+                status,
+                cache_path,
+                is_focused,
+                duration,
+                last_status_change,
+                publish_date,
+                watch_progress,
+                description,
+                title,
+            } => {
+                query!(
+                    r#"
+                    INSERT INTO videos (
+                        description,
+                        duration,
+                        extractor_hash,
+                        is_focused,
+                        last_status_change,
+                        parent_subscription_name,
+                        publish_date,
+                        status,
+                        thumbnail_url,
+                        title,
+                        url,
+                        watch_progress,
+                        cache_path
+                        )
+                    VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
+                    "#,
+                    description,
+                    duration,
+                    extractor_hash,
+                    is_focused,
+                    last_status_change,
+                    parent_subscription_name,
+                    publish_date,
+                    status,
+                    thumbnail_url,
+                    title,
+                    url,
+                    watch_progress,
+                    cache_path,
+                )
+                .execute(txn)
+                .await?;
+
+                Ok(())
+            }
+        }
+    }
+}
+
+impl Video {
+    /// Add this in-memory video to the db.
+    pub(crate) fn add(self, ops: &mut Operations<Operation>) -> Result<Self> {
+        let description = self.description.clone();
+        let title = self.title.clone();
+        let parent_subscription_name = self.parent_subscription_name.clone();
+
+        let thumbnail_url = self.thumbnail_url.as_ref().map(ToString::to_string);
+
+        let url = self.url.to_string();
+        let extractor_hash = self.extractor_hash.hash().to_string();
+
+        let status = self.status.as_marker().as_db_integer();
+        let (cache_path, is_focused) = if let VideoStatus::Cached {
+            cache_path,
+            is_focused,
+        } = &self.status
+        {
+            (
+                Some(
+                    cache_path
+                        .to_str()
+                        .with_context(|| {
+                            format!(
+                                "Failed to prase cache path '{}' as utf-8 string",
+                                cache_path.display()
+                            )
+                        })?
+                        .to_string(),
+                ),
+                is_focused_to_value(*is_focused),
+            )
+        } else {
+            (None, None)
+        };
+
+        let duration: Option<f64> = self.duration.as_secs_f64();
+        let last_status_change: i64 = self.last_status_change.as_secs();
+        let publish_date: Option<i64> = self.publish_date.map(TimeStamp::as_secs);
+        let watch_progress: i64 =
+            i64::try_from(self.watch_progress.as_secs()).expect("This should never exceed a u32");
+
+        ops.push(Operation::Add {
+            description,
+            title,
+            parent_subscription_name,
+            thumbnail_url,
+            url,
+            extractor_hash,
+            status,
+            cache_path,
+            is_focused,
+            duration,
+            last_status_change,
+            publish_date,
+            watch_progress,
+        });
+
+        Ok(self)
+    }
+
+    /// Set the download path of a video.
+    ///
+    /// # Note
+    /// This will also set the status to `Cached`.
+    pub(crate) fn set_download_path(&mut self, path: &Path, ops: &mut Operations<Operation>) {
+        if let VideoStatus::Cached { cache_path, .. } = &mut self.status {
+            if cache_path != path {
+                // Update the in-memory video.
+                path.clone_into(cache_path);
+
+                ops.push(Operation::SetDownloadPath {
+                    video: self.extractor_hash,
+                    path: path.to_owned(),
+                });
+            }
+        } else {
+            self.status = VideoStatus::Cached {
+                cache_path: path.to_owned(),
+                is_focused: false,
+            };
+
+            ops.push(Operation::SetDownloadPath {
+                video: self.extractor_hash,
+                path: path.to_owned(),
+            });
+        }
+    }
+
+    /// Remove the download path of a video.
+    ///
+    /// # Note
+    /// This will also set the status to `Watch`.
+    ///
+    /// # Panics
+    /// If the status is not `Cached`.
+    pub(crate) fn remove_download_path(&mut self, ops: &mut Operations<Operation>) {
+        if let VideoStatus::Cached { .. } = &mut self.status {
+            self.status = VideoStatus::Watch;
+            ops.push(Operation::RemoveDownloadPath {
+                video: self.extractor_hash,
+            });
+        } else {
+            unreachable!("Can only remove the path from a `Cached` video");
+        }
+    }
+
+    /// Update the `is_focused` flag of this video.
+    ///
+    /// # Note
+    /// It will only actually add operations, if the `is_focused` flag is different.
+    ///
+    /// # Panics
+    /// If the status is not `Cached`.
+    pub(crate) fn set_focused(&mut self, new_is_focused: bool, ops: &mut Operations<Operation>) {
+        if let VideoStatus::Cached { is_focused, .. } = &mut self.status {
+            if *is_focused != new_is_focused {
+                *is_focused = new_is_focused;
+
+                ops.push(Operation::SetIsFocused {
+                    video: self.extractor_hash,
+                    is_focused: new_is_focused,
+                });
+            }
+        } else {
+            unreachable!("Can only change `is_focused` on a Cached video.");
+        }
+    }
+
+    /// Set the status of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new status equals the old one.
+    pub(crate) fn set_status(&mut self, status: VideoStatus, ops: &mut Operations<Operation>) {
+        if self.status != status {
+            status.clone_into(&mut self.status);
+
+            ops.push(Operation::SetStatus {
+                video: self.extractor_hash,
+                status,
+            });
+        }
+    }
+
+    /// Set the priority of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new priority equals the old one.
+    pub(crate) fn set_priority(&mut self, priority: Priority, ops: &mut Operations<Operation>) {
+        if self.priority != priority {
+            self.priority = priority;
+
+            ops.push(Operation::SetPriority {
+                video: self.extractor_hash,
+                priority,
+            });
+        }
+    }
+
+    /// Set the watch progress.
+    ///
+    /// # Note
+    /// This will not actually add any operations,
+    /// if the new watch progress equals the old one.
+    pub(crate) fn set_watch_progress(
+        &mut self,
+        watch_progress: time::Duration,
+        ops: &mut Operations<Operation>,
+    ) {
+        if self.watch_progress != watch_progress {
+            self.watch_progress = watch_progress;
+
+            ops.push(Operation::SetWatchProgress {
+                video: self.extractor_hash,
+                watch_progress,
+            });
+        }
+    }
+
+    /// Set the playback speed of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new speed equals the old one.
+    pub(crate) fn set_playback_speed(
+        &mut self,
+        playback_speed: f64,
+        ops: &mut Operations<Operation>,
+    ) {
+        if self.playback_speed != Some(playback_speed) {
+            self.playback_speed = Some(playback_speed);
+
+            ops.push(Operation::SetPlaybackSpeed {
+                video: self.extractor_hash,
+                playback_speed,
+            });
+        }
+    }
+
+    /// Set the subtitle langs of this video.
+    ///
+    /// # Note
+    /// This will not actually add any operations, if the new langs equal the old one.
+    pub(crate) fn set_subtitle_langs(
+        &mut self,
+        subtitle_langs: String,
+        ops: &mut Operations<Operation>,
+    ) {
+        if self.subtitle_langs.as_ref() != Some(&subtitle_langs) {
+            self.subtitle_langs = Some(subtitle_langs.clone());
+
+            ops.push(Operation::SetSubtitleLangs {
+                video: self.extractor_hash,
+                subtitle_langs,
+            });
+        }
+    }
+
+    /// Mark this video watched.
+    /// This will both set the status to `Watched` and the `cache_path` to Null.
+    ///
+    /// # Panics
+    /// Only if assertions fail.
+    pub(crate) fn set_watched(&mut self, ops: &mut Operations<Operation>) {
+        self.remove_download_path(ops);
+        self.set_status(VideoStatus::Watched, ops);
+    }
+}
diff --git a/crates/yt/src/storage/db/mod.rs b/crates/yt/src/storage/db/mod.rs
new file mode 100644
index 0000000..926bab0
--- /dev/null
+++ b/crates/yt/src/storage/db/mod.rs
@@ -0,0 +1,18 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+pub(crate) mod get;
+pub(crate) mod insert;
+
+pub(crate) mod extractor_hash;
+pub(crate) mod playlist;
+pub(crate) mod subscription;
+pub(crate) mod txn_log;
+pub(crate) mod video;
diff --git a/crates/yt/src/storage/db/playlist/mod.rs b/crates/yt/src/storage/db/playlist/mod.rs
new file mode 100644
index 0000000..7366e8e
--- /dev/null
+++ b/crates/yt/src/storage/db/playlist/mod.rs
@@ -0,0 +1,59 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::ops::Add;
+
+use crate::storage::db::video::Video;
+
+/// Zero-based index into the internal playlist.
+#[derive(Debug, Clone, Copy)]
+pub(crate) struct PlaylistIndex(usize);
+
+impl From<PlaylistIndex> for usize {
+    fn from(value: PlaylistIndex) -> Self {
+        value.0
+    }
+}
+
+impl From<usize> for PlaylistIndex {
+    fn from(value: usize) -> Self {
+        Self(value)
+    }
+}
+
+impl Add<usize> for PlaylistIndex {
+    type Output = Self;
+
+    fn add(self, rhs: usize) -> Self::Output {
+        Self(self.0 + rhs)
+    }
+}
+
+impl Add for PlaylistIndex {
+    type Output = Self;
+
+    fn add(self, rhs: Self) -> Self::Output {
+        Self(self.0 + rhs.0)
+    }
+}
+
+/// A representation of the internal Playlist
+#[derive(Debug)]
+pub(crate) struct Playlist {
+    pub(crate) videos: Vec<Video>,
+}
+
+impl Playlist {
+    /// Returns the number of videos in the playlist
+    #[must_use]
+    pub(crate) fn len(&self) -> usize {
+        self.videos.len()
+    }
+}
diff --git a/crates/yt/src/storage/db/subscription.rs b/crates/yt/src/storage/db/subscription.rs
new file mode 100644
index 0000000..c111b52
--- /dev/null
+++ b/crates/yt/src/storage/db/subscription.rs
@@ -0,0 +1,52 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::collections::HashMap;
+
+use anyhow::Result;
+use log::debug;
+use serde::{Deserialize, Serialize};
+use url::Url;
+use yt_dlp::{json_cast, json_try_get, options::YoutubeDLOptions};
+
+#[derive(Clone, Debug, Serialize, Deserialize)]
+pub(crate) struct Subscription {
+    /// The human readable name of this subscription
+    pub(crate) name: String,
+
+    /// The URL this subscription subscribes to
+    pub(crate) url: Url,
+}
+
+impl Subscription {
+    #[must_use]
+    pub(crate) fn new(name: String, url: Url) -> Self {
+        Self { name, url }
+    }
+}
+
+#[derive(Default, Debug)]
+pub(crate) struct Subscriptions(pub(crate) HashMap<String, Subscription>);
+
+/// Check whether an URL could be used as a subscription URL
+pub(crate) async fn check_url(url: Url) -> Result<bool> {
+    let yt_dlp = YoutubeDLOptions::new()
+        .set("playliststart", 1)
+        .set("playlistend", 10)
+        .set("noplaylist", false)
+        .set("extract_flat", "in_playlist")
+        .build()?;
+
+    let info = yt_dlp.extract_info(&url, false, false)?;
+
+    debug!("{info:#?}");
+
+    Ok(json_try_get!(info, "_type", as_str) == Some("playlist"))
+}
diff --git a/crates/yt/src/storage/db/txn_log.rs b/crates/yt/src/storage/db/txn_log.rs
new file mode 100644
index 0000000..64884b0
--- /dev/null
+++ b/crates/yt/src/storage/db/txn_log.rs
@@ -0,0 +1,24 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::storage::db::{insert::Committable, video::TimeStamp};
+
+pub(crate) struct TxnLog<O: Committable> {
+    inner: Vec<(TimeStamp, O)>,
+}
+
+impl<O: Committable> TxnLog<O> {
+    pub(crate) fn new(inner: Vec<(TimeStamp, O)>) -> Self {
+        Self { inner }
+    }
+    pub(crate) fn inner(&self) -> &[(TimeStamp, O)] {
+        &self.inner
+    }
+}
diff --git a/yt/src/comments/display.rs b/crates/yt/src/storage/db/video/comments/display.rs
index 6166b2b..c372603 100644
--- a/yt/src/comments/display.rs
+++ b/crates/yt/src/storage/db/video/comments/display.rs
@@ -1,6 +1,5 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
@@ -13,27 +12,22 @@ use std::fmt::Write;
 
 use chrono::{Local, TimeZone};
 use chrono_humanize::{Accuracy, HumanTime, Tense};
+use colors::{Colorize, IntoCanvas};
 
-use crate::comments::comment::CommentExt;
-
-use super::comment::Comments;
+use crate::{
+    output::format_text,
+    storage::db::video::comments::{Comment, Comments},
+};
 
 impl Comments {
-    pub fn render(&self, color: bool) -> String {
-        self.render_help(color).expect("This should never fail.")
+    pub(crate) fn render(&self, use_color: bool) -> String {
+        self.render_help(use_color)
+            .expect("This should never fail.")
     }
 
-    fn render_help(&self, color: bool) -> Result<String, std::fmt::Error> {
-        macro_rules! c {
-            ($color_str:expr, $write:ident, $color:expr) => {
-                if $color {
-                    $write.write_str(concat!("\x1b[", $color_str, "m"))?
-                }
-            };
-        }
-
+    fn render_help(&self, use_color: bool) -> Result<String, std::fmt::Error> {
         fn format(
-            comment: &CommentExt,
+            comment: &Comment,
             f: &mut String,
             ident_count: u32,
             color: bool,
@@ -43,14 +37,16 @@ impl Comments {
 
             f.write_str(ident)?;
 
-            if value.author_is_uploader {
-                c!("91;1", f, color);
-            } else {
-                c!("35", f, color);
-            }
+            write!(
+                f,
+                "{}",
+                if value.author_is_uploader {
+                    (&value.author).bold().bright_red().render(color)
+                } else {
+                    (&value.author).purple().render(color)
+                }
+            )?;
 
-            f.write_str(&value.author)?;
-            c!("0", f, color);
             if value.edited || value.is_favorited {
                 f.write_str("[")?;
                 if value.edited {
@@ -65,7 +61,6 @@ impl Comments {
                 f.write_str("]")?;
             }
 
-            c!("36;1", f, color);
             write!(
                 f,
                 " {}",
@@ -76,17 +71,31 @@ impl Comments {
                         .expect("This should be valid")
                 )
                 .to_text_en(Accuracy::Rough, Tense::Past)
+                .bold()
+                .cyan()
+                .render(color)
             )?;
-            c!("0", f, color);
 
-            // c!("31;1", f);
-            // f.write_fmt(format_args!(" [{}]", comment.value.like_count))?;
-            // c!("0", f);
+            write!(
+                f,
+                " [{}]",
+                comment.value.like_count.bold().red().render(color)
+            )?;
 
             f.write_str(":\n")?;
             f.write_str(ident)?;
 
-            f.write_str(&value.text.replace('\n', &format!("\n{ident}")))?;
+            f.write_str(
+                &format_text(
+                    value.text.trim(),
+                    Some(
+                        termsize::get().map_or(90, |ts| ts.cols)
+                            - u16::try_from(ident_count).expect("Should never overflow"),
+                    ),
+                )
+                .trim()
+                .replace('\n', &format!("\n{ident}")),
+            )?;
             f.write_str("\n")?;
 
             if comment.replies.is_empty() {
@@ -105,12 +114,12 @@ impl Comments {
 
         let mut f = String::new();
 
-        if !&self.vec.is_empty() {
-            let mut children = self.vec.clone();
+        if !&self.inner.is_empty() {
+            let mut children = self.inner.clone();
             children.sort_by(|a, b| b.value.like_count.cmp(&a.value.like_count));
 
             for child in children {
-                format(&child, &mut f, 0, color)?;
+                format(&child, &mut f, 0, use_color)?;
             }
         }
         Ok(f)
diff --git a/crates/yt/src/storage/db/video/comments/mod.rs b/crates/yt/src/storage/db/video/comments/mod.rs
new file mode 100644
index 0000000..41a03be
--- /dev/null
+++ b/crates/yt/src/storage/db/video/comments/mod.rs
@@ -0,0 +1,202 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::mem;
+
+use regex::{Captures, Regex};
+
+use crate::storage::db::video::comments::raw::{Parent, RawComment};
+
+pub(crate) mod display;
+pub(crate) mod raw;
+
+#[cfg(test)]
+mod tests;
+
+#[derive(Debug, Clone, PartialEq)]
+pub(crate) struct Comment {
+    value: RawComment,
+    replies: Vec<Self>,
+}
+
+#[derive(Debug, Default, PartialEq)]
+pub(crate) struct Comments {
+    inner: Vec<Comment>,
+}
+
+impl Comments {
+    pub(crate) fn from_raw(raw: Vec<RawComment>) -> Self {
+        let mut me = Self::default();
+
+        // Apply the parent -> child mapping yt provides us with.
+        for raw_comment in raw {
+            if let Parent::Id(id) = &raw_comment.parent {
+                me.insert(&(id.clone()), Comment::from(raw_comment));
+            } else {
+                me.inner.push(Comment::from(raw_comment));
+            }
+        }
+
+        {
+            // Sort the final comments chronologically.
+            // This ensures that replies are matched with the comment they actually replied to and
+            // not a later comment from the same author.
+            for comment in &mut me.inner {
+                comment
+                    .replies
+                    .sort_by_key(|comment| comment.value.timestamp);
+
+                for reply in &comment.replies {
+                    assert!(reply.replies.is_empty());
+                }
+            }
+        }
+
+        {
+            let find_reply_indicator =
+                Regex::new(r"\u{200b}?(@[^\t\s]+)\u{200b}?").expect("This is hardcoded");
+
+            // Try to re-construct the replies for the reply comments.
+            for comment in &mut me.inner {
+                let previous_replies = mem::take(&mut comment.replies);
+
+                let mut reply_tree = Comments::default();
+
+                for reply in previous_replies {
+                    // We try to reconstruct the parent child relation ship by looking (naively)
+                    // for a reply indicator. Currently, this is just the `@<some_name>`, as yt
+                    // seems to insert that by default if you press `reply-to` in their clients.
+                    //
+                    // This follows these steps:
+                    // - Does this reply have a “reply indicator”?
+                    // - If yes, try to resolve the indicator.
+                    // - If it is resolvable, add this reply to the [`Comment`] it resolved to.
+                    // - If not, keep the comment as reply.
+
+                    if let Some(reply_indicator_matches) =
+                        find_reply_indicator.captures(&reply.value.text.clone())
+                    {
+                        // We found a reply indicator.
+                        // First we traverse the current `reply_tree` in reversed order to find a
+                        // match, than we check if the reply indicator matches the reply tree root
+                        // and afterward we declare it unmatching and add it as toplevel.
+
+                        let reply_target_author = reply_indicator_matches
+                            .get(1)
+                            .expect("This should also exist")
+                            .as_str();
+
+                        if let Some(parent) = reply_tree.find_author_mut(reply_target_author) {
+                            parent
+                                .replies
+                                .push(comment_from_reply(reply, &reply_indicator_matches));
+                        } else if comment.value.author == reply_target_author {
+                            reply_tree
+                                .add_toplevel(comment_from_reply(reply, &reply_indicator_matches));
+                        } else {
+                            eprintln!(
+                                "Failed to find a parent for ('{}') both directly \
+                                    and via replies! The reply text was:\n'{}'\n",
+                                reply_target_author, reply.value.text
+                            );
+                            reply_tree.add_toplevel(reply);
+                        }
+                    } else {
+                        // The comment text did not contain a reply indicator, so add it as
+                        // toplevel.
+                        reply_tree.add_toplevel(reply);
+                    }
+                }
+
+                comment.replies = reply_tree.inner;
+            }
+        }
+
+        me
+    }
+
+    fn add_toplevel(&mut self, value: Comment) {
+        self.inner.push(value);
+    }
+
+    fn insert(&mut self, id: &str, value: Comment) {
+        let parent = self
+            .inner
+            .iter_mut()
+            .find(|c| c.value.id.id == id)
+            .expect("One of these should exist");
+
+        parent.replies.push(value);
+    }
+
+    fn find_author_mut(&mut self, reply_target_author: &str) -> Option<&mut Comment> {
+        fn perform_check<'a>(
+            comment: &'a mut Comment,
+            reply_target_author: &str,
+        ) -> Option<&'a mut Comment> {
+            // TODO(@bpeetz): This is a workaround until rust has lexiographic lifetime support. <2025-07-18>
+            fn find_in_replies<'a>(
+                comment: &'a mut Comment,
+                reply_target_author: &str,
+            ) -> Option<&'a mut Comment> {
+                comment
+                    .replies
+                    .iter_mut()
+                    .rev()
+                    .find_map(|reply: &mut Comment| perform_check(reply, reply_target_author))
+            }
+            let comment_author_matches_target = comment.value.author == reply_target_author;
+
+            match find_in_replies(comment, reply_target_author) {
+                Some(_) => Some(
+                    // PERFORMANCE(@bpeetz): We should not need to run this code twice. <2025-07-18>
+                    find_in_replies(comment, reply_target_author)
+                        .expect("We already had a Some result for this."),
+                ),
+                None if comment_author_matches_target => Some(comment),
+                None => None,
+            }
+        }
+
+        for comment in self.inner.iter_mut().rev() {
+            if let Some(output) = perform_check(comment, reply_target_author) {
+                return Some(output);
+            }
+        }
+
+        None
+    }
+}
+fn comment_from_reply(reply: Comment, reply_indicator_matches: &Captures<'_>) -> Comment {
+    Comment::from(RawComment {
+        text: {
+            // Remove the `@<some_name>` for the comment text.
+            let full_match = reply_indicator_matches
+                .get(0)
+                .expect("This will always exist");
+
+            let text = reply.value.text[0..full_match.start()].to_owned()
+                + &reply.value.text[full_match.end()..];
+
+            text.trim_matches(|c: char| c == '\u{200b}' || c == '\u{2060}' || c.is_whitespace())
+                .to_owned()
+        },
+        ..reply.value
+    })
+}
+
+impl From<RawComment> for Comment {
+    fn from(value: RawComment) -> Self {
+        Self {
+            value,
+            replies: vec![],
+        }
+    }
+}
diff --git a/crates/yt/src/storage/db/video/comments/raw.rs b/crates/yt/src/storage/db/video/comments/raw.rs
new file mode 100644
index 0000000..3b7f40f
--- /dev/null
+++ b/crates/yt/src/storage/db/video/comments/raw.rs
@@ -0,0 +1,87 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use serde::{Deserialize, Deserializer};
+use url::Url;
+
+#[derive(Debug, Deserialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
+#[serde(from = "String")]
+#[serde(deny_unknown_fields)]
+pub(crate) struct Id {
+    pub(crate) id: String,
+}
+impl From<String> for Id {
+    fn from(value: String) -> Self {
+        Self {
+            // Take the last element if the string is split with dots, otherwise take the full id
+            id: value.split('.').next_back().unwrap_or(&value).to_owned(),
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
+#[serde(from = "String")]
+#[serde(deny_unknown_fields)]
+pub(crate) enum Parent {
+    Root,
+    Id(String),
+}
+
+impl From<String> for Parent {
+    fn from(value: String) -> Self {
+        if value == "root" {
+            Self::Root
+        } else {
+            Self::Id(value)
+        }
+    }
+}
+
+#[derive(Debug, Deserialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
+#[allow(clippy::struct_excessive_bools)]
+pub(crate) struct RawComment {
+    pub(crate) id: Id,
+    pub(crate) text: String,
+    #[serde(default = "zero")]
+    pub(crate) like_count: u32,
+    pub(crate) is_pinned: bool,
+    pub(crate) author_id: String,
+    #[serde(default = "unknown")]
+    pub(crate) author: String,
+    pub(crate) author_is_verified: bool,
+    pub(crate) author_thumbnail: Url,
+    pub(crate) parent: Parent,
+    #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")]
+    pub(crate) edited: bool,
+    // Can't also be deserialized, as it's already used in 'edited'
+    // _time_text: String,
+    pub(crate) timestamp: i64,
+    pub(crate) author_url: Option<Url>,
+    pub(crate) author_is_uploader: bool,
+    pub(crate) is_favorited: bool,
+}
+
+fn unknown() -> String {
+    "<Unknown>".to_string()
+}
+fn zero() -> u32 {
+    0
+}
+fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error>
+where
+    D: Deserializer<'de>,
+{
+    let s = String::deserialize(d)?;
+    if s.contains(" (edited)") {
+        Ok(true)
+    } else {
+        Ok(false)
+    }
+}
diff --git a/crates/yt/src/storage/db/video/comments/tests.rs b/crates/yt/src/storage/db/video/comments/tests.rs
new file mode 100644
index 0000000..03e3597
--- /dev/null
+++ b/crates/yt/src/storage/db/video/comments/tests.rs
@@ -0,0 +1,249 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use pretty_assertions::assert_eq;
+use url::Url;
+
+use crate::storage::db::video::comments::{
+    Comment, Comments, RawComment,
+    raw::{Id, Parent},
+};
+
+/// Generate both an [`expected`] and an [`input`] value from an expected comment expression.
+macro_rules! mk_comments {
+    () => {{
+        let input: Vec<RawComment> = vec![];
+        let expected: Comments = Comments {
+            inner: vec![],
+        };
+
+        (input, expected)
+    }};
+
+    (
+        $(
+            parent: $parent:expr, $actual_parent:ident,
+            (
+                @ $name:ident : $comment:literal
+                $(
+                    $reply_chain:tt
+                )*
+            )
+        )+
+    ) => {{
+        let (nested_input, _) = mk_comments!(
+            $(
+                $(
+                    parent: $parent, $name,
+                    $reply_chain
+                )*
+            )+
+        );
+
+        let mut input: Vec<RawComment> = vec![
+            $(
+                mk_comments!(@to_raw input $name $comment $parent, $actual_parent)
+            ),+
+        ];
+        input.extend(nested_input);
+
+        let expected: Comments = Comments {
+            inner: vec![
+                $(
+                    Comment {
+                        value: mk_comments!(@to_raw expected $name $comment $parent, $actual_parent),
+                        replies: {
+                            let (_, nested_expected) = mk_comments!(
+                                $(
+                                    parent: $parent, $name,
+                                    $reply_chain
+                                )*
+                            );
+
+                            nested_expected.inner
+                        },
+                    }
+                ),+
+            ]
+        };
+
+        (input, expected)
+    }};
+    (
+        $(
+            (
+                @ $name:ident : $comment:literal
+                $(
+                    $reply_chain:tt
+                )*
+            )
+        )+
+    ) => {{
+        let (nested_input, _) = mk_comments!(
+            $(
+                $(
+                    parent: mk_comments!(@mk_id $name $comment), $name,
+                    $reply_chain
+                )*
+            )+
+        );
+
+        let mut input: Vec<RawComment> = vec![
+            $(
+                mk_comments!(@to_raw input $name $comment)
+            ),+
+        ];
+        input.extend(nested_input);
+
+        let expected: Comments = Comments {
+            inner: vec![
+                $(
+                    Comment {
+                        value: mk_comments!(@to_raw expected $name $comment),
+                        replies: {
+                            let (_, nested_expected) = mk_comments!(
+                                $(
+                                    parent: mk_comments!(@mk_id $name $comment), $name,
+                                    $reply_chain
+                                )*
+                            );
+
+                            nested_expected.inner
+                        },
+                    }
+                ),+
+            ]
+        };
+
+        (input, expected)
+    }};
+
+    (@mk_id $name:ident $comment:literal) => {{
+        use std::hash::{Hash, Hasher};
+
+        let input = format!("{}{}", stringify!($name), $comment);
+
+        let mut digest = std::hash::DefaultHasher::new();
+        input.hash(&mut digest);
+        Id { id: digest.finish().to_string() }
+    }};
+
+    (@to_raw $state:ident $name:ident $comment:literal $($parent:expr, $actual_parent:ident)?) => {
+        RawComment {
+            id: mk_comments!(@mk_id $name $comment),
+            text: mk_comments!(@mk_text $state $comment $(, $actual_parent)?),
+            like_count: 0,
+            is_pinned: false,
+            author_id: stringify!($name).to_owned(),
+            author: format!("@{}", stringify!($name)),
+            author_is_verified: false,
+            author_thumbnail: Url::from_file_path("/dev/null").unwrap(),
+            parent: mk_comments!(@mk_parent $($parent)?),
+            edited: false,
+            timestamp: 0,
+            author_url: None,
+            author_is_uploader: false,
+            is_favorited: false,
+        }
+    };
+
+    (@mk_parent) => {
+        Parent::Root
+    };
+    (@mk_parent $parent:expr) => {
+        Parent::Id($parent.id)
+    };
+
+    (@mk_text input $text:expr) => {
+        $text.to_owned()
+    };
+    (@mk_text input $text:expr, $actual_parent:ident) => {
+        format!("@{} {}", stringify!($actual_parent), $text)
+    };
+    (@mk_text expected $text:expr $(, $_:tt)?) => {
+        $text.to_owned()
+    };
+}
+
+#[test]
+fn test_comments_toplevel() {
+    let (input, expected) = mk_comments!(
+        (@kant: "I think, that using the results of an action to determine morality is flawed.")
+        (@hume: "I think, that we should use our feeling for morality more.")
+        (@lock: "I think, that we should rely on the sum of happiness caused by an action to determine it's morality.")
+    );
+
+    assert_eq!(Comments::from_raw(input), expected);
+}
+
+#[test]
+fn test_comments_replies_1_level() {
+    let (input, expected) = mk_comments!(
+        (@hume: "I think, that we should use our feeling for morality more."
+            (@kant: "This is so wrong! I shall now dedicate my next 7? years to writing books that prove this.")
+            (@lock: "It feels not very applicable, no? We should focus on something that can be used in the court of law!"))
+    );
+
+    assert_eq!(
+        Comments::from_raw(input).render(true),
+        expected.render(true)
+    );
+}
+
+#[test]
+fn test_comments_replies_2_levels() {
+    let (input, expected) = mk_comments!(
+        (@singer: "We perform medical studies on animals; Children have lower or similar mental ability as these animals.."
+            (@singer: "Therefore, we should perform these studies on children instead, if we were to follow our own principals"
+                (@james: "This is ridiculous! I will not entertain this thought.")
+                (@singer: "Although one could also use this argument to argue for abortion _after_ birth.")))
+    );
+
+    assert_eq!(
+        Comments::from_raw(input).render(true),
+        expected.render(true)
+    );
+}
+
+#[test]
+fn test_comments_replies_3_levels() {
+    let (input, expected) = mk_comments!(
+        (@singer: "We perform medical studies on animals; Children have lower or similar mental ability as these animals.."
+            (@singer: "Therefore, we should perform these studies on children instead, if we were to follow our own principals"
+                (@james: "This is ridiculous! I will not entertain this thought."
+                    (@singer: "You know that I am not actually suggesting that? This is but a way to critizise the society"))
+                (@singer: "Although one could also use this argument to argue for abortion _after_ birth.")))
+    );
+
+    assert_eq!(
+        Comments::from_raw(input).render(true),
+        expected.render(true)
+    );
+}
+
+#[test]
+fn test_comments_sub_answer_selection() {
+    let (input, expected) = mk_comments!(
+        (@coffeewolfproductions9113: "I mean, brothels and sex workers in of themselves are not a bad thing."
+            (@aikikaname6508: "probably not so much in the 50s, pre contraception")
+            (@as_ri1mb: "it’s an incredibly sad, degrading line of work, often resulting in self loathing and self-deletion."
+                (@coffeewolfproductions9113: "Are you speaking from experience?"
+                    (@as_ri1mb: "what an immature response, as expected."
+                        (@coffeewolfproductions9113: "I literally just asked if you were talking from experience.")))))
+
+    );
+
+    eprintln!("{}", expected.render(true));
+
+    assert_eq!(
+        Comments::from_raw(input).render(true),
+        expected.render(true)
+    );
+}
diff --git a/yt/src/storage/video_database/mod.rs b/crates/yt/src/storage/db/video/mod.rs
index 74d09f0..deeb82c 100644
--- a/yt/src/storage/video_database/mod.rs
+++ b/crates/yt/src/storage/db/video/mod.rs
@@ -1,6 +1,5 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
@@ -9,55 +8,108 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-use std::{
-    fmt::{Display, Write},
-    path::PathBuf,
-    time::Duration,
-};
+use std::{fmt::Display, path::PathBuf, time::Duration};
 
 use chrono::{DateTime, Utc};
+use serde::{Deserialize, Serialize};
 use url::Url;
 
-use crate::{
-    app::App, select::selection_file::duration::MaybeDuration,
-    storage::video_database::extractor_hash::ExtractorHash,
-};
+use crate::{select::duration::MaybeDuration, storage::db::extractor_hash::ExtractorHash};
 
-pub mod downloader;
-pub mod extractor_hash;
-pub mod get;
-pub mod notify;
-pub mod set;
+pub(crate) mod comments;
+
+macro_rules! video_from_record {
+    ($record:expr) => {
+        $crate::storage::db::video::Video {
+            description: $record.description.clone(),
+            duration: $crate::select::duration::MaybeDuration::from_maybe_secs_f64(
+                $record.duration,
+            ),
+            extractor_hash: $crate::storage::db::extractor_hash::ExtractorHash::from_hash(
+                $record
+                    .extractor_hash
+                    .parse()
+                    .expect("The db hash should be a valid blake3 hash"),
+            ),
+            last_status_change: $crate::storage::db::video::TimeStamp::from_secs(
+                $record.last_status_change,
+            ),
+            parent_subscription_name: $record.parent_subscription_name.clone(),
+            publish_date: $record
+                .publish_date
+                .map(|pd| $crate::storage::db::video::TimeStamp::from_secs(pd)),
+            status: {
+                let marker =
+                    $crate::storage::db::video::VideoStatusMarker::from_db_integer($record.status);
+                let optional = if let Some(cache_path) = &$record.cache_path {
+                    Some((
+                        std::path::PathBuf::from(cache_path),
+                        if $record.is_focused == Some(1) {
+                            true
+                        } else {
+                            false
+                        },
+                    ))
+                } else {
+                    None
+                };
+                $crate::storage::db::video::VideoStatus::from_marker(marker, optional)
+            },
+            thumbnail_url: if let Some(url) = &$record.thumbnail_url {
+                Some(url::Url::parse(url).expect("Parsing this as url should always work"))
+            } else {
+                None
+            },
+            title: $record.title.clone(),
+            url: url::Url::parse(&$record.url).expect("Parsing this as url should always work"),
+            priority: $crate::storage::db::video::Priority::from($record.priority),
+            watch_progress: std::time::Duration::from_secs(
+                u64::try_from($record.watch_progress).expect("The record is positive i64"),
+            ),
+            subtitle_langs: $record.subtitle_langs.clone(),
+            playback_speed: $record.playback_speed,
+        }
+    };
+}
+pub(crate) use video_from_record;
 
 #[derive(Debug, Clone)]
-pub struct Video {
-    pub description: Option<String>,
-    pub duration: MaybeDuration,
-    pub extractor_hash: ExtractorHash,
-    pub last_status_change: TimeStamp,
+pub(crate) struct Video {
+    pub(crate) description: Option<String>,
+    pub(crate) duration: MaybeDuration,
+    pub(crate) extractor_hash: ExtractorHash,
+    pub(crate) last_status_change: TimeStamp,
 
     /// The associated subscription this video was fetched from (null, when the video was `add`ed)
-    pub parent_subscription_name: Option<String>,
-    pub priority: Priority,
-    pub publish_date: Option<TimeStamp>,
-    pub status: VideoStatus,
-    pub thumbnail_url: Option<Url>,
-    pub title: String,
-    pub url: Url,
+    pub(crate) parent_subscription_name: Option<String>,
+    pub(crate) priority: Priority,
+    pub(crate) publish_date: Option<TimeStamp>,
+    pub(crate) status: VideoStatus,
+    pub(crate) thumbnail_url: Option<Url>,
+    pub(crate) title: String,
+    pub(crate) url: Url,
 
     /// The seconds the user has already watched the video
-    pub watch_progress: Duration,
+    pub(crate) watch_progress: Duration,
+
+    /// Which subtitles to include, when downloading this video.
+    /// In the form of `lang1,lang2,lang3` (e.g. `en,de,sv`)
+    pub(crate) subtitle_langs: Option<String>,
+
+    /// The playback speed to use, when watching this video.
+    /// Value is in percent, so 1 is 100%, 2.7 is 270%, and so on.
+    pub(crate) playback_speed: Option<f64>,
 }
 
 /// The priority of a [`Video`].
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
-pub struct Priority {
+#[derive(Debug, Serialize, Deserialize, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
+pub(crate) struct Priority {
     value: i64,
 }
 impl Priority {
     /// Return the underlying value to insert that into the database
     #[must_use]
-    pub fn as_db_integer(&self) -> i64 {
+    pub(crate) fn as_db_integer(self) -> i64 {
         self.value
     }
 }
@@ -74,25 +126,25 @@ impl Display for Priority {
 
 /// An UNIX time stamp.
 #[derive(Debug, Default, Clone, Copy)]
-pub struct TimeStamp {
+pub(crate) struct TimeStamp {
     value: i64,
 }
 impl TimeStamp {
     /// Return the seconds since the UNIX epoch for this [`TimeStamp`].
     #[must_use]
-    pub fn as_secs(&self) -> i64 {
+    pub(crate) fn as_secs(self) -> i64 {
         self.value
     }
 
     /// Construct a [`TimeStamp`] from a count of seconds since the UNIX epoch.
     #[must_use]
-    pub fn from_secs(value: i64) -> Self {
+    pub(crate) fn from_secs(value: i64) -> Self {
         Self { value }
     }
 
     /// Construct a [`TimeStamp`] from the current time.
     #[must_use]
-    pub fn from_now() -> Self {
+    pub(crate) fn from_now() -> Self {
         Self {
             value: Utc::now().timestamp(),
         }
@@ -107,49 +159,6 @@ impl Display for TimeStamp {
     }
 }
 
-#[derive(Debug)]
-pub struct VideoOptions {
-    pub yt_dlp: YtDlpOptions,
-    pub mpv: MpvOptions,
-}
-impl VideoOptions {
-    pub(crate) fn new(subtitle_langs: String, playback_speed: f64) -> Self {
-        let yt_dlp = YtDlpOptions { subtitle_langs };
-        let mpv = MpvOptions { playback_speed };
-        Self { yt_dlp, mpv }
-    }
-
-    /// This will write out the options that are different from the defaults.
-    /// Beware, that this does not set the priority.
-    #[must_use]
-    pub fn to_cli_flags(self, app: &App) -> String {
-        let mut f = String::new();
-
-        if (self.mpv.playback_speed - app.config.select.playback_speed).abs() > f64::EPSILON {
-            write!(f, " --speed '{}'", self.mpv.playback_speed).expect("Works");
-        }
-        if self.yt_dlp.subtitle_langs != app.config.select.subtitle_langs {
-            write!(f, " --subtitle-langs '{}'", self.yt_dlp.subtitle_langs).expect("Works");
-        }
-
-        f.trim().to_owned()
-    }
-}
-
-#[derive(Debug, Clone, Copy)]
-/// Additionally settings passed to mpv on watch
-pub struct MpvOptions {
-    /// The playback speed. (1 is 100%, 2.7 is 270%, and so on)
-    pub playback_speed: f64,
-}
-
-#[derive(Debug)]
-/// Additionally configuration options, passed to yt-dlp on download
-pub struct YtDlpOptions {
-    /// In the form of `lang1,lang2,lang3` (e.g. `en,de,sv`)
-    pub subtitle_langs: String,
-}
-
 /// # Video Lifetime (words in <brackets> are commands):
 ///      <Pick>
 ///     /    \
@@ -158,8 +167,8 @@ pub struct YtDlpOptions {
 /// Cache                       // yt cache
 ///     |
 /// Watched                     // yt watch
-#[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
-pub enum VideoStatus {
+#[derive(Default, Serialize, Deserialize, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)]
+pub(crate) enum VideoStatus {
     #[default]
     Pick,
 
@@ -186,7 +195,10 @@ impl VideoStatus {
     /// # Panics
     /// Only if internal expectations fail.
     #[must_use]
-    pub fn from_marker(marker: VideoStatusMarker, optional: Option<(PathBuf, bool)>) -> Self {
+    pub(crate) fn from_marker(
+        marker: VideoStatusMarker,
+        optional: Option<(PathBuf, bool)>,
+    ) -> Self {
         match marker {
             VideoStatusMarker::Pick => Self::Pick,
             VideoStatusMarker::Watch => Self::Watch,
@@ -204,26 +216,9 @@ impl VideoStatus {
         }
     }
 
-    /// Turn the [`VideoStatus`] to its internal parts. This is only really useful for the database
-    /// functions.
-    #[must_use]
-    pub fn to_parts_for_db(self) -> (VideoStatusMarker, Option<(PathBuf, bool)>) {
-        match self {
-            VideoStatus::Pick => (VideoStatusMarker::Pick, None),
-            VideoStatus::Watch => (VideoStatusMarker::Watch, None),
-            VideoStatus::Cached {
-                cache_path,
-                is_focused,
-            } => (VideoStatusMarker::Cached, Some((cache_path, is_focused))),
-            VideoStatus::Watched => (VideoStatusMarker::Watched, None),
-            VideoStatus::Drop => (VideoStatusMarker::Drop, None),
-            VideoStatus::Dropped => (VideoStatusMarker::Dropped, None),
-        }
-    }
-
     /// Return the associated [`VideoStatusMarker`] for this [`VideoStatus`].
     #[must_use]
-    pub fn as_marker(&self) -> VideoStatusMarker {
+    pub(crate) fn as_marker(&self) -> VideoStatusMarker {
         match self {
             VideoStatus::Pick => VideoStatusMarker::Pick,
             VideoStatus::Watch => VideoStatusMarker::Watch,
@@ -237,7 +232,7 @@ impl VideoStatus {
 
 /// Unit only variant of [`VideoStatus`]
 #[derive(Default, Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
-pub enum VideoStatusMarker {
+pub(crate) enum VideoStatusMarker {
     #[default]
     Pick,
 
@@ -255,7 +250,7 @@ pub enum VideoStatusMarker {
 }
 
 impl VideoStatusMarker {
-    pub const ALL: &'static [Self; 6] = &[
+    pub(crate) const ALL: &'static [Self; 6] = &[
         Self::Pick,
         //
         Self::Watch,
@@ -267,7 +262,7 @@ impl VideoStatusMarker {
     ];
 
     #[must_use]
-    pub fn as_command(&self) -> &str {
+    pub(crate) fn as_command(&self) -> &str {
         // NOTE: Keep the serialize able variants synced with the main `select` function <2024-06-14>
         // Also try to ensure, that the strings have the same length
         match self {
@@ -281,7 +276,7 @@ impl VideoStatusMarker {
     }
 
     #[must_use]
-    pub fn as_db_integer(&self) -> i64 {
+    pub(crate) fn as_db_integer(self) -> i64 {
         // These numbers should not change their mapping!
         // Oh, and keep them in sync with the SQLite check constraint.
         match self {
@@ -296,7 +291,7 @@ impl VideoStatusMarker {
         }
     }
     #[must_use]
-    pub fn from_db_integer(num: i64) -> Self {
+    pub(crate) fn from_db_integer(num: i64) -> Self {
         match num {
             0 => Self::Pick,
 
@@ -314,7 +309,7 @@ impl VideoStatusMarker {
     }
 
     #[must_use]
-    pub fn as_str(&self) -> &'static str {
+    pub(crate) fn as_str(self) -> &'static str {
         match self {
             Self::Pick => "Pick",
 
diff --git a/yt/src/storage/migrate/mod.rs b/crates/yt/src/storage/migrate/mod.rs
index badeb6f..418c893 100644
--- a/yt/src/storage/migrate/mod.rs
+++ b/crates/yt/src/storage/migrate/mod.rs
@@ -21,8 +21,61 @@ use sqlx::{Sqlite, SqlitePool, Transaction, query};
 
 use crate::app::App;
 
+macro_rules! make_upgrade {
+    ($app:expr, $old_version:expr, $new_version:expr, $sql_name:expr) => {
+        add_error_context(
+            async {
+                let mut tx = $app
+                    .database
+                    .begin()
+                    .await
+                    .context("Failed to start the update transaction")?;
+                debug!("Migrating: {} -> {}", $old_version, $new_version);
+
+                sqlx::raw_sql(include_str!($sql_name))
+                    .execute(&mut *tx)
+                    .await
+                    .context("Failed to run the update sql script")?;
+
+                set_db_version(
+                    &mut tx,
+                    if $old_version == Self::Empty {
+                        // There is no previous version we would need to remove
+                        None
+                    } else {
+                        Some($old_version)
+                    },
+                    $new_version,
+                )
+                .await
+                .with_context(|| format!("Failed to set the new version ({})", $new_version))?;
+
+                tx.commit()
+                    .await
+                    .context("Failed to commit the update transaction")?;
+
+                // NOTE: This is needed, so that sqlite "sees" our changes to the table
+                // without having to reconnect. <2025-02-18>
+                query!("VACUUM")
+                    .execute(&$app.database)
+                    .await
+                    .context("Failed to vacuum database")?;
+
+                Ok(())
+            },
+            $new_version,
+        )
+        .await?;
+
+        Box::pin($new_version.update($app)).await.context(concat!(
+            "While updating to version: ",
+            stringify!($new_version)
+        ))
+    };
+}
+
 #[derive(Debug, Clone, Copy, PartialEq, PartialOrd)]
-pub enum DbVersion {
+pub(crate) enum DbVersion {
     /// The database is not yet initialized.
     Empty,
 
@@ -35,8 +88,17 @@ pub enum DbVersion {
 
     /// Introduced: 2025-02-18.
     Two,
+
+    /// Introduced: 2025-03-21.
+    Three,
+
+    /// Introduced: 2025-07-05.
+    Four,
+
+    /// Introduced: 2025-07-20.
+    Five,
 }
-const CURRENT_VERSION: DbVersion = DbVersion::Two;
+const CURRENT_VERSION: DbVersion = DbVersion::Five;
 
 async fn add_error_context(
     function: impl Future<Output = Result<()>>,
@@ -44,7 +106,7 @@ async fn add_error_context(
 ) -> Result<()> {
     function
         .await
-        .with_context(|| format!("Format failed to migrate database to version: {level}"))
+        .with_context(|| format!("Failed to migrate database to version: {level}"))
 }
 
 async fn set_db_version(
@@ -83,21 +145,32 @@ async fn set_db_version(
 impl DbVersion {
     fn as_sql_integer(self) -> i32 {
         match self {
-            DbVersion::Empty => unreachable!("A empty version does not have an associated integer"),
             DbVersion::Zero => 0,
             DbVersion::One => 1,
             DbVersion::Two => 2,
+            DbVersion::Three => 3,
+            DbVersion::Four => 4,
+            DbVersion::Five => 5,
+
+            DbVersion::Empty => unreachable!("A empty version does not have an associated integer"),
         }
     }
+
     fn from_db(number: i64, namespace: &str) -> Result<Self> {
         match (number, namespace) {
             (0, "yt") => Ok(DbVersion::Zero),
             (1, "yt") => Ok(DbVersion::One),
             (2, "yt") => Ok(DbVersion::Two),
+            (3, "yt") => Ok(DbVersion::Three),
+            (4, "yt") => Ok(DbVersion::Four),
+            (5, "yt") => Ok(DbVersion::Five),
 
             (0, other) => bail!("Db version is Zero, but got unknown namespace: '{other}'"),
             (1, other) => bail!("Db version is One, but got unknown namespace: '{other}'"),
             (2, other) => bail!("Db version is Two, but got unknown namespace: '{other}'"),
+            (3, other) => bail!("Db version is Three, but got unknown namespace: '{other}'"),
+            (4, other) => bail!("Db version is Four, but got unknown namespace: '{other}'"),
+            (5, other) => bail!("Db version is Five, but got unknown namespace: '{other}'"),
 
             (other, "yt") => bail!("Got unkown version for 'yt' namespace: {other}"),
             (num, nasp) => bail!("Got unkown version number ({num}) and namespace ('{nasp}')"),
@@ -111,126 +184,32 @@ impl DbVersion {
     #[allow(clippy::too_many_lines)]
     async fn update(self, app: &App) -> Result<()> {
         match self {
-            DbVersion::Empty => {
-                add_error_context(
-                    async {
-                        let mut tx = app
-                            .database
-                            .begin()
-                            .await
-                            .context("Failed to start transaction")?;
-                        debug!("Migrate: Empty -> Zero");
-
-                        sqlx::raw_sql(include_str!("./sql/00_empty_to_zero.sql"))
-                            .execute(&mut *tx)
-                            .await
-                            .context("Failed to execute sql update script")?;
-
-                        set_db_version(&mut tx, None, DbVersion::Zero)
-                            .await
-                            .context("Failed to set new version")?;
-
-                        tx.commit()
-                            .await
-                            .context("Failed to commit changes")?;
-
-                        // NOTE: This is needed, so that sqlite "sees" our changes to the table
-                        // without having to reconnect. <2025-02-18>
-                        query!("VACUUM")
-                            .execute(&app.database)
-                            .await
-                            .context("Failed to vacuum database")?;
-
-                        Ok(())
-                    },
-                    DbVersion::One,
-                )
-                .await?;
-                Box::pin(Self::Zero.update(app)).await
+            Self::Empty => {
+                make_upgrade! {app, Self::Empty, Self::Zero, "./sql/0_Empty_to_Zero.sql"}
             }
 
-            DbVersion::Zero => {
-                add_error_context(
-                    async {
-                        let mut tx = app
-                            .database
-                            .begin()
-                            .await
-                            .context("Failed to start transaction")?;
-                        debug!("Migrate: Zero -> One");
-
-                        sqlx::raw_sql(include_str!("./sql/01_zero_to_one.sql"))
-                            .execute(&mut *tx)
-                            .await
-                            .context("Failed to execute the update sql script")?;
-
-                        set_db_version(&mut tx, Some(DbVersion::Zero), DbVersion::One)
-                            .await
-                            .context("Failed to set the new version")?;
-
-                        tx.commit()
-                            .await
-                            .context("Failed to commit the update transaction")?;
-
-                        // NOTE: This is needed, so that sqlite "sees" our changes to the table
-                        // without having to reconnect. <2025-02-18>
-                        query!("VACUUM")
-                            .execute(&app.database)
-                            .await
-                            .context("Failed to vacuum database")?;
-
-                        Ok(())
-                    },
-                    DbVersion::Zero,
-                )
-                .await?;
+            Self::Zero => {
+                make_upgrade! {app, Self::Zero, Self::One, "./sql/1_Zero_to_One.sql"}
+            }
 
-                Box::pin(Self::One.update(app)).await
+            Self::One => {
+                make_upgrade! {app, Self::One, Self::Two, "./sql/2_One_to_Two.sql"}
             }
 
-            DbVersion::One => {
-                add_error_context(
-                    async {
-                        let mut tx = app
-                            .database
-                            .begin()
-                            .await
-                            .context("Failed to start the update transaction")?;
-                        debug!("Migrate: One -> Two");
-
-                        sqlx::raw_sql(include_str!("./sql/02_one_to_two.sql"))
-                            .execute(&mut *tx)
-                            .await
-                            .context("Failed to run the update sql script")?;
-
-                        set_db_version(&mut tx, Some(DbVersion::One), DbVersion::Two)
-                            .await
-                            .context("Failed to set the new version")?;
-
-                        tx.commit()
-                            .await
-                            .context("Failed to commit the update transaction")?;
-
-                        // NOTE: This is needed, so that sqlite "sees" our changes to the table
-                        // without having to reconnect. <2025-02-18>
-                        query!("VACUUM")
-                            .execute(&app.database)
-                            .await
-                            .context("Failed to vacuum database")?;
-
-                        Ok(())
-                    },
-                    DbVersion::One,
-                )
-                .await?;
+            Self::Two => {
+                make_upgrade! {app, Self::Two, Self::Three, "./sql/3_Two_to_Three.sql"}
+            }
 
-                Box::pin(Self::Two.update(app))
-                    .await
-                    .context("Failed to update to version: Three")
+            Self::Three => {
+                make_upgrade! {app, Self::Three, Self::Four, "./sql/4_Three_to_Four.sql"}
+            }
+
+            Self::Four => {
+                make_upgrade! {app, Self::Four, Self::Five, "./sql/5_Four_to_Five.sql"}
             }
 
             // This is the current_version
-            DbVersion::Two => {
+            Self::Five => {
                 assert_eq!(self, CURRENT_VERSION);
                 assert_eq!(self, get_version(app).await?);
                 Ok(())
@@ -263,9 +242,10 @@ fn get_current_date() -> i64 {
 ///
 /// # Panics
 /// Only if internal assertions fail.
-pub async fn get_version(app: &App) -> Result<DbVersion> {
+pub(crate) async fn get_version(app: &App) -> Result<DbVersion> {
     get_version_db(&app.database).await
 }
+
 /// Return the current database version.
 ///
 /// In contrast to the [`get_version`] function, this function does not
@@ -273,13 +253,19 @@ pub async fn get_version(app: &App) -> Result<DbVersion> {
 ///
 /// # Panics
 /// Only if internal assertions fail.
-pub async fn get_version_db(pool: &SqlitePool) -> Result<DbVersion> {
+pub(crate) async fn get_version_db(pool: &SqlitePool) -> Result<DbVersion> {
     let version_table_exists = {
         let query = query!(
-            "SELECT 1 as result FROM sqlite_master WHERE type = 'table' AND name = 'version'"
+            "
+            SELECT 1 as result
+            FROM sqlite_master
+            WHERE type = 'table'
+            AND name = 'version'
+            "
         )
         .fetch_optional(pool)
         .await?;
+
         if let Some(output) = query {
             assert_eq!(output.result, 1);
             true
@@ -287,13 +273,16 @@ pub async fn get_version_db(pool: &SqlitePool) -> Result<DbVersion> {
             false
         }
     };
+
     if !version_table_exists {
         return Ok(DbVersion::Empty);
     }
 
     let current_version = query!(
         "
-        SELECT namespace, number FROM version WHERE valid_to IS NULL;
+        SELECT namespace, number
+        FROM version
+        WHERE valid_to IS NULL;
         "
     )
     .fetch_one(pool)
@@ -303,7 +292,7 @@ pub async fn get_version_db(pool: &SqlitePool) -> Result<DbVersion> {
     DbVersion::from_db(current_version.number, current_version.namespace.as_str())
 }
 
-pub async fn migrate_db(app: &App) -> Result<()> {
+pub(crate) async fn migrate_db(app: &App) -> Result<()> {
     let current_version = get_version(app)
         .await
         .context("Failed to determine initial version")?;
diff --git a/yt/src/storage/migrate/sql/00_empty_to_zero.sql b/crates/yt/src/storage/migrate/sql/0_Empty_to_Zero.sql
index d703bfc..d703bfc 100644
--- a/yt/src/storage/migrate/sql/00_empty_to_zero.sql
+++ b/crates/yt/src/storage/migrate/sql/0_Empty_to_Zero.sql
diff --git a/yt/src/storage/migrate/sql/01_zero_to_one.sql b/crates/yt/src/storage/migrate/sql/1_Zero_to_One.sql
index da9315b..da9315b 100644
--- a/yt/src/storage/migrate/sql/01_zero_to_one.sql
+++ b/crates/yt/src/storage/migrate/sql/1_Zero_to_One.sql
diff --git a/yt/src/storage/migrate/sql/02_one_to_two.sql b/crates/yt/src/storage/migrate/sql/2_One_to_Two.sql
index 806de07..806de07 100644
--- a/yt/src/storage/migrate/sql/02_one_to_two.sql
+++ b/crates/yt/src/storage/migrate/sql/2_One_to_Two.sql
diff --git a/crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql b/crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql
new file mode 100644
index 0000000..b33f849
--- /dev/null
+++ b/crates/yt/src/storage/migrate/sql/3_Two_to_Three.sql
@@ -0,0 +1,85 @@
+-- yt - A fully featured command line YouTube client
+--
+-- Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+-- This file is part of Yt.
+--
+-- You should have received a copy of the License along with this program.
+-- If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+
+-- 1. Create new table
+-- 2. Copy data
+-- 3. Drop old table
+-- 4. Rename new into old
+
+-- remove the original TRANSACTION
+COMMIT TRANSACTION;
+
+-- tweak config
+PRAGMA foreign_keys=OFF;
+
+-- start your own TRANSACTION
+BEGIN TRANSACTION;
+
+CREATE TABLE videos_new (
+    cache_path                  TEXT    UNIQUE                       CHECK (CASE
+                                                                              WHEN cache_path IS NOT NULL THEN status == 2
+                                                                              ELSE 1
+                                                                            END),
+    description                 TEXT,
+    duration                    REAL,
+    extractor_hash              TEXT    UNIQUE NOT NULL PRIMARY KEY,
+    last_status_change          INTEGER        NOT NULL,
+    parent_subscription_name    TEXT,
+    priority                    INTEGER        NOT NULL DEFAULT 0,
+    publish_date                INTEGER,
+    status                      INTEGER        NOT NULL DEFAULT 0    CHECK (status IN (0, 1, 2, 3, 4, 5) AND
+                                                                            CASE
+                                                                              WHEN status == 2 THEN cache_path IS NOT NULL
+                                                                              WHEN status != 2 THEN cache_path IS NULL
+                                                                              ELSE 1
+                                                                            END),
+    thumbnail_url               TEXT,
+    title                       TEXT           NOT NULL,
+    url                         TEXT    UNIQUE NOT NULL,
+    is_focused                  INTEGER UNIQUE          DEFAULT NULL CHECK (CASE
+                                                                              WHEN is_focused IS NOT NULL THEN is_focused == 1
+                                                                              ELSE 1
+                                                                            END),
+    watch_progress              INTEGER        NOT NULL DEFAULT 0    CHECK (watch_progress <= duration)
+) STRICT;
+
+INSERT INTO videos_new SELECT
+    videos.cache_path,
+    videos.description,
+    videos.duration,
+    videos.extractor_hash,
+    videos.last_status_change,
+    videos.parent_subscription_name,
+    videos.priority,
+    videos.publish_date,
+    videos.status,
+    videos.thumbnail_url,
+    videos.title,
+    videos.url,
+    dummy.is_focused,
+    videos.watch_progress
+FROM videos, (SELECT NULL AS is_focused) AS dummy;
+
+DROP TABLE videos;
+
+ALTER TABLE videos_new RENAME TO videos;
+
+-- check foreign key constraint still upholding.
+PRAGMA foreign_key_check;
+
+-- commit your own TRANSACTION
+COMMIT TRANSACTION;
+
+-- rollback all config you setup before.
+PRAGMA foreign_keys=ON;
+
+-- start a new TRANSACTION to let migrator commit it.
+BEGIN TRANSACTION;
diff --git a/crates/yt/src/storage/migrate/sql/4_Three_to_Four.sql b/crates/yt/src/storage/migrate/sql/4_Three_to_Four.sql
new file mode 100644
index 0000000..9c283a1
--- /dev/null
+++ b/crates/yt/src/storage/migrate/sql/4_Three_to_Four.sql
@@ -0,0 +1,24 @@
+-- yt - A fully featured command line YouTube client
+--
+-- Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+-- This file is part of Yt.
+--
+-- You should have received a copy of the License along with this program.
+-- If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+ALTER TABLE videos
+ADD COLUMN subtitle_langs TEXT;
+
+ALTER TABLE videos
+ADD COLUMN playback_speed REAL CHECK (playback_speed >= 0);
+
+UPDATE videos
+   SET playback_speed = video_options.playback_speed,
+       subtitle_langs = video_options.subtitle_langs
+  FROM video_options
+ WHERE videos.extractor_hash = video_options.extractor_hash;
+
+
+DROP TABLE video_options;
diff --git a/crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql b/crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql
new file mode 100644
index 0000000..6c4b7cc
--- /dev/null
+++ b/crates/yt/src/storage/migrate/sql/5_Four_to_Five.sql
@@ -0,0 +1,15 @@
+-- yt - A fully featured command line YouTube client
+--
+-- Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+-- SPDX-License-Identifier: GPL-3.0-or-later
+--
+-- This file is part of Yt.
+--
+-- You should have received a copy of the License along with this program.
+-- If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+
+CREATE TABLE txn_log (
+    timestamp INTEGER NOT NULL,
+    operation TEXT    NOT NULL
+) STRICT;
diff --git a/yt/src/storage/mod.rs b/crates/yt/src/storage/mod.rs
index 8653eb3..6dcff74 100644
--- a/yt/src/storage/mod.rs
+++ b/crates/yt/src/storage/mod.rs
@@ -9,6 +9,6 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-pub mod subscriptions;
-pub mod video_database;
-pub mod migrate;
+pub(crate) mod db;
+pub(crate) mod migrate;
+pub(crate) mod notify;
diff --git a/yt/src/storage/video_database/notify.rs b/crates/yt/src/storage/notify.rs
index b55c00a..e0ee4e9 100644
--- a/yt/src/storage/video_database/notify.rs
+++ b/crates/yt/src/storage/notify.rs
@@ -26,7 +26,7 @@ use tokio::task;
 
 /// This functions registers a watcher for the database and only returns once a write was
 /// registered for the database.
-pub async fn wait_for_db_write(app: &App) -> Result<()> {
+pub(crate) async fn wait_for_db_write(app: &App) -> Result<()> {
     let db_path: PathBuf = app.config.paths.database_path.clone();
     task::spawn_blocking(move || wait_for_db_write_sync(&db_path)).await?
 }
@@ -53,7 +53,7 @@ fn wait_for_db_write_sync(db_path: &Path) -> Result<()> {
 }
 
 /// This functions registers a watcher for the cache path and returns once a file was removed
-pub async fn wait_for_cache_reduction(app: &App) -> Result<()> {
+pub(crate) async fn wait_for_cache_reduction(app: &App) -> Result<()> {
     let download_directory: PathBuf = app.config.paths.download_dir.clone();
     task::spawn_blocking(move || wait_for_cache_reduction_sync(&download_directory)).await?
 }
diff --git a/yt/src/version/mod.rs b/crates/yt/src/version/mod.rs
index 05d85e0..b12eadd 100644
--- a/yt/src/version/mod.rs
+++ b/crates/yt/src/version/mod.rs
@@ -8,27 +8,13 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-use std::process::Command;
-
 use anyhow::{Context, Result};
 use sqlx::{SqlitePool, sqlite::SqliteConnectOptions};
+use yt_dlp::options::YoutubeDLOptions;
 
 use crate::{config::Config, storage::migrate::get_version_db};
 
-fn get_cmd_version(cmd: &str) -> Result<String> {
-    let out = String::from_utf8(
-        Command::new(cmd)
-            .arg("--version")
-            .output()
-            .with_context(|| format!("Failed to run `{cmd} --version`"))?
-            .stdout,
-    )
-    .context("Failed to interpret output as utf8")?;
-
-    Ok(out.trim().to_owned())
-}
-
-pub async fn show(config: &Config) -> Result<()> {
+pub(crate) async fn show(config: &Config) -> Result<()> {
     let db_version = {
         let options = SqliteConnectOptions::new()
             .filename(&config.paths.database_path)
@@ -44,17 +30,20 @@ pub async fn show(config: &Config) -> Result<()> {
             .context("Failed to determine database version")?
     };
 
-    // TODO(@bpeetz): Use `pyo3`'s build in mechanism instead of executing the python CLI <2025-02-21>
-    let python_version = get_cmd_version("python")?;
-    let yt_dlp_version = get_cmd_version("yt-dlp")?;
+    let (yt_dlp, python) = {
+        let yt_dlp = YoutubeDLOptions::new().build()?;
+        yt_dlp.version()?
+    };
+
+    let python = python.replace('\n', " ");
 
     println!(
         "{}: {}
 
 db version: {db_version}
 
-python: {python_version}
-yt-dlp: {yt_dlp_version}",
+yt-dlp: {yt_dlp}
+python: {python}",
         env!("CARGO_PKG_NAME"),
         env!("CARGO_PKG_VERSION"),
     );
diff --git a/crates/yt/src/videos/format_video.rs b/crates/yt/src/videos/format_video.rs
new file mode 100644
index 0000000..6598780
--- /dev/null
+++ b/crates/yt/src/videos/format_video.rs
@@ -0,0 +1,133 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use anyhow::Result;
+use colors::Colorize;
+
+use crate::{app::App, output::format_text, storage::db::video::Video, videos::RenderWithApp};
+
+impl Video {
+    pub(crate) async fn to_info_display(
+        &self,
+        app: &App,
+        format: Option<String>,
+    ) -> Result<String> {
+        let cache_path = self.cache_path_fmt().to_string(app);
+        let description = self.description_fmt().to_string(app);
+        let duration = self.duration_fmt().to_string(app);
+        let extractor_hash = self.extractor_hash_fmt(app).await?.to_string(app);
+        let in_playlist = self.in_playlist_fmt().to_string(app);
+        let last_status_change = self.last_status_change_fmt().to_string(app);
+        let parent_subscription_name = self.parent_subscription_name_fmt().to_string(app);
+        let priority = self.priority_fmt().to_string(app);
+        let publish_date = self.publish_date_fmt().to_string(app);
+        let status = self.status_fmt().to_string(app);
+        let thumbnail_url = self.thumbnail_url_fmt().to_string(app);
+        let title = self.title_fmt().to_string(app);
+        let url = self.url_fmt().to_string(app);
+        let video_options = self.video_options_fmt(app).to_string(app);
+
+        let watched_percentage_fmt = {
+            if let Some(percent) = self.watch_progress_percent_fmt() {
+                format!(" (watched: {})", percent.to_string(app))
+            } else {
+                format!(" {}", self.watch_progress_fmt().to_string(app))
+            }
+        };
+
+        let options = video_options.to_string();
+        let options = options.trim();
+        let description = format_text(description.to_string().as_str(), None);
+
+        let string = if let Some(format) = format {
+            format
+                .replace("{title}", &title)
+                .replace("{extractor_hash}", &extractor_hash)
+                .replace("{cache_path}", &cache_path)
+                .replace("{duration}", &duration)
+                .replace("{watched_percentage_fmt}", &watched_percentage_fmt)
+                .replace("{parent_subscription_name}", &parent_subscription_name)
+                .replace("{priority}", &priority)
+                .replace("{publish_date}", &publish_date)
+                .replace("{status}", &status)
+                .replace("{last_status_change}", &last_status_change)
+                .replace("{in_playlist}", &in_playlist)
+                .replace("{thumbnail_url}", &thumbnail_url)
+                .replace("{url}", &url)
+                .replace("{options}", options)
+                .replace("{description}", &description)
+        } else {
+            format!(
+                "\
+{title} ({extractor_hash})
+| -> {cache_path}
+| -> {duration}{watched_percentage_fmt}
+| -> {parent_subscription_name}
+| -> priority: {priority}
+| -> {publish_date}
+| -> status: {status} since {last_status_change} ({in_playlist})
+| -> {thumbnail_url}
+| -> {url}
+| -> options: {options}
+{description}\n",
+            )
+        };
+        Ok(string)
+    }
+
+    pub(crate) async fn to_line_display(
+        &self,
+        app: &App,
+        format: Option<String>,
+    ) -> Result<String> {
+        let status = self.status_fmt().to_string(app);
+        let extractor_hash = self.extractor_hash_fmt(app).await?.to_string(app);
+        let title = self.title_fmt().to_string(app);
+        let publish_date = self.publish_date_fmt().to_string(app);
+        let parent_subscription_name = self.parent_subscription_name_fmt().to_string(app);
+        let duration = self.duration_fmt().to_string(app);
+        let url = self.url_fmt().to_string(app);
+
+        let f = if let Some(format) = format {
+            format
+                .replace("{status}", &status)
+                .replace("{extractor_hash}", &extractor_hash)
+                .replace("{title}", &title)
+                .replace("{publish_date}", &publish_date)
+                .replace("{parent_subscription_name}", &parent_subscription_name)
+                .replace("{duration}", &duration)
+                .replace("{url}", &url)
+        } else {
+            format!(
+                "{status} {extractor_hash} {title} {publish_date} {parent_subscription_name} {duration}"
+            )
+        };
+
+        Ok(f)
+    }
+
+    pub(crate) async fn to_select_file_display(&self, app: &App) -> Result<String> {
+        let f = format!(
+            r#"{}{} {} "{}" "{}" "{}" "{}" "{}"{}"#,
+            self.status_fmt().render(false),
+            self.video_options_fmt(app).render(false),
+            self.extractor_hash_fmt(app).await?.render(false),
+            self.title_fmt().render(false),
+            self.publish_date_fmt().render(false),
+            self.parent_subscription_name_fmt().render(false),
+            self.duration_fmt().render(false),
+            self.url_fmt().render(false),
+            '\n'
+        );
+
+        Ok(f)
+    }
+}
diff --git a/crates/yt/src/videos/mod.rs b/crates/yt/src/videos/mod.rs
new file mode 100644
index 0000000..c2f01fa
--- /dev/null
+++ b/crates/yt/src/videos/mod.rs
@@ -0,0 +1,213 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::fmt::Write;
+
+use anyhow::{Context, Result};
+use colors::{Colorize, IntoCanvas};
+use url::Url;
+
+use crate::{
+    app::App,
+    select::duration::MaybeDuration,
+    storage::db::video::{TimeStamp, Video, VideoStatus},
+};
+
+pub(crate) mod format_video;
+
+macro_rules! get {
+    ($value:expr, $key:ident, $name:expr, $code:tt) => {
+        if let Some(value) = &$value.$key {
+            $code(value)
+        } else {
+            concat!("[No ", $name, "]").to_owned()
+        }
+    };
+}
+
+pub(crate) trait RenderWithApp: Colorize {
+    fn to_string(self, app: &App) -> String {
+        self.render(app.config.global.display_colors)
+    }
+}
+impl<C: Colorize> RenderWithApp for C {}
+
+impl Video {
+    #[must_use]
+    pub(crate) fn cache_path_fmt(&self) -> impl Colorize {
+        let cache_path = if let VideoStatus::Cached {
+            cache_path,
+            is_focused: _,
+        } = &self.status
+        {
+            cache_path.to_string_lossy().to_string()
+        } else {
+            "[No Cache Path]".to_owned()
+        };
+
+        cache_path.blue().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn description_fmt(&self) -> impl Colorize {
+        get!(
+            self,
+            description,
+            "Description",
+            (|value: &str| value.to_owned())
+        )
+        .into_canvas()
+    }
+
+    #[must_use]
+    pub(crate) fn duration_fmt(&self) -> impl Colorize {
+        self.duration.cyan().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn watch_progress_fmt(&self) -> impl Colorize {
+        MaybeDuration::from_std(self.watch_progress).cyan().bold()
+    }
+    #[must_use]
+    pub(crate) fn watch_progress_percent_fmt(&self) -> Option<impl Colorize> {
+        self.duration.as_secs_f64().map(|duration| {
+            let watch_progress = self.watch_progress.as_secs_f64();
+
+            (format!("{:0.0}%", (watch_progress / duration) * 100.0)).into_canvas()
+        })
+    }
+
+    pub(crate) async fn extractor_hash_fmt(&self, app: &App) -> Result<impl Colorize> {
+        let hash = self
+            .extractor_hash
+            .as_short_hash(app)
+            .await
+            .with_context(|| {
+                format!(
+                    "Failed to format extractor hash, whilst formatting video: '{}'",
+                    self.title
+                )
+            })?;
+
+        Ok(hash.purple().bold().italic())
+    }
+
+    #[must_use]
+    pub(crate) fn in_playlist_fmt(&self) -> impl Colorize {
+        let output = match &self.status {
+            VideoStatus::Pick
+            | VideoStatus::Watch
+            | VideoStatus::Watched
+            | VideoStatus::Drop
+            | VideoStatus::Dropped => "Not in the playlist",
+            VideoStatus::Cached { is_focused, .. } => {
+                if *is_focused {
+                    "In the playlist and focused"
+                } else {
+                    "In the playlist"
+                }
+            }
+        };
+        output.yellow().italic()
+    }
+    #[must_use]
+    pub(crate) fn last_status_change_fmt(&self) -> impl Colorize {
+        self.last_status_change.bright_cyan()
+    }
+
+    #[must_use]
+    pub(crate) fn parent_subscription_name_fmt(&self) -> impl Colorize {
+        let psn = get!(
+            self,
+            parent_subscription_name,
+            "author",
+            (|sub: &str| sub.replace('"', "'"))
+        );
+
+        psn.bright_magenta()
+    }
+
+    #[must_use]
+    pub(crate) fn priority_fmt(&self) -> impl Colorize {
+        self.priority.into_canvas()
+    }
+
+    #[must_use]
+    pub(crate) fn publish_date_fmt(&self) -> impl Colorize {
+        let date = get!(
+            self,
+            publish_date,
+            "release date",
+            (|date: &TimeStamp| date.to_string())
+        );
+
+        date.bright_white().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn status_fmt(&self) -> impl Colorize {
+        // TODO: We might support `.trim()`ing that, as the extra whitespace could be bad in the
+        // selection file. <2024-10-07>
+        let status = self.status.as_marker().as_command().to_owned();
+
+        status.red().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn thumbnail_url_fmt(&self) -> impl Colorize {
+        get!(
+            self,
+            thumbnail_url,
+            "thumbnail URL",
+            (|url: &Url| url.to_string())
+        )
+        .into_canvas()
+    }
+
+    #[must_use]
+    pub(crate) fn title_fmt(&self) -> impl Colorize {
+        let title = self.title.replace(['"', '„', '”', '“'], "'");
+
+        title.green().bold()
+    }
+
+    #[must_use]
+    pub(crate) fn url_fmt(&self) -> impl Colorize {
+        let url = self.url.as_str().replace('"', "\\\"");
+
+        url.italic()
+    }
+
+    pub(crate) fn video_options_fmt(&self, app: &App) -> impl Colorize {
+        let video_options = {
+            let mut opts = String::new();
+
+            if let Some(playback_speed) = self.playback_speed {
+                if (playback_speed - app.config.select.playback_speed).abs() > f64::EPSILON {
+                    write!(opts, " --playback-speed '{}'", playback_speed).expect("In-memory");
+                }
+            }
+
+            if let Some(subtitle_langs) = &self.subtitle_langs {
+                if subtitle_langs != &app.config.select.subtitle_langs {
+                    write!(opts, " --subtitle-langs '{}'", subtitle_langs).expect("In-memory");
+                }
+            }
+
+            let opts = opts.trim().to_owned();
+
+            let opts_white = if opts.is_empty() { "" } else { " " };
+            format!("{opts_white}{opts}")
+        };
+
+        video_options.bright_green()
+    }
+}
diff --git a/crates/yt/src/yt_dlp/mod.rs b/crates/yt/src/yt_dlp/mod.rs
new file mode 100644
index 0000000..eaa80a1
--- /dev/null
+++ b/crates/yt/src/yt_dlp/mod.rs
@@ -0,0 +1,253 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{borrow::ToOwned, str::FromStr, time::Duration};
+
+use anyhow::{Context, Result};
+use chrono::{DateTime, Utc};
+use futures::{FutureExt, future::BoxFuture};
+use log::{error, warn};
+use serde_json::json;
+use tokio::{fs, io};
+use url::Url;
+use yt_dlp::{
+    YoutubeDL, info_json::InfoJson, json_cast, json_get, json_try_get, options::YoutubeDLOptions,
+};
+
+use crate::{
+    app::App,
+    select::duration::MaybeDuration,
+    shared::bytes::Bytes,
+    storage::db::{
+        extractor_hash::ExtractorHash,
+        subscription::Subscription,
+        video::{Priority, TimeStamp, Video, VideoStatus},
+    },
+};
+
+pub(crate) fn yt_dlp_opts_updating(max_backlog: usize) -> Result<YoutubeDL> {
+    Ok(YoutubeDLOptions::new()
+        .set("playliststart", 1)
+        .set("playlistend", max_backlog)
+        .set("noplaylist", false)
+        .set(
+            "extractor_args",
+            json! {{"youtubetab": {"approximate_date": [""]}}},
+        )
+        // // TODO: This also removes unlisted and other stuff. Find a good way to remove the
+        // // members-only videos from the feed. <2025-04-17>
+        // .set("match-filter", "availability=public")
+        .build()?)
+}
+
+impl Video {
+    pub(crate) fn get_approx_size(&self) -> Result<u64> {
+        let yt_dlp = {
+            YoutubeDLOptions::new()
+                .set("prefer_free_formats", true)
+                .set("format", "bestvideo[height<=?1080]+bestaudio/best")
+                .set("fragment_retries", 10)
+                .set("retries", 10)
+                .set("getcomments", false)
+                .set("ignoreerrors", false)
+                .build()
+                .context("Failed to instanciate get approx size yt_dlp")
+        }?;
+
+        let result = yt_dlp
+            .extract_info(&self.url, false, true)
+            .with_context(|| format!("Failed to extract video information: '{}'", self.title))?;
+
+        let size = if let Some(filesize) = json_try_get!(result, "filesize", as_u64) {
+            filesize
+        } else if let Some(num) = json_try_get!(result, "filesize_approx", as_u64) {
+            num
+        } else if let Some(duration) = json_try_get!(result, "duration", as_f64)
+            && let Some(tbr) = json_try_get!(result, "tbr", as_f64)
+        {
+            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+            let duration = duration.ceil() as u64;
+
+            // TODO: yt_dlp gets this from the format
+            #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
+            let tbr = tbr.ceil() as u64;
+
+            duration * tbr * (1000 / 8)
+        } else {
+            let hardcoded_default = Bytes::from_str("250 MiB").expect("This is hardcoded");
+            error!(
+                "Failed to find a filesize for video: {:?} (Using hardcoded value of {})",
+                self.title, hardcoded_default
+            );
+            hardcoded_default.as_u64()
+        };
+
+        Ok(size)
+    }
+}
+
+impl Video {
+    #[allow(clippy::too_many_lines)]
+    pub(crate) fn from_info_json(entry: &InfoJson, sub: Option<&Subscription>) -> Result<Video> {
+        fn fmt_context(date: &str, extended: Option<&str>) -> String {
+            let f = format!(
+                "Failed to parse the `upload_date` of the entry ('{date}'). \
+                    Expected `YYYY-MM-DD`, has the format changed?"
+            );
+            if let Some(date_string) = extended {
+                format!("{f}\nThe parsed '{date_string}' can't be turned to a valid UTC date.'")
+            } else {
+                f
+            }
+        }
+
+        let publish_date = if let Some(date) = json_try_get!(entry, "upload_date", as_str) {
+            let year: u32 = date
+                .chars()
+                .take(4)
+                .collect::<String>()
+                .parse()
+                .with_context(|| fmt_context(date, None))?;
+            let month: u32 = date
+                .chars()
+                .skip(4)
+                .take(2)
+                .collect::<String>()
+                .parse()
+                .with_context(|| fmt_context(date, None))?;
+            let day: u32 = date
+                .chars()
+                .skip(4 + 2)
+                .take(2)
+                .collect::<String>()
+                .parse()
+                .with_context(|| fmt_context(date, None))?;
+
+            let date_string = format!("{year:04}-{month:02}-{day:02}T00:00:00Z");
+            Some(
+                DateTime::<Utc>::from_str(&date_string)
+                    .with_context(|| fmt_context(date, Some(&date_string)))?
+                    .timestamp(),
+            )
+        } else {
+            warn!(
+                "The video '{}' lacks it's upload date!",
+                json_get!(entry, "title", as_str)
+            );
+            None
+        };
+
+        let thumbnail_url = match (
+            &json_try_get!(entry, "thumbnails", as_array),
+            &json_try_get!(entry, "thumbnail", as_str),
+        ) {
+            (None, None) => None,
+            (None, Some(thumbnail)) => Some(Url::from_str(thumbnail)?),
+
+            // TODO: The algorithm is not exactly the best <2024-05-28>
+            (Some(thumbnails), None) => {
+                if let Some(thumbnail) = thumbnails.first() {
+                    Some(Url::from_str(json_get!(
+                        json_cast!(thumbnail, as_object),
+                        "url",
+                        as_str
+                    ))?)
+                } else {
+                    None
+                }
+            }
+            (Some(_), Some(thumnail)) => Some(Url::from_str(thumnail)?),
+        };
+
+        let url = {
+            let smug_url: Url = json_get!(entry, "webpage_url", as_str).parse()?;
+            // TODO(@bpeetz): We should probably add this? <2025-06-14>
+            // if '#__youtubedl_smuggle' not in smug_url:
+            //     return smug_url, default
+            // url, _, sdata = smug_url.rpartition('#')
+            // jsond = urllib.parse.parse_qs(sdata)['__youtubedl_smuggle'][0]
+            // data = json.loads(jsond)
+            // return url, data
+
+            smug_url
+        };
+
+        let extractor_hash = ExtractorHash::from_info_json(entry);
+
+        let subscription_name = if let Some(sub) = sub {
+            Some(sub.name.clone())
+        } else if let Some(uploader) = json_try_get!(entry, "uploader", as_str) {
+            if json_try_get!(entry, "webpage_url_domain", as_str) == Some("youtube.com") {
+                Some(format!("{uploader} - Videos"))
+            } else {
+                Some(uploader.to_owned())
+            }
+        } else {
+            None
+        };
+
+        let video = Video {
+            description: json_try_get!(entry, "description", as_str).map(ToOwned::to_owned),
+            duration: MaybeDuration::from_maybe_secs_f64(json_try_get!(entry, "duration", as_f64)),
+            extractor_hash,
+            last_status_change: TimeStamp::from_now(),
+            parent_subscription_name: subscription_name,
+            priority: Priority::default(),
+            publish_date: publish_date.map(TimeStamp::from_secs),
+            status: VideoStatus::Pick,
+            thumbnail_url,
+            title: json_get!(entry, "title", as_str).to_owned(),
+            url,
+            watch_progress: Duration::default(),
+            playback_speed: None,
+            subtitle_langs: None,
+        };
+        Ok(video)
+    }
+}
+
+pub(crate) async fn get_current_cache_allocation(app: &App) -> Result<Bytes> {
+    fn dir_size(mut dir: fs::ReadDir) -> BoxFuture<'static, Result<Bytes>> {
+        async move {
+            let mut acc = 0;
+            while let Some(entry) = dir.next_entry().await? {
+                let size = match entry.metadata().await? {
+                    data if data.is_dir() => {
+                        let path = entry.path();
+                        let read_dir = fs::read_dir(path).await?;
+
+                        dir_size(read_dir).await?.as_u64()
+                    }
+                    data => data.len(),
+                };
+                acc += size;
+            }
+            Ok(Bytes::new(acc))
+        }
+        .boxed()
+    }
+
+    let read_dir_result = match fs::read_dir(&app.config.paths.download_dir).await {
+        Ok(ok) => ok,
+        Err(err) => match err.kind() {
+            io::ErrorKind::NotFound => {
+                unreachable!("The download dir should always be created in the config finalizers.");
+            }
+            err => Err(io::Error::from(err)).with_context(|| {
+                format!(
+                    "Failed to get dir size of download dir at: '{}'",
+                    &app.config.paths.download_dir.display()
+                )
+            })?,
+        },
+    };
+
+    dir_size(read_dir_result).await
+}
diff --git a/crates/yt/tests/_testenv/init.rs b/crates/yt/tests/_testenv/init.rs
new file mode 100644
index 0000000..5970c7c
--- /dev/null
+++ b/crates/yt/tests/_testenv/init.rs
@@ -0,0 +1,136 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    env,
+    fs::{self, OpenOptions},
+    io::{self, Write},
+    path::PathBuf,
+};
+
+use crate::{_testenv::Paths, testenv::TestEnv};
+
+fn target_dir() -> PathBuf {
+    // Tests exe is in target/debug/deps, the *yt* exe is in target/debug
+    env::current_exe()
+        .expect("./target/debug/deps/yt-*")
+        .parent()
+        .expect("./target/debug/deps")
+        .parent()
+        .expect("./target/debug")
+        .parent()
+        .expect("./target")
+        .to_path_buf()
+}
+
+fn test_dir(name: &'static str) -> PathBuf {
+    target_dir().join("tests").join(name)
+}
+
+fn prepare_files_and_dirs(name: &'static str) -> io::Result<Paths> {
+    let test_dir = test_dir(name);
+
+    fs::create_dir_all(&test_dir)?;
+
+    let db_path = test_dir.join("database.sqlite");
+    let last_selection_path = test_dir.join("last_selection");
+    let config_path = test_dir.join("config.toml");
+    let download_dir = test_dir.join("download");
+
+    {
+        // Remove all files, that are not the download dir.
+        for entry in fs::read_dir(test_dir).expect("Works") {
+            let entry = entry.unwrap();
+            let entry_ft = entry.file_type().unwrap();
+
+            if entry_ft.is_dir() && entry.file_name() == "download" {
+                continue;
+            }
+
+            if entry_ft.is_dir() {
+                fs::remove_dir_all(entry.path()).unwrap();
+            } else {
+                fs::remove_file(entry.path()).unwrap();
+            }
+        }
+    }
+
+    fs::create_dir_all(&download_dir)?;
+
+    {
+        let mut config_file = OpenOptions::new()
+            .write(true)
+            .truncate(true)
+            .create(true)
+            .open(&config_path)?;
+
+        writeln!(
+            config_file,
+            r#"[paths]
+download_dir = "{}"
+mpv_config_path = "/dev/null"
+mpv_input_path = "/dev/null"
+database_path = "{}"
+last_selection_path = "{}"
+
+[download]
+max_cache_size = "100GiB"
+
+[update]
+max_backlog = 1
+"#,
+            download_dir.display(),
+            db_path.display(),
+            last_selection_path.display(),
+        )?;
+        config_file.flush()?;
+    }
+
+    Ok(Paths {
+        db: db_path,
+        last_selection: last_selection_path,
+        config: config_path,
+        download_dir,
+    })
+}
+
+/// Find the *yt* executable.
+fn find_yt_exe() -> PathBuf {
+    let target = target_dir().join("debug");
+
+    let exe_name = if cfg!(windows) { "yt.exe" } else { "yt" };
+
+    target.join(exe_name)
+}
+
+impl TestEnv {
+    pub(crate) fn new(name: &'static str) -> TestEnv {
+        let yt_exe = find_yt_exe();
+        let test_dir = test_dir(name);
+
+        let paths = prepare_files_and_dirs(name).expect("config dir");
+
+        TestEnv {
+            name,
+            yt_exe,
+            test_dir,
+            paths,
+            spawned_childs: vec![],
+        }
+    }
+}
+
+impl Drop for TestEnv {
+    fn drop(&mut self) {
+        for child in &mut self.spawned_childs {
+            drop(child.kill());
+        }
+    }
+}
diff --git a/crates/yt/tests/_testenv/mod.rs b/crates/yt/tests/_testenv/mod.rs
new file mode 100644
index 0000000..38d1f0a
--- /dev/null
+++ b/crates/yt/tests/_testenv/mod.rs
@@ -0,0 +1,35 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+//! This code was taken from *fd* at 30-06-2025.
+
+use std::{path::PathBuf, process};
+
+mod init;
+mod run;
+pub(crate) mod util;
+
+/// Environment for the integration tests.
+pub(crate) struct TestEnv {
+    pub(crate) name: &'static str,
+    pub(crate) yt_exe: PathBuf,
+    pub(crate) test_dir: PathBuf,
+
+    pub(crate) paths: Paths,
+
+    pub(crate) spawned_childs: Vec<process::Child>,
+}
+
+pub(crate) struct Paths {
+    pub(crate) db: PathBuf,
+    pub(crate) last_selection: PathBuf,
+    pub(crate) config: PathBuf,
+    pub(crate) download_dir: PathBuf,
+}
diff --git a/crates/yt/tests/_testenv/run.rs b/crates/yt/tests/_testenv/run.rs
new file mode 100644
index 0000000..578d823
--- /dev/null
+++ b/crates/yt/tests/_testenv/run.rs
@@ -0,0 +1,183 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    collections::HashMap,
+    process::{self, Stdio},
+};
+
+use colors::{Colorize, IntoCanvas};
+
+use crate::testenv::TestEnv;
+
+/// Format an error message for when *yt* did not exit successfully.
+fn format_exit_error(args: &[&str], output: &process::Output) -> String {
+    format!(
+        "`yt {}` did not exit successfully.\nstdout:\n---\n{}---\nstderr:\n---\n{}---",
+        args.join(" "),
+        String::from_utf8_lossy(&output.stdout),
+        String::from_utf8_lossy(&output.stderr)
+    )
+}
+
+/// Format an error message for when the output of *yt* did not match the expected output.
+fn format_output_error(args: &[&str], expected: &str, actual: &str) -> String {
+    fn normalize_str(input: &str) -> &str {
+        if input.is_empty() { "<Empty>" } else { input }
+    }
+
+    let expected = normalize_str(expected);
+    let actual = normalize_str(actual);
+
+    format!(
+        concat!(
+            "`yt {}` did not produce the expected output.\n",
+            "expected:\n---\n{}\n---\n and actual:\n---\n{}\n---\n"
+        ),
+        args.join(" "),
+        expected,
+        actual
+    )
+}
+
+/// Normalize the output for comparison.
+fn normalize_output(s: &str, trim_start: bool, normalize_line: bool) -> String {
+    // Split into lines and normalize separators.
+    let mut lines = s
+        .replace('\0', "NULL\n")
+        .lines()
+        .map(|line| {
+            let line = if trim_start { line.trim_start() } else { line };
+            let line = line.replace('/', std::path::MAIN_SEPARATOR_STR);
+            if normalize_line {
+                let mut words: Vec<_> = line.split_whitespace().collect();
+                words.sort_unstable();
+                return words.join(" ");
+            }
+            line
+        })
+        .collect::<Vec<_>>();
+
+    lines.sort();
+    lines.join("\n")
+}
+
+impl TestEnv {
+    /// Assert that calling *yt* in the specified path under the root working directory,
+    /// and with the specified arguments produces the expected output.
+    pub(crate) fn assert_output(&self, args: &[&str], expected: &str) {
+        let expected = normalize_output(expected, true, false);
+        let actual = self.run(args);
+
+        assert!(
+            expected == actual,
+            "{}",
+            format_output_error(args, &expected, &actual)
+        );
+    }
+
+    /// Run *yt* once with the first args, pipe the output of this command to *yt* with the second
+    /// args and return the normalized output.
+    pub(crate) fn run_piped(&self, first_args: &[&str], second_args: &[&str]) -> String {
+        let mut first_cmd = self.prepare_yt(first_args);
+        let mut second_cmd = self.prepare_yt(second_args);
+
+        first_cmd.stdout(Stdio::piped());
+        let mut first_child = first_cmd.spawn().expect("yt spawn");
+
+        second_cmd.stdin(first_child.stdout.take().expect("Was set"));
+
+        let first_output = first_child.wait_with_output().expect("yt run");
+        assert!(
+            first_output.status.success(),
+            "{}",
+            format_exit_error(first_args, &first_output)
+        );
+
+        Self::finalize_cmd(second_cmd, second_args)
+    }
+
+    /// Run *yt*, with the given args.
+    /// Returns the normalized stdout output.
+    pub(crate) fn run(&self, args: &[&str]) -> String {
+        let cmd = self.prepare_yt(args);
+        Self::finalize_cmd(cmd, args)
+    }
+
+    /// Run *yt*, with the given args and environment variables.
+    /// Returns the normalized stdout output.
+    pub(crate) fn run_env(&self, args: &[&str], env: &HashMap<&str, &str>) -> String {
+        let mut cmd = self.prepare_yt(args);
+        cmd.envs(env.iter());
+        Self::finalize_cmd(cmd, args)
+    }
+
+    /// Run *yt*, with the given args and fork into the background.
+    /// Returns a sender for the lines on stdout.
+    pub(crate) fn run_background(&mut self, args: &[&str]) -> process::ChildStdout {
+        let mut cmd = self.prepare_yt(args);
+
+        cmd.stdout(Stdio::piped());
+        cmd.stderr(Stdio::inherit());
+
+        // The whole point of this function, is calling a command and keep it running for the whole
+        // programs run-time.
+        //
+        // And we provide a clean-up mechanism.
+        #[allow(clippy::zombie_processes)]
+        let mut child = cmd.spawn().expect("yt spawn");
+
+        let stdout = child.stdout.take().expect("Was piped");
+
+        self.spawned_childs.push(child);
+
+        stdout
+    }
+
+    fn finalize_cmd(mut cmd: process::Command, args: &[&str]) -> String {
+        let child = cmd.spawn().expect("yt spawn");
+        let output = child.wait_with_output().expect("yt output");
+
+        assert!(
+            output.status.success(),
+            "{}",
+            format_exit_error(args, &output)
+        );
+
+        normalize_output(&String::from_utf8_lossy(&output.stdout), false, false)
+    }
+
+    fn prepare_yt(&self, args: &[&str]) -> process::Command {
+        let mut cmd = process::Command::new(&self.yt_exe);
+
+        cmd.current_dir(&self.test_dir);
+
+        cmd.args([
+            "-v",
+            "--color",
+            "false",
+            "--config-path",
+            self.paths.config.to_str().unwrap(),
+        ]);
+
+        cmd.stdout(Stdio::piped());
+        cmd.stderr(Stdio::piped());
+
+        cmd.args(args);
+
+        eprintln!(
+            "{} `yt {}`",
+            self.name.blue().italic().render(true),
+            args.join(" ")
+        );
+
+        cmd
+    }
+}
diff --git a/crates/yt/tests/_testenv/util.rs b/crates/yt/tests/_testenv/util.rs
new file mode 100644
index 0000000..6633fbf
--- /dev/null
+++ b/crates/yt/tests/_testenv/util.rs
@@ -0,0 +1,371 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::_testenv::TestEnv;
+
+use std::{
+    collections::HashMap,
+    fs::{OpenOptions, Permissions},
+    io::Write,
+    os::unix::fs::PermissionsExt,
+    path::PathBuf,
+};
+
+pub(crate) fn get_first_hash(env: &TestEnv) -> String {
+    let output = env.run(&["videos", "ls", "--format", "{extractor_hash}"]);
+
+    let first_hash = output.lines().next().unwrap();
+    first_hash.to_owned()
+}
+
+#[derive(Clone, Copy)]
+pub(crate) enum Subscription {
+    Tagesschau,
+}
+
+impl Subscription {
+    const fn as_url(self) -> &'static str {
+        match self {
+            Subscription::Tagesschau => {
+                "https://www.ardmediathek.de/sendung/tagesschau/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXU"
+            }
+        }
+    }
+
+    const fn as_name(self) -> &'static str {
+        match self {
+            Subscription::Tagesschau => "Tagesschau",
+        }
+    }
+}
+
+/// Provide the given number of videos.
+pub(crate) fn provide_videos(env: &TestEnv, sub: Subscription, num: u8) {
+    add_sub(env, sub);
+    update_sub(env, num, sub);
+}
+
+pub(crate) fn add_sub(env: &TestEnv, sub: Subscription) {
+    env.run(&[
+        "subs",
+        "add",
+        sub.as_url(),
+        "--name",
+        sub.as_name(),
+        "--no-check",
+    ]);
+}
+
+pub(crate) fn update_sub(env: &TestEnv, num_of_videos: u8, sub: Subscription) {
+    let num_of_videos: &str = u8_to_char(num_of_videos);
+
+    env.run(&["update", "--max-backlog", num_of_videos, sub.as_name()]);
+}
+
+pub(crate) fn run_select(env: &TestEnv, sed_regex: &str) {
+    let mut map = HashMap::new();
+
+    let command = make_command(
+        env,
+        format!(r#"sed --in-place '{sed_regex}' "$1""#).as_str(),
+    );
+    map.insert("EDITOR", command.to_str().unwrap());
+
+    env.run_env(&["select", "file", "--done"], &map);
+}
+
+fn make_command(env: &TestEnv, shell_command: &str) -> PathBuf {
+    let command_path = env.test_dir.join("command.sh");
+
+    {
+        let mut file = OpenOptions::new()
+            .write(true)
+            .create(true)
+            .truncate(true)
+            .open(&command_path)
+            .unwrap();
+
+        writeln!(file, "#!/usr/bin/env sh").unwrap();
+
+        file.write_all(shell_command.as_bytes()).unwrap();
+        file.flush().unwrap();
+
+        {
+            let perms = Permissions::from_mode(0o0700);
+            file.set_permissions(perms).unwrap();
+        }
+    }
+
+    command_path
+}
+
+// Char conversion {{{
+#[allow(clippy::too_many_lines)]
+const fn u8_to_char(input: u8) -> &'static str {
+    match input {
+        0 => "0",
+        1 => "1",
+        2 => "2",
+        3 => "3",
+        4 => "4",
+        5 => "5",
+        6 => "6",
+        7 => "7",
+        8 => "8",
+        9 => "9",
+        10 => "10",
+        11 => "11",
+        12 => "12",
+        13 => "13",
+        14 => "14",
+        15 => "15",
+        16 => "16",
+        17 => "17",
+        18 => "18",
+        19 => "19",
+        20 => "20",
+        21 => "21",
+        22 => "22",
+        23 => "23",
+        24 => "24",
+        25 => "25",
+        26 => "26",
+        27 => "27",
+        28 => "28",
+        29 => "29",
+        30 => "30",
+        31 => "31",
+        32 => "32",
+        33 => "33",
+        34 => "34",
+        35 => "35",
+        36 => "36",
+        37 => "37",
+        38 => "38",
+        39 => "39",
+        40 => "40",
+        41 => "41",
+        42 => "42",
+        43 => "43",
+        44 => "44",
+        45 => "45",
+        46 => "46",
+        47 => "47",
+        48 => "48",
+        49 => "49",
+        50 => "50",
+        51 => "51",
+        52 => "52",
+        53 => "53",
+        54 => "54",
+        55 => "55",
+        56 => "56",
+        57 => "57",
+        58 => "58",
+        59 => "59",
+        60 => "60",
+        61 => "61",
+        62 => "62",
+        63 => "63",
+        64 => "64",
+        65 => "65",
+        66 => "66",
+        67 => "67",
+        68 => "68",
+        69 => "69",
+        70 => "70",
+        71 => "71",
+        72 => "72",
+        73 => "73",
+        74 => "74",
+        75 => "75",
+        76 => "76",
+        77 => "77",
+        78 => "78",
+        79 => "79",
+        80 => "80",
+        81 => "81",
+        82 => "82",
+        83 => "83",
+        84 => "84",
+        85 => "85",
+        86 => "86",
+        87 => "87",
+        88 => "88",
+        89 => "89",
+        90 => "90",
+        91 => "91",
+        92 => "92",
+        93 => "93",
+        94 => "94",
+        95 => "95",
+        96 => "96",
+        97 => "97",
+        98 => "98",
+        99 => "99",
+        100 => "100",
+        101 => "101",
+        102 => "102",
+        103 => "103",
+        104 => "104",
+        105 => "105",
+        106 => "106",
+        107 => "107",
+        108 => "108",
+        109 => "109",
+        110 => "110",
+        111 => "111",
+        112 => "112",
+        113 => "113",
+        114 => "114",
+        115 => "115",
+        116 => "116",
+        117 => "117",
+        118 => "118",
+        119 => "119",
+        120 => "120",
+        121 => "121",
+        122 => "122",
+        123 => "123",
+        124 => "124",
+        125 => "125",
+        126 => "126",
+        127 => "127",
+        128 => "128",
+        129 => "129",
+        130 => "130",
+        131 => "131",
+        132 => "132",
+        133 => "133",
+        134 => "134",
+        135 => "135",
+        136 => "136",
+        137 => "137",
+        138 => "138",
+        139 => "139",
+        140 => "140",
+        141 => "141",
+        142 => "142",
+        143 => "143",
+        144 => "144",
+        145 => "145",
+        146 => "146",
+        147 => "147",
+        148 => "148",
+        149 => "149",
+        150 => "150",
+        151 => "151",
+        152 => "152",
+        153 => "153",
+        154 => "154",
+        155 => "155",
+        156 => "156",
+        157 => "157",
+        158 => "158",
+        159 => "159",
+        160 => "160",
+        161 => "161",
+        162 => "162",
+        163 => "163",
+        164 => "164",
+        165 => "165",
+        166 => "166",
+        167 => "167",
+        168 => "168",
+        169 => "169",
+        170 => "170",
+        171 => "171",
+        172 => "172",
+        173 => "173",
+        174 => "174",
+        175 => "175",
+        176 => "176",
+        177 => "177",
+        178 => "178",
+        179 => "179",
+        180 => "180",
+        181 => "181",
+        182 => "182",
+        183 => "183",
+        184 => "184",
+        185 => "185",
+        186 => "186",
+        187 => "187",
+        188 => "188",
+        189 => "189",
+        190 => "190",
+        191 => "191",
+        192 => "192",
+        193 => "193",
+        194 => "194",
+        195 => "195",
+        196 => "196",
+        197 => "197",
+        198 => "198",
+        199 => "199",
+        200 => "200",
+        201 => "201",
+        202 => "202",
+        203 => "203",
+        204 => "204",
+        205 => "205",
+        206 => "206",
+        207 => "207",
+        208 => "208",
+        209 => "209",
+        210 => "210",
+        211 => "211",
+        212 => "212",
+        213 => "213",
+        214 => "214",
+        215 => "215",
+        216 => "216",
+        217 => "217",
+        218 => "218",
+        219 => "219",
+        220 => "220",
+        221 => "221",
+        222 => "222",
+        223 => "223",
+        224 => "224",
+        225 => "225",
+        226 => "226",
+        227 => "227",
+        228 => "228",
+        229 => "229",
+        230 => "230",
+        231 => "231",
+        232 => "232",
+        233 => "233",
+        234 => "234",
+        235 => "235",
+        236 => "236",
+        237 => "237",
+        238 => "238",
+        239 => "239",
+        240 => "240",
+        241 => "241",
+        242 => "242",
+        243 => "243",
+        244 => "244",
+        245 => "245",
+        246 => "246",
+        247 => "247",
+        248 => "248",
+        249 => "249",
+        250 => "250",
+        251 => "251",
+        252 => "252",
+        253 => "253",
+        254 => "254",
+        255 => "255",
+    }
+}
+// }}}
diff --git a/crates/yt/tests/select/base.rs b/crates/yt/tests/select/base.rs
new file mode 100644
index 0000000..24e198b
--- /dev/null
+++ b/crates/yt/tests/select/base.rs
@@ -0,0 +1,50 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{collections::HashMap, fs};
+
+use crate::{
+    _testenv::{
+        TestEnv,
+        util::{self, Subscription},
+    },
+    select::get_videos_in_state,
+};
+
+#[test]
+fn test_base() {
+    let env = TestEnv::new(module_path!());
+
+    util::provide_videos(&env, Subscription::Tagesschau, 4);
+
+    let first_hash = &util::get_first_hash(&env);
+    env.run(&["select", "drop", first_hash]);
+
+    let mut map = HashMap::new();
+    map.insert("EDITOR", "true");
+    env.run_env(&["select", "file", "--done"], &map);
+
+    fs::remove_file(&env.paths.last_selection).unwrap();
+
+    env.run_env(&["select", "split", "--done"], &map);
+    assert_states(&env);
+
+    env.run_env(&["select", "file", "--use-last-selection"], &map);
+    assert_states(&env);
+}
+
+fn assert_states(env: &TestEnv) {
+    assert_eq!(get_videos_in_state(env, "Picked"), 3);
+    assert_eq!(get_videos_in_state(env, "Drop"), 1);
+
+    assert_eq!(get_videos_in_state(env, "Watch"), 0);
+    assert_eq!(get_videos_in_state(env, "Cached"), 0);
+    assert_eq!(get_videos_in_state(env, "Watched"), 0);
+}
diff --git a/crates/yt/tests/select/file.rs b/crates/yt/tests/select/file.rs
new file mode 100644
index 0000000..b8bd2b5
--- /dev/null
+++ b/crates/yt/tests/select/file.rs
@@ -0,0 +1,31 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{
+    _testenv::util::{self, Subscription},
+    select::get_videos_in_state,
+    testenv::TestEnv,
+};
+
+#[test]
+fn test_file() {
+    let env = TestEnv::new(module_path!());
+
+    util::provide_videos(&env, Subscription::Tagesschau, 4);
+
+    util::run_select(&env, "s/pick/watch/");
+
+    assert_eq!(get_videos_in_state(&env, "Picked"), 0);
+    assert_eq!(get_videos_in_state(&env, "Drop"), 0);
+
+    assert_eq!(get_videos_in_state(&env, "Watch"), 4);
+    assert_eq!(get_videos_in_state(&env, "Cached"), 0);
+    assert_eq!(get_videos_in_state(&env, "Watched"), 0);
+}
diff --git a/crates/yt/tests/select/mod.rs b/crates/yt/tests/select/mod.rs
new file mode 100644
index 0000000..d7033f8
--- /dev/null
+++ b/crates/yt/tests/select/mod.rs
@@ -0,0 +1,25 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::_testenv::TestEnv;
+
+mod base;
+mod file;
+mod options;
+
+fn get_videos_in_state(env: &TestEnv, state: &str) -> usize {
+    let status = env.run(&[
+        "status",
+        "--format",
+        format!("{{{}_videos_len}}", state.to_lowercase()).as_str(),
+    ]);
+
+    status.parse().unwrap()
+}
diff --git a/crates/yt/tests/select/options.rs b/crates/yt/tests/select/options.rs
new file mode 100644
index 0000000..6a0d155
--- /dev/null
+++ b/crates/yt/tests/select/options.rs
@@ -0,0 +1,51 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{_testenv::util, select::get_videos_in_state, testenv::TestEnv};
+
+#[test]
+fn test_options() {
+    let env = TestEnv::new(module_path!());
+
+    util::provide_videos(&env, util::Subscription::Tagesschau, 1);
+
+    let video_hash = &util::get_first_hash(&env);
+    env.run(&["select", "watch", video_hash]);
+
+    env.assert_output(&["videos", "info", video_hash, "--format", "{options}"], "");
+
+    env.run(&[
+        "select",
+        "watch",
+        video_hash,
+        "--playback-speed",
+        "1",
+        "--subtitle-langs",
+        "en,de,sv",
+    ]);
+
+    env.assert_output(
+        &["videos", "info", video_hash, "--format", "{options}"],
+        "--playback-speed '1' --subtitle-langs 'en,de,sv'",
+    );
+
+    env.run(&["select", "watch", video_hash, "-s", "1.7", "-l", "de"]);
+
+    env.assert_output(
+        &["videos", "info", video_hash, "--format", "{options}"],
+        "--playback-speed '1.7' --subtitle-langs 'de'",
+    );
+
+    assert_eq!(get_videos_in_state(&env, "Picked"), 0);
+    assert_eq!(get_videos_in_state(&env, "Drop"), 0);
+    assert_eq!(get_videos_in_state(&env, "Watch"), 1);
+    assert_eq!(get_videos_in_state(&env, "Cached"), 0);
+    assert_eq!(get_videos_in_state(&env, "Watched"), 0);
+}
diff --git a/crates/yt/tests/subscriptions/import_export/golden.txt b/crates/yt/tests/subscriptions/import_export/golden.txt
new file mode 100644
index 0000000..7ed5419
--- /dev/null
+++ b/crates/yt/tests/subscriptions/import_export/golden.txt
@@ -0,0 +1,2 @@
+Nyheter på lätt svenska: 'https://www.svtplay.se/nyheter-pa-latt-svenska'
+tagesschau: 'https://www.ardmediathek.de/sendung/tagesschau/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXU'
diff --git a/crates/yt_dlp/src/python_json_decode_failed.error_msg.license b/crates/yt/tests/subscriptions/import_export/golden.txt.license
index 7813eb6..7813eb6 100644
--- a/crates/yt_dlp/src/python_json_decode_failed.error_msg.license
+++ b/crates/yt/tests/subscriptions/import_export/golden.txt.license
diff --git a/crates/yt/tests/subscriptions/import_export/mod.rs b/crates/yt/tests/subscriptions/import_export/mod.rs
new file mode 100644
index 0000000..1156508
--- /dev/null
+++ b/crates/yt/tests/subscriptions/import_export/mod.rs
@@ -0,0 +1,35 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::testenv::TestEnv;
+
+#[test]
+fn test_import_export() {
+    let env = TestEnv::new(module_path!());
+
+    env.run(&[
+        "subs",
+        "add",
+        "https://www.svtplay.se/nyheter-pa-latt-svenska",
+    ]);
+    env.run(&[
+        "subs",
+        "add",
+        "https://www.ardmediathek.de/sendung/tagesschau/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXU",
+    ]);
+
+    let before = env.run(&["subs", "list"]);
+    env.assert_output(&["subs", "list"], include_str!("./golden.txt"));
+
+    env.run_piped(&["subs", "export"], &["subs", "import", "--force"]);
+
+    env.assert_output(&["subs", "list"], &before);
+    env.assert_output(&["subs", "list"], include_str!("./golden.txt"));
+}
diff --git a/crates/yt/tests/subscriptions/mod.rs b/crates/yt/tests/subscriptions/mod.rs
new file mode 100644
index 0000000..0b300c5
--- /dev/null
+++ b/crates/yt/tests/subscriptions/mod.rs
@@ -0,0 +1,12 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+mod import_export;
+mod naming_subscriptions;
diff --git a/crates/yt/tests/subscriptions/naming_subscriptions/golden.txt b/crates/yt/tests/subscriptions/naming_subscriptions/golden.txt
new file mode 100644
index 0000000..46ede50
--- /dev/null
+++ b/crates/yt/tests/subscriptions/naming_subscriptions/golden.txt
@@ -0,0 +1,2 @@
+Nyheter: 'https://www.svtplay.se/nyheter-pa-latt-svenska'
+Vewn: 'https://www.ardmediathek.de/sendung/tagesschau/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXU'
diff --git a/crates/bytes/Cargo.lock.license b/crates/yt/tests/subscriptions/naming_subscriptions/golden.txt.license
index d4d410f..7813eb6 100644
--- a/crates/bytes/Cargo.lock.license
+++ b/crates/yt/tests/subscriptions/naming_subscriptions/golden.txt.license
@@ -1,6 +1,6 @@
 yt - A fully featured command line YouTube client
 
-Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 SPDX-License-Identifier: GPL-3.0-or-later
 
 This file is part of Yt.
diff --git a/crates/yt/tests/subscriptions/naming_subscriptions/mod.rs b/crates/yt/tests/subscriptions/naming_subscriptions/mod.rs
new file mode 100644
index 0000000..50fe3e4
--- /dev/null
+++ b/crates/yt/tests/subscriptions/naming_subscriptions/mod.rs
@@ -0,0 +1,33 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::testenv::TestEnv;
+
+#[test]
+fn test_naming_subscriptions() {
+    let env = TestEnv::new(module_path!());
+
+    env.run(&[
+        "subs",
+        "add",
+        "https://www.svtplay.se/nyheter-pa-latt-svenska",
+        "--name",
+        "Nyheter",
+    ]);
+    env.run(&[
+        "subs",
+        "add",
+        "https://www.ardmediathek.de/sendung/tagesschau/Y3JpZDovL2Rhc2Vyc3RlLmRlL3RhZ2Vzc2NoYXU",
+        "--name",
+        "Vewn",
+    ]);
+
+    env.assert_output(&["subs", "list"], include_str!("./golden.txt"));
+}
diff --git a/crates/yt/tests/tests.rs b/crates/yt/tests/tests.rs
new file mode 100644
index 0000000..89c3091
--- /dev/null
+++ b/crates/yt/tests/tests.rs
@@ -0,0 +1,22 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+// Use this, for the background run pids
+// #![feature(linux_pidfd)]
+
+#![allow(unused_crate_dependencies)]
+
+mod _testenv;
+pub(crate) use _testenv as testenv;
+
+mod select;
+mod subscriptions;
+mod videos;
+mod watch;
diff --git a/crates/yt/tests/videos/downloading.rs b/crates/yt/tests/videos/downloading.rs
new file mode 100644
index 0000000..f026858
--- /dev/null
+++ b/crates/yt/tests/videos/downloading.rs
@@ -0,0 +1,52 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use crate::{_testenv::util, testenv::TestEnv};
+
+#[test]
+fn test_downloading() {
+    let env = TestEnv::new(module_path!());
+
+    util::provide_videos(&env, util::Subscription::Tagesschau, 1);
+
+    let first_hash = &util::get_first_hash(&env);
+    env.run(&["select", "watch", first_hash]);
+
+    env.run(&["download"]);
+
+    let usage = get_cache_usage(&env);
+    assert!(usage > 0.0);
+
+    env.run(&["cache", "clear"]);
+
+    let usage = get_cache_usage(&env);
+
+    #[allow(clippy::float_cmp)]
+    {
+        assert_eq!(usage, 0.0);
+    }
+}
+
+fn get_cache_usage(env: &TestEnv) -> f64 {
+    let status = env.run(&["status", "--format", "{cache_usage}"]);
+
+    let split: Vec<_> = status.split(' ').collect();
+    let usage: f64 = split[0].parse().unwrap();
+    let unit = split[1];
+
+    #[allow(clippy::cast_precision_loss)]
+    match unit {
+        "B" => usage * (1024u64.pow(0)) as f64,
+        "KiB" => usage * (1024u64.pow(1)) as f64,
+        "MiB" => usage * (1024u64.pow(2)) as f64,
+        "GiB" => usage * (1024u64.pow(3)) as f64,
+        other => unreachable!("Unknown unit: {other}"),
+    }
+}
diff --git a/crates/yt/tests/videos/mod.rs b/crates/yt/tests/videos/mod.rs
new file mode 100644
index 0000000..6a80761
--- /dev/null
+++ b/crates/yt/tests/videos/mod.rs
@@ -0,0 +1,11 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+mod downloading;
diff --git a/crates/yt/tests/watch/focus_switch.rs b/crates/yt/tests/watch/focus_switch.rs
new file mode 100644
index 0000000..81246f3
--- /dev/null
+++ b/crates/yt/tests/watch/focus_switch.rs
@@ -0,0 +1,53 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use yt_dlp::json_cast;
+
+use crate::{_testenv::util, testenv::TestEnv, watch::MpvControl};
+
+#[test]
+#[ignore = "Currently, this test is missing it's goal"]
+fn test_focus_switch() {
+    let mut env = TestEnv::new(module_path!());
+
+    {
+        util::provide_videos(&env, util::Subscription::Tagesschau, 32);
+
+        util::run_select(&env, "s/pick/watch/");
+
+        env.run(&["download"]);
+    }
+
+    let mut mpv = MpvControl::new(&mut env);
+
+    assert_pos(&mut mpv, 0);
+
+    for i in 1..32 {
+        mpv.assert(&["playlist-next", "weak"]);
+        assert_pos(&mut mpv, i);
+    }
+
+    mpv.assert(&["playlist-next", "weak"]);
+    assert_pos(&mut mpv, 2);
+
+    mpv.assert(&["playlist-prev", "weak"]);
+    assert_pos(&mut mpv, 1);
+
+    mpv.assert(&["playlist-prev", "weak"]);
+    assert_pos(&mut mpv, 0);
+
+    mpv.assert(&["playlist-prev", "weak"]);
+    assert_pos(&mut mpv, 0);
+}
+
+fn assert_pos(mpv: &mut MpvControl, pos: i64) {
+    let mpv_pos = mpv.assert(&["get_property", "playlist-pos"]);
+    assert_eq!(json_cast!(mpv_pos, as_i64), pos);
+}
diff --git a/crates/yt/tests/watch/mod.rs b/crates/yt/tests/watch/mod.rs
new file mode 100644
index 0000000..7af8b39
--- /dev/null
+++ b/crates/yt/tests/watch/mod.rs
@@ -0,0 +1,135 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    io::{BufRead, BufReader, Write},
+    os::unix::net::UnixStream,
+    path::PathBuf,
+    sync::atomic::AtomicU64,
+};
+
+use colors::{Colorize, IntoCanvas};
+use serde_json::json;
+use yt_dlp::{json_cast, json_get, json_try_get};
+
+use crate::_testenv::TestEnv;
+
+mod focus_switch;
+
+struct MpvControl {
+    stream: UnixStream,
+    current_request_id: AtomicU64,
+    name: &'static str,
+}
+
+impl MpvControl {
+    fn new(env: &mut TestEnv) -> Self {
+        let socket_path = {
+            let stdout = env.run_background(&[
+                "watch",
+                // "--headless",
+                "--provide-ipc-socket",
+            ]);
+
+            let line = {
+                let mut buf = String::new();
+                let mut reader = BufReader::new(stdout);
+                reader.read_line(&mut buf).expect("In-memory");
+                buf
+            };
+
+            PathBuf::from(line.trim())
+        };
+
+        let stream = UnixStream::connect(&socket_path).unwrap_or_else(|e| {
+            panic!(
+                "Path to socket ('{}') should exist, but did not: {e}",
+                socket_path.display()
+            )
+        });
+
+        let mut me = Self {
+            stream,
+            name: env.name,
+            current_request_id: AtomicU64::new(0),
+        };
+
+        // Disable all events.
+        // We do not use them, and this should reduce the read load on the socket.
+        me.assert(&["disable_event", "all"]);
+
+        me
+    }
+
+    /// Run a command and assert that it ran successfully.
+    fn assert(&mut self, args: &[&str]) -> serde_json::Value {
+        let out = self.command(args);
+
+        out.unwrap_or_else(|e| panic!("`mpv {}` failed; error {e}.", args.join(" ")))
+    }
+
+    /// Run a command in mpv.
+    /// Will return true if the command ran correctly and false if not.
+    fn command(&mut self, args: &[&str]) -> Result<serde_json::Value, String> {
+        let tl_rid = self
+            .current_request_id
+            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
+
+        eprint!(
+            "{} `mpv {}`",
+            self.name.blue().italic().render(true),
+            args.join(" ")
+        );
+
+        writeln!(
+            self.stream,
+            "{}",
+            json!( { "command": args, "request_id": tl_rid })
+        )
+        .expect("Should always work");
+
+        loop {
+            let response: serde_json::Value = {
+                let mut reader = BufReader::new(&mut self.stream);
+
+                let mut buf = String::new();
+                reader.read_line(&mut buf).expect("Works");
+                serde_json::from_str(&buf).expect("Mpv only returns json")
+            };
+
+            if let Some(rid) = json_try_get!(response, "request_id", as_u64) {
+                if rid == tl_rid {
+                    let error = json_get!(response, "error", as_str);
+
+                    if error == "success" {
+                        let data: serde_json::Value = {
+                            match response.get("data") {
+                                Some(val) => val.to_owned(),
+                                None => serde_json::Value::Null,
+                            }
+                        };
+
+                        eprintln!(", {}: {data}", "output".bright_blue().render(true),);
+                        return Ok(data);
+                    }
+
+                    eprintln!(", {}: {error}", "error".bright_red().render(true));
+                    return Err(error.to_owned());
+                }
+            }
+        }
+    }
+}
+
+impl Drop for MpvControl {
+    fn drop(&mut self) {
+        self.assert(&["quit"]);
+    }
+}
diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml
index a948a34..87bb610 100644
--- a/crates/yt_dlp/Cargo.toml
+++ b/crates/yt_dlp/Cargo.toml
@@ -10,7 +10,7 @@
 
 [package]
 name = "yt_dlp"
-description = "A wrapper around the python yt_dlp library"
+description = "A rust ffi wrapper library for the python yt_dlp library"
 keywords = []
 categories = []
 version.workspace = true
@@ -19,19 +19,18 @@ authors.workspace = true
 license.workspace = true
 repository.workspace = true
 rust-version.workspace = true
-publish = false
+publish = true
 
 [dependencies]
-pyo3 = { version = "0.23.4", features = ["auto-initialize"] }
-bytes.workspace = true
+curl = "0.4.48"
 log.workspace = true
-serde.workspace = true
+pyo3 = { workspace = true }
+pyo3-pylogger = { path = "crates/pyo3-pylogger" }
+serde = { workspace = true, features = ["derive"] }
 serde_json.workspace = true
+thiserror = "2.0.12"
 url.workspace = true
 
-[dev-dependencies]
-tokio.workspace = true
-
 [lints]
 workspace = true
 
diff --git a/crates/yt_dlp/README.md b/crates/yt_dlp/README.md
index 591ef2e..ece8540 100644
--- a/crates/yt_dlp/README.md
+++ b/crates/yt_dlp/README.md
@@ -12,7 +12,7 @@ If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 # Yt_py
 
-> \[can be empty\]
+> [can be empty]
 
 Some text about the project.
 
diff --git a/crates/yt_dlp/.cargo/config.toml b/crates/yt_dlp/crates/pyo3-pylogger/.gitignore
index d84f14d..733c5bc 100644
--- a/crates/yt_dlp/.cargo/config.toml
+++ b/crates/yt_dlp/crates/pyo3-pylogger/.gitignore
@@ -1,12 +1,13 @@
 # yt - A fully featured command line YouTube client
 #
-# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-# SPDX-License-Identifier: GPL-3.0-or-later
+# Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+# SPDX-License-Identifier: Apache-2.0
 #
 # This file is part of Yt.
 #
 # You should have received a copy of the License along with this program.
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-[env]
-PYO3_PYTHON = "/nix/store/7xzk119acyws2c4ysygdv66l0grxkr39-python3-3.11.9-env/bin/python3"
+target
+Cargo.lock
+.idea
diff --git a/crates/yt_dlp/crates/pyo3-pylogger/Cargo.toml b/crates/yt_dlp/crates/pyo3-pylogger/Cargo.toml
new file mode 100644
index 0000000..28dfacd
--- /dev/null
+++ b/crates/yt_dlp/crates/pyo3-pylogger/Cargo.toml
@@ -0,0 +1,31 @@
+# yt - A fully featured command line YouTube client
+#
+# Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+# SPDX-License-Identifier: Apache-2.0
+#
+# This file is part of Yt.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+[package]
+name = "pyo3-pylogger"
+version = "1.8.0"
+edition = "2021"
+authors = [
+  "Dylan Bobby Storey <dylan.storey@gmail.com>",
+  "cpu <daniel@binaryparadox.net>",
+  "Warren Snipes <contact@warrensnipes.dev>",
+]
+description = "Enables `log` for pyo3 based Rust applications using the `logging` modules."
+publish = ["crates-io"]
+license = "Apache-2.0"
+readme = "README.md"
+homepage = "https://github.com/dylanbstorey/pyo3-pylogger"
+repository = "https://github.com/dylanbstorey/pyo3-pylogger"
+documentation = "https://github.com/dylanbstorey/pyo3-pylogger"
+
+[dependencies]
+pyo3 = { workspace = true }
+log = { workspace = true }
+phf = { version = "0.12", features = ["macros"] }
diff --git a/crates/yt_dlp/crates/pyo3-pylogger/LICENSE b/crates/yt_dlp/crates/pyo3-pylogger/LICENSE
new file mode 100644
index 0000000..261eeb9
--- /dev/null
+++ b/crates/yt_dlp/crates/pyo3-pylogger/LICENSE
@@ -0,0 +1,201 @@
+                                 Apache License
+                           Version 2.0, January 2004
+                        http://www.apache.org/licenses/
+
+   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+   1. Definitions.
+
+      "License" shall mean the terms and conditions for use, reproduction,
+      and distribution as defined by Sections 1 through 9 of this document.
+
+      "Licensor" shall mean the copyright owner or entity authorized by
+      the copyright owner that is granting the License.
+
+      "Legal Entity" shall mean the union of the acting entity and all
+      other entities that control, are controlled by, or are under common
+      control with that entity. For the purposes of this definition,
+      "control" means (i) the power, direct or indirect, to cause the
+      direction or management of such entity, whether by contract or
+      otherwise, or (ii) ownership of fifty percent (50%) or more of the
+      outstanding shares, or (iii) beneficial ownership of such entity.
+
+      "You" (or "Your") shall mean an individual or Legal Entity
+      exercising permissions granted by this License.
+
+      "Source" form shall mean the preferred form for making modifications,
+      including but not limited to software source code, documentation
+      source, and configuration files.
+
+      "Object" form shall mean any form resulting from mechanical
+      transformation or translation of a Source form, including but
+      not limited to compiled object code, generated documentation,
+      and conversions to other media types.
+
+      "Work" shall mean the work of authorship, whether in Source or
+      Object form, made available under the License, as indicated by a
+      copyright notice that is included in or attached to the work
+      (an example is provided in the Appendix below).
+
+      "Derivative Works" shall mean any work, whether in Source or Object
+      form, that is based on (or derived from) the Work and for which the
+      editorial revisions, annotations, elaborations, or other modifications
+      represent, as a whole, an original work of authorship. For the purposes
+      of this License, Derivative Works shall not include works that remain
+      separable from, or merely link (or bind by name) to the interfaces of,
+      the Work and Derivative Works thereof.
+
+      "Contribution" shall mean any work of authorship, including
+      the original version of the Work and any modifications or additions
+      to that Work or Derivative Works thereof, that is intentionally
+      submitted to Licensor for inclusion in the Work by the copyright owner
+      or by an individual or Legal Entity authorized to submit on behalf of
+      the copyright owner. For the purposes of this definition, "submitted"
+      means any form of electronic, verbal, or written communication sent
+      to the Licensor or its representatives, including but not limited to
+      communication on electronic mailing lists, source code control systems,
+      and issue tracking systems that are managed by, or on behalf of, the
+      Licensor for the purpose of discussing and improving the Work, but
+      excluding communication that is conspicuously marked or otherwise
+      designated in writing by the copyright owner as "Not a Contribution."
+
+      "Contributor" shall mean Licensor and any individual or Legal Entity
+      on behalf of whom a Contribution has been received by Licensor and
+      subsequently incorporated within the Work.
+
+   2. Grant of Copyright License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      copyright license to reproduce, prepare Derivative Works of,
+      publicly display, publicly perform, sublicense, and distribute the
+      Work and such Derivative Works in Source or Object form.
+
+   3. Grant of Patent License. Subject to the terms and conditions of
+      this License, each Contributor hereby grants to You a perpetual,
+      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+      (except as stated in this section) patent license to make, have made,
+      use, offer to sell, sell, import, and otherwise transfer the Work,
+      where such license applies only to those patent claims licensable
+      by such Contributor that are necessarily infringed by their
+      Contribution(s) alone or by combination of their Contribution(s)
+      with the Work to which such Contribution(s) was submitted. If You
+      institute patent litigation against any entity (including a
+      cross-claim or counterclaim in a lawsuit) alleging that the Work
+      or a Contribution incorporated within the Work constitutes direct
+      or contributory patent infringement, then any patent licenses
+      granted to You under this License for that Work shall terminate
+      as of the date such litigation is filed.
+
+   4. Redistribution. You may reproduce and distribute copies of the
+      Work or Derivative Works thereof in any medium, with or without
+      modifications, and in Source or Object form, provided that You
+      meet the following conditions:
+
+      (a) You must give any other recipients of the Work or
+          Derivative Works a copy of this License; and
+
+      (b) You must cause any modified files to carry prominent notices
+          stating that You changed the files; and
+
+      (c) You must retain, in the Source form of any Derivative Works
+          that You distribute, all copyright, patent, trademark, and
+          attribution notices from the Source form of the Work,
+          excluding those notices that do not pertain to any part of
+          the Derivative Works; and
+
+      (d) If the Work includes a "NOTICE" text file as part of its
+          distribution, then any Derivative Works that You distribute must
+          include a readable copy of the attribution notices contained
+          within such NOTICE file, excluding those notices that do not
+          pertain to any part of the Derivative Works, in at least one
+          of the following places: within a NOTICE text file distributed
+          as part of the Derivative Works; within the Source form or
+          documentation, if provided along with the Derivative Works; or,
+          within a display generated by the Derivative Works, if and
+          wherever such third-party notices normally appear. The contents
+          of the NOTICE file are for informational purposes only and
+          do not modify the License. You may add Your own attribution
+          notices within Derivative Works that You distribute, alongside
+          or as an addendum to the NOTICE text from the Work, provided
+          that such additional attribution notices cannot be construed
+          as modifying the License.
+
+      You may add Your own copyright statement to Your modifications and
+      may provide additional or different license terms and conditions
+      for use, reproduction, or distribution of Your modifications, or
+      for any such Derivative Works as a whole, provided Your use,
+      reproduction, and distribution of the Work otherwise complies with
+      the conditions stated in this License.
+
+   5. Submission of Contributions. Unless You explicitly state otherwise,
+      any Contribution intentionally submitted for inclusion in the Work
+      by You to the Licensor shall be under the terms and conditions of
+      this License, without any additional terms or conditions.
+      Notwithstanding the above, nothing herein shall supersede or modify
+      the terms of any separate license agreement you may have executed
+      with Licensor regarding such Contributions.
+
+   6. Trademarks. This License does not grant permission to use the trade
+      names, trademarks, service marks, or product names of the Licensor,
+      except as required for reasonable and customary use in describing the
+      origin of the Work and reproducing the content of the NOTICE file.
+
+   7. Disclaimer of Warranty. Unless required by applicable law or
+      agreed to in writing, Licensor provides the Work (and each
+      Contributor provides its Contributions) on an "AS IS" BASIS,
+      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+      implied, including, without limitation, any warranties or conditions
+      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+      PARTICULAR PURPOSE. You are solely responsible for determining the
+      appropriateness of using or redistributing the Work and assume any
+      risks associated with Your exercise of permissions under this License.
+
+   8. Limitation of Liability. In no event and under no legal theory,
+      whether in tort (including negligence), contract, or otherwise,
+      unless required by applicable law (such as deliberate and grossly
+      negligent acts) or agreed to in writing, shall any Contributor be
+      liable to You for damages, including any direct, indirect, special,
+      incidental, or consequential damages of any character arising as a
+      result of this License or out of the use or inability to use the
+      Work (including but not limited to damages for loss of goodwill,
+      work stoppage, computer failure or malfunction, or any and all
+      other commercial damages or losses), even if such Contributor
+      has been advised of the possibility of such damages.
+
+   9. Accepting Warranty or Additional Liability. While redistributing
+      the Work or Derivative Works thereof, You may choose to offer,
+      and charge a fee for, acceptance of support, warranty, indemnity,
+      or other liability obligations and/or rights consistent with this
+      License. However, in accepting such obligations, You may act only
+      on Your own behalf and on Your sole responsibility, not on behalf
+      of any other Contributor, and only if You agree to indemnify,
+      defend, and hold each Contributor harmless for any liability
+      incurred by, or claims asserted against, such Contributor by reason
+      of your accepting any such warranty or additional liability.
+
+   END OF TERMS AND CONDITIONS
+
+   APPENDIX: How to apply the Apache License to your work.
+
+      To apply the Apache License to your work, attach the following
+      boilerplate notice, with the fields enclosed by brackets "[]"
+      replaced with your own identifying information. (Don't include
+      the brackets!)  The text should be enclosed in the appropriate
+      comment syntax for the file format. We also recommend that a
+      file or class name and description of purpose be included on the
+      same "printed page" as the copyright notice for easier
+      identification within third-party archives.
+
+   Copyright [yyyy] [name of copyright owner]
+
+   Licensed under the Apache License, Version 2.0 (the "License");
+   you may not use this file except in compliance with the License.
+   You may obtain a copy of the License at
+
+       http://www.apache.org/licenses/LICENSE-2.0
+
+   Unless required by applicable law or agreed to in writing, software
+   distributed under the License is distributed on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+   See the License for the specific language governing permissions and
+   limitations under the License.
diff --git a/crates/yt_dlp/crates/pyo3-pylogger/README.md b/crates/yt_dlp/crates/pyo3-pylogger/README.md
new file mode 100644
index 0000000..e68903b
--- /dev/null
+++ b/crates/yt_dlp/crates/pyo3-pylogger/README.md
@@ -0,0 +1,160 @@
+<!--
+yt - A fully featured command line YouTube client
+
+Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+SPDX-License-Identifier: Apache-2.0
+
+This file is part of Yt.
+
+You should have received a copy of the License along with this program.
+If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+-->
+
+# pyo3-pylogger
+
+Enables log messages for pyo3 embedded Python applications using Python's
+`logging` or module.
+
+# Features
+
+- Logging integration between Python's `logging` module and Rust's `log` crate
+- Structured logging support via the logging
+  [extra](https://docs.python.org/3/library/logging.html#logging.Logger.debug)
+  field (requires `kv` or `tracing-kv`feature)
+- Integration with Rust's `tracing` library (requires `tracing` feature)
+
+# Usage
+
+```rust
+use log::{info, warn};
+use pyo3::{ffi::c_str, prelude::*};
+fn main() {
+    // register the host handler with python logger, providing a logger target
+    pyo3_pylogger::register("example_application_py_logger");
+
+    // initialize up a logger
+    env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("trace")).init();
+    //just show the logger working from Rust.
+    info!("Just some normal information!");
+    warn!("Something spooky happened!");
+
+    // Ask pyo3 to set up embedded Python interpreter
+    pyo3::prepare_freethreaded_python();
+    Python::with_gil(|py| {
+        // Python code can now `import logging` as usual
+        py.run(
+            c_str!(
+                r#"
+import logging
+logging.getLogger().setLevel(0)
+logging.debug('DEBUG')
+logging.info('INFO')
+logging.warning('WARNING')
+logging.error('ERROR')
+logging.getLogger('foo.bar.baz').info('INFO')"#
+            ),
+            None,
+            None,
+        )
+        .unwrap();
+    })
+}
+
+
+```
+
+## Outputs
+
+```bash
+[2025-03-28T01:12:29Z INFO  helloworld] Just some normal information!
+[2025-03-28T01:12:29Z WARN  helloworld] Something spooky happened!
+[2025-03-28T01:12:29Z DEBUG example_application_py_logger] DEBUG
+[2025-03-28T01:12:29Z INFO  example_application_py_logger] INFO
+[2025-03-28T01:12:29Z WARN  example_application_py_logger] WARNING
+[2025-03-28T01:12:29Z ERROR example_application_py_logger] ERROR
+[2025-03-28T01:12:29Z INFO  example_application_py_logger::foo::bar::baz] INFO
+```
+
+## Structured Logging
+
+To enable structured logging support, add the `kv` feature to your `Cargo.toml`:
+
+```toml
+[dependencies]
+pyo3-pylogger = { version = "0.4", features = ["kv"] }
+```
+
+Then you can use Python's `extra` parameter to pass structured data:
+
+```python
+logging.info("Processing order", extra={"order_id": "12345", "amount": 99.99})
+```
+
+When using a structured logging subscriber in Rust, these key-value pairs will
+be properly captured, for example:
+
+```bash
+[2025-03-28T01:12:29Z INFO  example_application_py_logger] Processing order order_id=12345 amount=99.99
+```
+
+## Tracing Support
+
+To enable integration with Rust's `tracing` library, add the `tracing` feature
+to your `Cargo.toml`:
+
+```toml
+[dependencies]
+pyo3-pylogger = { version = "0.4", default-features = false, features = ["tracing"] }
+```
+
+When the `tracing` feature is enabled, Python logs will be forwarded to the
+active tracing subscriber:
+
+```rust
+use tracing::{info, warn};
+use pyo3::{ffi::c_str, prelude::*};
+
+fn main() {
+    // Register the tracing handler with Python logger
+    pyo3_pylogger::register_tracing("example_application_py_logger");
+
+    // Initialize tracing subscriber
+    tracing_subscriber::fmt::init();
+
+    // Tracing events from Rust
+    info!("Tracing information from Rust");
+
+    // Python logging will be captured by the tracing subscriber
+    pyo3::prepare_freethreaded_python();
+    Python::with_gil(|py| {
+        py.run(
+            c_str!(
+                r#"
+import logging
+logging.getLogger().setLevel(0)
+logging.info('This will be captured by tracing')"#
+            ),
+            None,
+            None,
+        )
+        .unwrap();
+    })
+}
+```
+
+### Structured Data with Tracing
+
+The `tracing` feature automatically supports Python's `extra` field for
+structured data. However, the KV fields are json serialized and not available as
+tracing attributes. This is a limitation of the `tracing` library and is not
+specific to this crate. See
+[this issue](https://github.com/tokio-rs/tracing/issues/372) for more
+information.
+
+# Feature Flags
+
+- `kv`: Enables structured logging support via Python's `extra` fields. This
+  adds support for the `log` crate's key-value system.
+- `tracing`: Enables integration with Rust's `tracing` library.
+- `tracing-kv`: Enables structured logging support via Python's `extra` fields
+  and integration with Rust's `tracing` library.
diff --git a/crates/yt_dlp/crates/pyo3-pylogger/src/kv.rs b/crates/yt_dlp/crates/pyo3-pylogger/src/kv.rs
new file mode 100644
index 0000000..67a0c3e
--- /dev/null
+++ b/crates/yt_dlp/crates/pyo3-pylogger/src/kv.rs
@@ -0,0 +1,127 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+// SPDX-License-Identifier: Apache-2.0
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+//! Key-Value handling module for Python LogRecord attributes.
+//!
+//! This module provides functionality to extract and handle custom key-value pairs
+//! from Python LogRecord objects, facilitating integration between Python's logging
+//! system and Rust's log crate.
+
+use pyo3::{
+    Bound, PyAny, PyResult,
+    types::{PyAnyMethods, PyDict, PyDictMethods, PyListMethods},
+};
+use std::collections::HashMap;
+
+/// A static hashset containing all standard [LogRecord](https://github.com/python/cpython/blob/8a00c9a4d2ce9d373b13f8f0a2265a65f4523293/Lib/logging/__init__.py#L286-L287) attributes defined in the CPython logging module.
+///
+/// This set is used to differentiate between standard [LogRecord](https://github.com/python/cpython/blob/8a00c9a4d2ce9d373b13f8f0a2265a65f4523293/Lib/logging/__init__.py#L286-L287) attributes and custom key-value pairs
+/// that users might add to their log records. The attributes listed here correspond to the default
+/// attributes created by Python's [makeRecord](https://github.com/python/cpython/blob/8a00c9a4d2ce9d373b13f8f0a2265a65f4523293/Lib/logging/__init__.py#L1633-L1634) function.
+pub static LOG_RECORD_KV_ATTRIBUTES: phf::Set<&'static str> = phf::phf_set! {
+    "name",
+    "msg",
+    "args",
+    "levelname",
+    "levelno",
+    "pathname",
+    "filename",
+    "module",
+    "exc_info",
+    "exc_text",
+    "stack_info",
+    "lineno",
+    "funcName",
+    "created",
+    "msecs",
+    "relativeCreated",
+    "thread",
+    "threadName",
+    "processName",
+    "process",
+    "taskName",
+};
+
+/// Extracts custom key-value pairs from a Python LogRecord object.
+///
+/// This function examines the `__dict__` of a LogRecord(https://github.com/python/cpython/blob/8a00c9a4d2ce9d373b13f8f0a2265a65f4523293/Lib/logging/__init__.py#L286-L287) object and identifies any attributes
+/// that are not part of the standard [LogRecord](https://github.com/python/cpython/blob/8a00c9a4d2ce9d373b13f8f0a2265a65f4523293/Lib/logging/__init__.py#L286-L287) attributes. These custom attributes are
+/// treated as key-value pairs for structured logging.
+///
+/// # Arguments
+/// * `record` - A reference to a Python LogRecord object
+///
+/// # Returns
+/// * `PyResult<Option<HashMap<String, pyo3::Bound<'a, pyo3::PyAny>>>>` - If custom attributes
+///   are found, returns a HashMap containing the key-value pairs. Returns None if no custom
+///   attributes are present.
+///
+/// # Note
+/// This function relies on the fact that Python will not implement new attributes on the LogRecord object.
+/// If new attributes are added, this function will not be able to filter them out and will return them as key-value pairs.
+/// In that future, [LOG_RECORD_KV_ATTRIBUTES] will need to be updated to include the new attributes.
+/// This is an unfortunate side effect of using the `__dict__` attribute to extract key-value pairs. However, there are no other ways to handle this given that CPython does not distinguish between user-provided attributes and attributes created by the logging module.
+pub fn find_kv_args<'a>(
+    record: &Bound<'a, PyAny>,
+) -> PyResult<Option<std::collections::HashMap<String, pyo3::Bound<'a, pyo3::PyAny>>>> {
+    let dict: Bound<'_, PyDict> = record.getattr("__dict__")?.extract()?;
+
+    // We can abuse the fact that Python dictionaries are ordered by insertion order to reverse iterate over the keys
+    // and stop at the first key that is not a predefined key-value pair attribute.
+    let mut kv_args: Option<HashMap<String, pyo3::Bound<'_, pyo3::PyAny>>> = None;
+
+    for item in dict.items().iter().rev() {
+        let (key, value) =
+            item.extract::<(pyo3::Bound<'_, pyo3::PyAny>, pyo3::Bound<'_, pyo3::PyAny>)>()?;
+
+        let key_str = key.to_string();
+        if LOG_RECORD_KV_ATTRIBUTES.contains(&key_str) {
+            break;
+        }
+        if kv_args.is_none() {
+            kv_args = Some(HashMap::new());
+        }
+
+        kv_args.as_mut().unwrap().insert(key_str, value);
+    }
+
+    Ok(kv_args)
+}
+
+/// A wrapper struct that implements the `log::kv::Source` trait for Python key-value pairs.
+///
+/// This struct allows Python LogRecord custom attributes to be used with Rust's
+/// structured logging system by implementing the necessary trait for key-value handling.
+///
+/// # Type Parameters
+/// * `'a` - The lifetime of the contained Python values
+pub struct KVSource<'a>(pub HashMap<String, pyo3::Bound<'a, pyo3::PyAny>>);
+
+impl log::kv::Source for KVSource<'_> {
+    /// Visits each key-value pair in the source, converting Python values to debug representations.
+    ///
+    /// # Arguments
+    /// * `visitor` - The visitor that will process each key-value pair
+    ///
+    /// # Returns
+    /// * `Result<(), log::kv::Error>` - Success if all pairs are visited successfully,
+    ///   or an error if visitation fails
+    fn visit<'kvs>(
+        &'kvs self,
+        visitor: &mut dyn log::kv::VisitSource<'kvs>,
+    ) -> Result<(), log::kv::Error> {
+        for (key, value) in &self.0 {
+            let v: log::kv::Value<'_> = log::kv::Value::from_debug(value);
+
+            visitor.visit_pair(log::kv::Key::from_str(key), v)?;
+        }
+        Ok(())
+    }
+}
diff --git a/crates/yt_dlp/crates/pyo3-pylogger/src/level.rs b/crates/yt_dlp/crates/pyo3-pylogger/src/level.rs
new file mode 100644
index 0000000..d244ef4
--- /dev/null
+++ b/crates/yt_dlp/crates/pyo3-pylogger/src/level.rs
@@ -0,0 +1,43 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+// SPDX-License-Identifier: Apache-2.0
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+/// A wrapper type for logging levels that supports both `tracing` and `log` features.
+pub(crate) struct Level(pub log::Level);
+
+/// Converts a numeric level value to the appropriate logging Level.
+///
+/// # Arguments
+///
+/// * `level` - A u8 value representing the logging level:
+///   * 40+ = Error
+///   * 30-39 = Warn
+///   * 20-29 = Info
+///   * 10-19 = Debug
+///   * 0-9 = Trace
+///
+/// # Returns
+///
+/// Returns a `Level` wrapper containing either a `tracing::Level` or `log::Level`
+/// depending on which feature is enabled.
+pub(crate) fn get_level(level: u8) -> Level {
+    {
+        if level.ge(&40u8) {
+            Level(log::Level::Error)
+        } else if level.ge(&30u8) {
+            Level(log::Level::Warn)
+        } else if level.ge(&20u8) {
+            Level(log::Level::Info)
+        } else if level.ge(&10u8) {
+            Level(log::Level::Debug)
+        } else {
+            Level(log::Level::Trace)
+        }
+    }
+}
diff --git a/crates/yt_dlp/crates/pyo3-pylogger/src/lib.rs b/crates/yt_dlp/crates/pyo3-pylogger/src/lib.rs
new file mode 100644
index 0000000..3ecb123
--- /dev/null
+++ b/crates/yt_dlp/crates/pyo3-pylogger/src/lib.rs
@@ -0,0 +1,211 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+// SPDX-License-Identifier: Apache-2.0
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::{
+    ffi::CString,
+    sync::{self, OnceLock},
+};
+
+use log::{debug, log_enabled};
+use pyo3::{
+    Bound, Py, PyAny, PyResult, Python, pyfunction,
+    sync::OnceLockExt,
+    types::{PyAnyMethods, PyDict, PyListMethods, PyModuleMethods},
+    wrap_pyfunction,
+};
+
+mod kv;
+mod level;
+
+static LOGGER: sync::OnceLock<Py<PyAny>> = OnceLock::new();
+
+/// Is the specified record to be logged? Returns false for no,
+/// true for yes. Filters can either modify log records in-place or
+/// return a completely different record instance which will replace
+/// the original log record in any future processing of the event.
+#[pyfunction]
+fn filter_error_log<'py>(record: Bound<'py, PyAny>) -> bool {
+    // Filter out all error logs (they are propagated as rust errors)
+    let levelname: String = record
+        .getattr("levelname")
+        .expect("This should exist")
+        .extract()
+        .expect("This should be a String");
+
+    let return_value = levelname.as_str() != "ERROR";
+
+    if log_enabled!(log::Level::Debug) && !return_value {
+        let message: String = {
+            let get_message = record.getattr("getMessage").expect("Is set");
+            let message: String = get_message
+                .call((), None)
+                .expect("Can be called")
+                .extract()
+                .expect("Downcasting works");
+
+            message.as_str().to_owned()
+        };
+
+        debug!("Swollowed error message: '{message}'");
+    }
+    return_value
+}
+
+/// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead.
+#[pyfunction]
+fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
+    let level = record.getattr("levelno")?.extract()?;
+    let message = record.getattr("getMessage")?.call0()?.to_string();
+    let pathname = record.getattr("pathname")?.extract::<String>()?;
+    let lineno = record.getattr("lineno")?.extract::<u32>()?;
+
+    let logger_name = record.getattr("name")?.extract::<String>()?;
+
+    let full_target: Option<String> = if logger_name.trim().is_empty() || logger_name == "root" {
+        None
+    } else {
+        // Libraries (ex: tracing_subscriber::filter::Directive) expect rust-style targets like foo::bar,
+        // and may not deal well with "." as a module separator:
+        let logger_name = logger_name.replace('.', "::");
+        Some(format!("{rust_target}::{logger_name}"))
+    };
+    let target = full_target.as_deref().unwrap_or(rust_target);
+
+    handle_record(record, target, &message, lineno, &pathname, level)?;
+
+    Ok(())
+}
+
+fn handle_record(
+    #[allow(unused_variables)] record: Bound<'_, PyAny>,
+    target: &str,
+    message: &str,
+    lineno: u32,
+    pathname: &str,
+    level: u8,
+) -> PyResult<()> {
+    // If log feature is enabled, use log::logger
+    let level = crate::level::get_level(level).0;
+
+    {
+        let mut metadata_builder = log::MetadataBuilder::new();
+        metadata_builder.target(target);
+        metadata_builder.level(level);
+
+        let mut record_builder = log::Record::builder();
+
+        {
+            let kv_args = kv::find_kv_args(&record)?;
+
+            let kv_source = kv_args.map(kv::KVSource);
+            if let Some(kv_source) = kv_source {
+                log::logger().log(
+                    &record_builder
+                        .metadata(metadata_builder.build())
+                        .args(format_args!("{}", &message))
+                        .line(Some(lineno))
+                        .file(Some(pathname))
+                        .module_path(Some(pathname))
+                        .key_values(&kv_source)
+                        .build(),
+                );
+                return Ok(());
+            }
+        }
+
+        log::logger().log(
+            &record_builder
+                .metadata(metadata_builder.build())
+                .args(format_args!("{}", &message))
+                .line(Some(lineno))
+                .file(Some(pathname))
+                .module_path(Some(pathname))
+                .build(),
+        );
+    }
+
+    Ok(())
+}
+
+/// Registers the host_log function in rust as the event handler for Python's logging logger
+/// This function needs to be called from within a pyo3 context as early as possible to ensure logging messages
+/// arrive to the rust consumer.
+pub fn setup_logging<'py>(py: Python<'py>, target: &str) -> PyResult<Bound<'py, PyAny>> {
+    let logger = LOGGER
+        .get_or_init_py_attached(py, || match setup_logging_inner(py, target) {
+            Ok(ok) => ok.unbind(),
+            Err(err) => {
+                panic!("Failed to initialize logger: {}", err);
+            }
+        })
+        .clone_ref(py);
+
+    Ok(logger.into_bound(py))
+}
+
+fn setup_logging_inner<'py>(py: Python<'py>, target: &str) -> PyResult<Bound<'py, PyAny>> {
+    let logging = py.import("logging")?;
+
+    logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?;
+
+    #[allow(clippy::uninlined_format_args)]
+    let code = CString::new(format!(
+        r#"
+class HostHandler(Handler):
+	def __init__(self, level=0):
+		super().__init__(level=level)
+
+	def emit(self, record: LogRecord):
+		host_log(record, "{}")
+
+oldBasicConfig = basicConfig
+def basicConfig(*pargs, **kwargs):
+    if "handlers" not in kwargs:
+        kwargs["handlers"] = [HostHandler()]
+    return oldBasicConfig(*pargs, **kwargs)
+"#,
+        target
+    ))?;
+
+    let logging_scope = logging.dict();
+    py.run(&code, Some(&logging_scope), None)?;
+
+    let all = logging.index()?;
+    all.append("HostHandler")?;
+
+    let logger = {
+        let get_logger = logging_scope.get_item("getLogger")?;
+        get_logger.call((target,), None)?
+    };
+
+    {
+        let basic_config = logging_scope.get_item("basicConfig")?;
+        basic_config.call(
+            (),
+            {
+                let dict = PyDict::new(py);
+
+                // Ensure that all events are logged by setting
+                // the log level to NOTSET (we filter on rust's side)
+                dict.set_item("level", 0)?;
+
+                Some(dict)
+            }
+            .as_ref(),
+        )?;
+    }
+
+    {
+        let add_filter = logger.getattr("addFilter")?;
+        add_filter.call((wrap_pyfunction!(filter_error_log, &logging)?,), None)?;
+    }
+
+    Ok(logger)
+}
diff --git a/crates/bytes/update.sh b/crates/yt_dlp/crates/pyo3-pylogger/update.sh
index c1a0215..dd3e57e 100755
--- a/crates/bytes/update.sh
+++ b/crates/yt_dlp/crates/pyo3-pylogger/update.sh
@@ -1,9 +1,9 @@
-#!/usr/bin/env sh
+#! /usr/bin/env sh
 
 # yt - A fully featured command line YouTube client
 #
-# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-# SPDX-License-Identifier: GPL-3.0-or-later
+# Copyright (C) 2025 Dylan Bobby Storey <dylan.storey@gmail.com>, cpu <daniel@binaryparadox.net>, Warren Snipes <contact@warrensnipes.dev>
+# SPDX-License-Identifier: Apache-2.0
 #
 # This file is part of Yt.
 #
@@ -13,3 +13,5 @@
 cd "$(dirname "$0")" || exit 1
 [ "$1" = "upgrade" ] && cargo upgrade --incompatible
 cargo update
+
+# vim: ft=sh
diff --git a/crates/yt_dlp/examples/main.rs b/crates/yt_dlp/examples/main.rs
new file mode 100644
index 0000000..e924407
--- /dev/null
+++ b/crates/yt_dlp/examples/main.rs
@@ -0,0 +1,15 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+fn main() {
+    let yt_dlp = yt_dlp::options::YoutubeDLOptions::new().build().unwrap();
+
+    dbg!(yt_dlp.version().unwrap());
+}
diff --git a/crates/yt_dlp/src/duration.rs b/crates/yt_dlp/src/duration.rs
deleted file mode 100644
index 19181a5..0000000
--- a/crates/yt_dlp/src/duration.rs
+++ /dev/null
@@ -1,78 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-// TODO: This file should be de-duplicated with the same file in the 'yt' crate <2024-06-25>
-
-#[derive(Debug, Clone, Copy)]
-pub struct Duration {
-    time: u32,
-}
-
-impl From<&str> for Duration {
-    fn from(v: &str) -> Self {
-        let buf: Vec<_> = v.split(':').take(2).collect();
-        Self {
-            time: (buf[0].parse::<u32>().expect("Should be a number") * 60)
-                + buf[1].parse::<u32>().expect("Should be a number"),
-        }
-    }
-}
-
-impl From<Option<f64>> for Duration {
-    fn from(value: Option<f64>) -> Self {
-        Self {
-            #[allow(
-                clippy::cast_possible_truncation,
-                clippy::cast_precision_loss,
-                clippy::cast_sign_loss
-            )]
-            time: value.unwrap_or(0.0).ceil() as u32,
-        }
-    }
-}
-
-impl std::fmt::Display for Duration {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        const SECOND: u32 = 1;
-        const MINUTE: u32 = 60 * SECOND;
-        const HOUR: u32 = 60 * MINUTE;
-
-        let base_hour = self.time - (self.time % HOUR);
-        let base_min = (self.time % HOUR) - ((self.time % HOUR) % MINUTE);
-        let base_sec = (self.time % HOUR) % MINUTE;
-
-        let h = base_hour / HOUR;
-        let m = base_min / MINUTE;
-        let s = base_sec / SECOND;
-
-        if self.time == 0 {
-            write!(f, "0s")
-        } else if h > 0 {
-            write!(f, "{h}h {m}m")
-        } else {
-            write!(f, "{m}m {s}s")
-        }
-    }
-}
-#[cfg(test)]
-mod test {
-    use super::Duration;
-
-    #[test]
-    fn test_display_duration_1h() {
-        let dur = Duration { time: 60 * 60 };
-        assert_eq!("1h 0m".to_owned(), dur.to_string());
-    }
-    #[test]
-    fn test_display_duration_30min() {
-        let dur = Duration { time: 60 * 30 };
-        assert_eq!("30m 0s".to_owned(), dur.to_string());
-    }
-}
diff --git a/crates/yt_dlp/src/error.rs b/crates/yt_dlp/src/error.rs
deleted file mode 100644
index 3881f0b..0000000
--- a/crates/yt_dlp/src/error.rs
+++ /dev/null
@@ -1,68 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{fmt::Display, io};
-
-use pyo3::Python;
-
-#[derive(Debug)]
-#[allow(clippy::module_name_repetitions)]
-pub enum YtDlpError {
-    ResponseParseError {
-        error: serde_json::error::Error,
-    },
-    PythonError {
-        error: Box<pyo3::PyErr>,
-        kind: String,
-    },
-    IoError {
-        error: io::Error,
-    },
-}
-
-impl std::error::Error for YtDlpError {}
-
-impl Display for YtDlpError {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        match self {
-            YtDlpError::ResponseParseError { error } => write!(
-                f,
-                include_str!("./python_json_decode_failed.error_msg"),
-                error
-            ),
-            YtDlpError::PythonError { error, kind: _ } => write!(f, "Python error: {error}"),
-            YtDlpError::IoError { error } => write!(f, "Io error: {error}"),
-        }
-    }
-}
-
-impl From<serde_json::error::Error> for YtDlpError {
-    fn from(value: serde_json::error::Error) -> Self {
-        Self::ResponseParseError { error: value }
-    }
-}
-
-impl From<pyo3::PyErr> for YtDlpError {
-    fn from(value: pyo3::PyErr) -> Self {
-        Python::with_gil(|py| {
-            let kind = value.get_type(py).to_string();
-            Self::PythonError {
-                error: Box::new(value),
-                kind,
-            }
-        })
-    }
-}
-
-impl From<io::Error> for YtDlpError {
-    fn from(value: io::Error) -> Self {
-        Self::IoError { error: value }
-    }
-}
diff --git a/crates/yt_dlp/src/info_json.rs b/crates/yt_dlp/src/info_json.rs
new file mode 100644
index 0000000..3ed08ee
--- /dev/null
+++ b/crates/yt_dlp/src/info_json.rs
@@ -0,0 +1,56 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use pyo3::{
+    Bound, Python, intern,
+    types::{PyAnyMethods, PyDict},
+};
+
+pub type InfoJson = serde_json::Map<String, serde_json::Value>;
+
+/// # Panics
+/// If expectation about python operations fail.
+#[must_use]
+pub fn json_loads(
+    input: serde_json::Map<String, serde_json::Value>,
+    py: Python<'_>,
+) -> Bound<'_, PyDict> {
+    let json = py.import(intern!(py, "json")).expect("Module exists");
+    let loads = json.getattr(intern!(py, "loads")).expect("Method exists");
+    let self_str = serde_json::to_string(&serde_json::Value::Object(input)).expect("Vaild json");
+    let dict = loads
+        .call((self_str,), None)
+        .expect("Vaild json is always a valid dict");
+
+    dict.downcast_into().expect("Should always be a dict")
+}
+
+/// # Panics
+/// If expectation about python operations fail.
+#[must_use]
+pub fn json_dumps(input: &Bound<'_, PyDict>) -> serde_json::Map<String, serde_json::Value> {
+    let py = input.py();
+
+    let json = py.import(intern!(py, "json")).expect("Module exists");
+    let dumps = json.getattr(intern!(py, "dumps")).expect("Method exists");
+    let dict = dumps
+        .call((input,), None)
+        .map_err(|err| err.print(py))
+        .expect("Might not always work, but for our dicts it works");
+
+    let string: String = dict.extract().expect("Should always be a string");
+
+    let value: serde_json::Value = serde_json::from_str(&string).expect("Should be valid json");
+
+    match value {
+        serde_json::Value::Object(map) => map,
+        _ => unreachable!("These should not be json.dumps output"),
+    }
+}
diff --git a/crates/yt_dlp/src/lib.rs b/crates/yt_dlp/src/lib.rs
index 40610c2..6be5e87 100644
--- a/crates/yt_dlp/src/lib.rs
+++ b/crates/yt_dlp/src/lib.rs
@@ -1,6 +1,6 @@
 // yt - A fully featured command line YouTube client
 //
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
 // SPDX-License-Identifier: GPL-3.0-or-later
 //
 // This file is part of Yt.
@@ -8,544 +8,371 @@
 // You should have received a copy of the License along with this program.
 // If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint.
-#![allow(unsafe_op_in_unsafe_fn)]
-#![allow(clippy::missing_errors_doc)]
+//! The `yt_dlp` interface is completely contained in the [`YoutubeDL`] structure.
 
-use std::io::stderr;
-use std::{env, process};
-use std::{fs::File, io::Write};
+use std::path::PathBuf;
 
-use std::{path::PathBuf, sync::Once};
-
-use crate::{duration::Duration, logging::setup_logging, wrapper::info_json::InfoJson};
-
-use bytes::Bytes;
-use error::YtDlpError;
-use log::{Level, debug, info, log_enabled};
-use pyo3::types::{PyString, PyTuple, PyTupleMethods};
+use log::{debug, info};
 use pyo3::{
-    Bound, PyAny, PyResult, Python, pyfunction,
-    types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyListMethods, PyModule},
-    wrap_pyfunction,
+    Bound, Py, PyAny, Python, intern,
+    types::{PyAnyMethods, PyDict, PyIterator, PyList},
 };
-use serde::Serialize;
-use serde_json::{Map, Value};
 use url::Url;
 
-pub mod duration;
-pub mod error;
-pub mod logging;
-pub mod wrapper;
-
-#[cfg(test)]
-mod tests;
-
-/// Synchronisation helper, to ensure that we don't setup the logger multiple times
-static SYNC_OBJ: Once = Once::new();
-
-/// Add a logger to the yt-dlp options.
-/// If you have an logger set (i.e. for rust), than this will log to rust
-///
-/// # Panics
-/// This should never panic.
-pub fn add_logger_and_sig_handler<'a>(
-    opts: Bound<'a, PyDict>,
-    py: Python<'_>,
-) -> PyResult<Bound<'a, PyDict>> {
-    /// Is the specified record to be logged? Returns false for no,
-    /// true for yes. Filters can either modify log records in-place or
-    /// return a completely different record instance which will replace
-    /// the original log record in any future processing of the event.
-    #[pyfunction]
-    fn filter_error_log(_py: Python<'_>, record: &Bound<'_, PyAny>) -> bool {
-        // Filter out all error logs (they are propagated as rust errors)
-        let levelname: String = record
-            .getattr("levelname")
-            .expect("This should exist")
-            .extract()
-            .expect("This should be a String");
-
-        let return_value = levelname.as_str() != "ERROR";
-
-        if log_enabled!(Level::Debug) && !return_value {
-            let message: String = record
-                .call_method0("getMessage")
-                .expect("This method exists")
-                .extract()
-                .expect("The message is a string");
+use crate::{
+    info_json::{InfoJson, json_dumps, json_loads},
+    python_error::{IntoPythonError, PythonError},
+};
 
-            debug!("Swollowed error message: '{message}'");
+pub mod info_json;
+pub mod options;
+pub mod post_processors;
+pub mod progress_hook;
+pub mod python_error;
+
+#[macro_export]
+macro_rules! json_get {
+    ($value:expr, $name:literal, $into:ident) => {{
+        match $value.get($name) {
+            Some(val) => $crate::json_cast!(@log_key $name, val, $into),
+            None => panic!(
+                concat!(
+                    "Expected '",
+                    $name,
+                    "' to be a key for the '",
+                    stringify!($value),
+                    "' object: {:#?}"
+                ),
+                $value
+            ),
         }
-        return_value
-    }
-
-    setup_logging(py, "yt_dlp")?;
-
-    let logging = PyModule::import(py, "logging")?;
-    let ytdl_logger = logging.call_method1("getLogger", ("yt_dlp",))?;
-
-    // Ensure that all events are logged by setting the log level to NOTSET (we filter on rust's side)
-    // Also use this static, to ensure that we don't configure the logger every time
-    SYNC_OBJ.call_once(|| {
-        // Disable the SIGINT (Ctrl+C) handler, python installs.
-        // This allows the user to actually stop the application with Ctrl+C.
-        // This is here because it can only be run in the main thread and this was here already.
-        py.run(
-            c"\
-import signal
-signal.signal(signal.SIGINT, signal.SIG_DFL)",
-            None,
-            None,
-        )
-        .expect("This code should always work");
-
-        let config_opts = PyDict::new(py);
-        config_opts
-            .set_item("level", 0)
-            .expect("Setting this item should always work");
-
-        logging
-            .call_method("basicConfig", (), Some(&config_opts))
-            .expect("This method exists");
-    });
-
-    ytdl_logger.call_method1(
-        "addFilter",
-        (wrap_pyfunction!(filter_error_log, py).expect("This function can be wrapped"),),
-    )?;
-
-    // This was taken from `ytcc`, I don't think it is still applicable
-    // ytdl_logger.setattr("propagate", false)?;
-    // let logging_null_handler = logging.call_method0("NullHandler")?;
-    // ytdl_logger.setattr("addHandler", logging_null_handler)?;
-
-    opts.set_item("logger", ytdl_logger).expect("Should work");
-
-    Ok(opts)
+    }};
 }
 
-#[pyfunction]
-#[allow(clippy::too_many_lines)]
-#[allow(clippy::missing_panics_doc)]
-#[allow(clippy::items_after_statements)]
-#[allow(
-    clippy::cast_possible_truncation,
-    clippy::cast_sign_loss,
-    clippy::cast_precision_loss
-)]
-pub fn progress_hook(py: Python<'_>, input: &Bound<'_, PyDict>) -> PyResult<()> {
-    // Only add the handler, if the log-level is higher than Debug (this avoids covering debug
-    // messages).
-    if log_enabled!(Level::Debug) {
-        return Ok(());
-    }
-
-    // ANSI ESCAPE CODES Wrappers {{{
-    // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
-    const CSI: &str = "\x1b[";
-    fn clear_whole_line() {
-        eprint!("{CSI}2K");
-    }
-    fn move_to_col(x: usize) {
-        eprint!("{CSI}{x}G");
-    }
-    // }}}
-
-    let input: Map<String, Value> = serde_json::from_str(&json_dumps(
-        py,
-        input
-            .downcast::<PyAny>()
-            .expect("Will always work")
-            .to_owned(),
-    )?)
-    .expect("python's json is valid");
-
-    macro_rules! get {
-        (@interrogate $item:ident, $type_fun:ident, $get_fun:ident, $name:expr) => {{
-            let a = $item.get($name).expect(concat!(
-                "The field '",
-                stringify!($name),
-                "' should exist."
-            ));
-
-            if a.$type_fun() {
-                a.$get_fun().expect(
-                    "The should have been checked in the if guard, so unpacking here is fine",
-                )
+#[macro_export]
+macro_rules! json_try_get {
+    ($value:expr, $name:literal, $into:ident) => {{
+        if let Some(val) = $value.get($name) {
+            if val.is_null() {
+                None
             } else {
-                panic!(
-                    "Value {} => \n{}\n is not of type: {}",
-                    $name,
-                    a,
-                    stringify!($type_fun)
-                );
+                Some(json_cast!(@log_key $name, val, $into))
             }
-        }};
+        } else {
+            None
+        }
+    }};
+}
 
-        ($type_fun:ident, $get_fun:ident, $name1:expr, $name2:expr) => {{
-            let a = get! {@interrogate input, is_object, as_object, $name1};
-            let b = get! {@interrogate a, $type_fun, $get_fun, $name2};
-            b
-        }};
+#[macro_export]
+macro_rules! json_cast {
+    ($value:expr, $into:ident) => {{
+        let value_name = stringify!($value);
+        json_cast!(@log_key value_name, $value, $into)
+    }};
+
+    (@log_key $name:expr, $value:expr, $into:ident) => {{
+        match $value.$into() {
+            Some(result) => result,
+            None => panic!(
+                concat!(
+                    "Expected to be able to cast '{}' value (which is '{:?}') ",
+                    stringify!($into)
+                ),
+                $name,
+                $value
+            ),
+        }
+    }};
+}
 
-        ($type_fun:ident, $get_fun:ident, $name:expr) => {{
-            get! {@interrogate input, $type_fun, $get_fun, $name}
-        }};
-    }
+macro_rules! py_kw_args {
+    ($py:expr => $($kw_arg_name:ident = $kw_arg_val:expr),*) => {{
+        use $crate::python_error::IntoPythonError;
 
-    macro_rules! default_get {
-        (@interrogate $item:ident, $default:expr, $get_fun:ident, $name:expr) => {{
-            let a = if let Some(field) = $item.get($name) {
-                field.$get_fun().unwrap_or($default)
-            } else {
-                $default
-            };
-            a
-        }};
-
-        ($get_fun:ident, $default:expr, $name1:expr, $name2:expr) => {{
-            let a = get! {@interrogate input, is_object, as_object, $name1};
-            let b = default_get! {@interrogate a, $default, $get_fun, $name2};
-            b
-        }};
-
-        ($get_fun:ident, $default:expr, $name:expr) => {{
-            default_get! {@interrogate input, $default, $get_fun, $name}
-        }};
-    }
+        let dict = PyDict::new($py);
 
-    macro_rules! c {
-        ($color:expr, $format:expr) => {
-            format!("\x1b[{}m{}\x1b[0m", $color, $format)
-        };
-    }
+        $(
+            dict.set_item(stringify!($kw_arg_name), $kw_arg_val).wrap_exc($py)?;
+        )*
 
-    fn format_bytes(bytes: u64) -> String {
-        let bytes = Bytes::new(bytes);
-        bytes.to_string()
+        Some(dict)
     }
+    .as_ref()};
+}
+pub(crate) use py_kw_args;
 
-    fn format_speed(speed: f64) -> String {
-        #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
-        let bytes = Bytes::new(speed.floor() as u64);
-        format!("{bytes}/s")
-    }
+/// The core of the `yt_dlp` interface.
+#[derive(Debug)]
+pub struct YoutubeDL {
+    inner: Py<PyAny>,
+    options: serde_json::Map<String, serde_json::Value>,
+}
 
-    let get_title = || -> String {
-        match get! {is_string, as_str, "info_dict", "ext"} {
-            "vtt" => {
-                format!(
-                    "Subtitles ({})",
-                    default_get! {as_str, "<No Subtitle Language>", "info_dict", "name"}
-                )
-            }
-            "webm" | "mp4" | "mp3" | "m4a" => {
-                default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned()
-            }
-            other => panic!("The extension '{other}' is not yet implemented"),
-        }
-    };
-
-    match get! {is_string, as_str, "status"} {
-        "downloading" => {
-            let elapsed = default_get! {as_f64, 0.0f64, "elapsed"};
-            let eta = default_get! {as_f64, 0.0, "eta"};
-            let speed = default_get! {as_f64, 0.0, "speed"};
-
-            let downloaded_bytes = get! {is_u64, as_u64, "downloaded_bytes"};
-            let (total_bytes, bytes_is_estimate): (u64, &'static str) = {
-                let total_bytes = default_get!(as_u64, 0, "total_bytes");
-                if total_bytes == 0 {
-                    let maybe_estimate = default_get!(as_u64, 0, "total_bytes_estimate");
-
-                    if maybe_estimate == 0 {
-                        // The download speed should be in bytes per second and the eta in seconds.
-                        // Thus multiplying them gets us the raw bytes (which were estimated by `yt_dlp`, from their `info.json`)
-                        let bytes_still_needed = (speed * eta).ceil() as u64;
-
-                        (downloaded_bytes + bytes_still_needed, "~")
-                    } else {
-                        (maybe_estimate, "~")
-                    }
-                } else {
-                    (total_bytes, "")
-                }
-            };
-            let percent: f64 = {
-                if total_bytes == 0 {
-                    100.0
-                } else {
-                    (downloaded_bytes as f64 / total_bytes as f64) * 100.0
-                }
-            };
+impl YoutubeDL {
+    /// Fetch the underlying `yt_dlp` and `python` version.
+    ///
+    /// # Errors
+    /// If python attribute access fails.
+    pub fn version(&self) -> Result<(String, String), PythonError> {
+        Python::with_gil(|py| {
+            let yt_dlp = py
+                .import(intern!(py, "yt_dlp"))
+                .wrap_exc(py)?
+                .getattr(intern!(py, "version"))
+                .wrap_exc(py)?
+                .getattr(intern!(py, "__version__"))
+                .wrap_exc(py)?
+                .extract()
+                .wrap_exc(py)?;
 
-            clear_whole_line();
-            move_to_col(1);
-
-            eprint!(
-                "'{}' [{}/{} at {}] -> [{} of {}{} {}] ",
-                c!("34;1", get_title()),
-                c!("33;1", Duration::from(Some(elapsed))),
-                c!("33;1", Duration::from(Some(eta))),
-                c!("32;1", format_speed(speed)),
-                c!("31;1", format_bytes(downloaded_bytes)),
-                c!("31;1", bytes_is_estimate),
-                c!("31;1", format_bytes(total_bytes)),
-                c!("36;1", format!("{:.02}%", percent))
-            );
-            stderr().flush()?;
-        }
-        "finished" => {
-            eprintln!("-> Finished downloading.");
-        }
-        "error" => {
-            // TODO: This should probably return an Err. But I'm not so sure where the error would
-            // bubble up to (i.e., who would catch it) <2025-01-21>
-            eprintln!("-> Error while downloading: {}", get_title());
-            process::exit(1);
-        }
-        other => unreachable!("'{other}' should not be a valid state!"),
-    };
+            let python = py.version();
 
-    Ok(())
-}
+            Ok((yt_dlp, python.to_owned()))
+        })
+    }
 
-pub fn add_hooks<'a>(opts: Bound<'a, PyDict>, py: Python<'_>) -> PyResult<Bound<'a, PyDict>> {
-    if let Some(hooks) = opts.get_item("progress_hooks")? {
-        let hooks = hooks.downcast::<PyList>()?;
-        hooks.append(wrap_pyfunction!(progress_hook, py)?)?;
+    /// Download a given list of URLs.
+    /// Returns the paths they were downloaded to.
+    ///
+    /// # Errors
+    /// If one of the downloads error.
+    pub fn download(&self, urls: &[Url]) -> Result<Vec<PathBuf>, extract_info::Error> {
+        let mut out_paths = Vec::with_capacity(urls.len());
+
+        for url in urls {
+            info!("Started downloading url: '{url}'");
+            let info_json = self.extract_info(url, true, true)?;
+
+            // Try to work around yt-dlp type weirdness
+            let result_string = if let Some(filename) = json_try_get!(info_json, "filename", as_str)
+            {
+                PathBuf::from(filename)
+            } else {
+                PathBuf::from(json_get!(
+                    json_cast!(
+                        json_get!(info_json, "requested_downloads", as_array)[0],
+                        as_object
+                    ),
+                    "filename",
+                    as_str
+                ))
+            };
 
-        opts.set_item("progress_hooks", hooks)?;
-    } else {
-        // No hooks are set yet
-        let hooks_list = PyList::new(py, &[wrap_pyfunction!(progress_hook, py)?])?;
+            out_paths.push(result_string);
+            info!("Finished downloading url");
+        }
 
-        opts.set_item("progress_hooks", hooks_list)?;
+        Ok(out_paths)
     }
 
-    Ok(opts)
-}
+    /// `extract_info(self, url, download=True, ie_key=None, extra_info=None, process=True, force_generic_extractor=False)`
+    ///
+    /// Extract and return the information dictionary of the URL
+    ///
+    /// Arguments:
+    /// - `url`          URL to extract
+    ///
+    /// Keyword arguments:
+    /// :`download`     Whether to download videos
+    /// :`process`      Whether to resolve all unresolved references (URLs, playlist items).
+    ///                 Must be True for download to work
+    ///
+    /// # Panics
+    /// If expectations about python fail to hold.
+    ///
+    /// # Errors
+    /// If python operations fail.
+    pub fn extract_info(
+        &self,
+        url: &Url,
+        download: bool,
+        process: bool,
+    ) -> Result<InfoJson, extract_info::Error> {
+        Python::with_gil(|py| {
+            let inner = self
+                .inner
+                .bind(py)
+                .getattr(intern!(py, "extract_info"))
+                .wrap_exc(py)?;
+
+            let result = inner
+                .call(
+                    (url.to_string(),),
+                    py_kw_args!(py => download = download, process = process),
+                )
+                .wrap_exc(py)?
+                .downcast_into::<PyDict>()
+                .expect("This is a dict");
+
+            // Resolve the generator object
+            if let Ok(generator) = result.get_item(intern!(py, "entries")) {
+                if generator.is_instance_of::<PyList>() {
+                    // already resolved. Do nothing
+                } else if let Ok(generator) = generator.downcast::<PyIterator>() {
+                    // A python generator object.
+                    let max_backlog = json_try_get!(self.options, "playlistend", as_u64)
+                        .map_or(10000, |playlistend| {
+                            usize::try_from(playlistend).expect("Should work")
+                        });
+
+                    let mut out = vec![];
+                    for output in generator {
+                        out.push(output.wrap_exc(py)?);
+
+                        if out.len() == max_backlog {
+                            break;
+                        }
+                    }
 
-/// Take the result of the ie (may be modified) and resolve all unresolved
-/// references (URLs, playlist items).
-///
-/// It will also download the videos if 'download'.
-/// Returns the resolved `ie_result`.
-#[allow(clippy::unused_async)]
-#[allow(clippy::missing_panics_doc)]
-pub async fn process_ie_result(
-    yt_dlp_opts: &Map<String, Value>,
-    ie_result: InfoJson,
-    download: bool,
-) -> Result<InfoJson, YtDlpError> {
-    Python::with_gil(|py| -> Result<InfoJson, YtDlpError> {
-        let opts = json_map_to_py_dict(yt_dlp_opts, py)?;
-
-        let instance = get_yt_dlp(py, opts)?;
-
-        let args = {
-            let ie_result = json_loads_str(py, ie_result)?;
-            (ie_result,)
-        };
-
-        let kwargs = PyDict::new(py);
-        kwargs.set_item("download", download)?;
-
-        let result = instance
-            .call_method("process_ie_result", args, Some(&kwargs))?
-            .downcast_into::<PyDict>()
-            .expect("This is a dict");
-
-        let result_str = json_dumps(py, result.into_any())?;
-
-        serde_json::from_str(&result_str).map_err(Into::into)
-    })
-}
+                    result.set_item(intern!(py, "entries"), out).wrap_exc(py)?;
+                } else {
+                    // Probably some sort of paged list (`OnDemand` or otherwise)
+                    let max_backlog = json_try_get!(self.options, "playlistend", as_u64)
+                        .map_or(10000, |playlistend| {
+                            usize::try_from(playlistend).expect("Should work")
+                        });
 
-/// `extract_info(self, url, download=True, ie_key=None, extra_info=None, process=True, force_generic_extractor=False)`
-///
-/// Extract and return the information dictionary of the URL
-///
-/// Arguments:
-/// @param url          URL to extract
-///
-/// Keyword arguments:
-/// @param download     Whether to download videos
-/// @param process      Whether to resolve all unresolved references (URLs, playlist items).
-///                     Must be True for download to work
-/// @param `ie_key`       Use only the extractor with this key
-///
-/// @param `extra_info`   Dictionary containing the extra values to add to the info (For internal use only)
-/// @`force_generic_extractor`  Force using the generic extractor (Deprecated; use `ie_key`='Generic')
-#[allow(clippy::unused_async)]
-#[allow(clippy::missing_panics_doc)]
-pub async fn extract_info(
-    yt_dlp_opts: &Map<String, Value>,
-    url: &Url,
-    download: bool,
-    process: bool,
-) -> Result<InfoJson, YtDlpError> {
-    Python::with_gil(|py| -> Result<InfoJson, YtDlpError> {
-        let opts = json_map_to_py_dict(yt_dlp_opts, py)?;
-
-        let instance = get_yt_dlp(py, opts)?;
-        let args = (url.as_str(),);
-
-        let kwargs = PyDict::new(py);
-        kwargs.set_item("download", download)?;
-        kwargs.set_item("process", process)?;
-
-        let result = instance
-            .call_method("extract_info", args, Some(&kwargs))?
-            .downcast_into::<PyDict>()
-            .expect("This is a dict");
-
-        // Resolve the generator object
-        if let Some(generator) = result.get_item("entries")? {
-            if generator.is_instance_of::<PyList>() {
-                // already resolved. Do nothing
-            } else {
-                let max_backlog = yt_dlp_opts.get("playlistend").map_or(10000, |value| {
-                    usize::try_from(value.as_u64().expect("Works")).expect("Should work")
-                });
+                    let next = generator.getattr(intern!(py, "getslice")).wrap_exc(py)?;
 
-                let mut out = vec![];
-                while let Ok(output) = generator.call_method0("__next__") {
-                    out.push(output);
+                    let output = next
+                        .call((), py_kw_args!(py => start = 0, end = max_backlog))
+                        .wrap_exc(py)?;
 
-                    if out.len() == max_backlog {
-                        break;
-                    }
+                    result
+                        .set_item(intern!(py, "entries"), output)
+                        .wrap_exc(py)?;
                 }
-                result.set_item("entries", out)?;
             }
-        }
-
-        let result_str = json_dumps(py, result.into_any())?;
 
-        if let Ok(confirm) = env::var("YT_STORE_INFO_JSON") {
-            if confirm == "yes" {
-                let mut file = File::create("output.info.json")?;
-                write!(file, "{result_str}").unwrap();
-            }
-        }
+            let result = self.prepare_info_json(&result, py)?;
 
-        serde_json::from_str(&result_str).map_err(Into::into)
-    })
-}
+            Ok(result)
+        })
+    }
 
-/// # Panics
-/// Only if python fails to return a valid URL.
-pub fn unsmuggle_url(smug_url: &Url) -> PyResult<Url> {
-    Python::with_gil(|py| {
-        let utils = get_yt_dlp_utils(py)?;
-        let url = utils
-            .call_method1("unsmuggle_url", (smug_url.as_str(),))?
-            .downcast::<PyTuple>()?
-            .get_item(0)?;
-
-        let url: Url = url
-            .downcast::<PyString>()?
-            .to_string()
-            .parse()
-            .expect("Python should be able to return a valid url");
-
-        Ok(url)
-    })
-}
+    /// Take the (potentially modified) result of the information extractor (i.e.,
+    /// [`Self::extract_info`] with `process` and `download` set to false)
+    /// and resolve all unresolved references (URLs,
+    /// playlist items).
+    ///
+    /// It will also download the videos if 'download' is true.
+    /// Returns the resolved `ie_result`.
+    ///
+    /// # Panics
+    /// If expectations about python fail to hold.
+    ///
+    /// # Errors
+    /// If python operations fail.
+    pub fn process_ie_result(
+        &self,
+        ie_result: InfoJson,
+        download: bool,
+    ) -> Result<InfoJson, process_ie_result::Error> {
+        Python::with_gil(|py| {
+            let inner = self
+                .inner
+                .bind(py)
+                .getattr(intern!(py, "process_ie_result"))
+                .wrap_exc(py)?;
+
+            let result = inner
+                .call(
+                    (json_loads(ie_result, py),),
+                    py_kw_args!(py => download = download),
+                )
+                .wrap_exc(py)?
+                .downcast_into::<PyDict>()
+                .expect("This is a dict");
 
-/// Download a given list of URLs.
-/// Returns the paths they were downloaded to.
-///
-/// # Panics
-/// Only if `yt_dlp` changes their `info_json` schema.
-pub async fn download(
-    urls: &[Url],
-    download_options: &Map<String, Value>,
-) -> Result<Vec<PathBuf>, YtDlpError> {
-    let mut out_paths = Vec::with_capacity(urls.len());
-
-    for url in urls {
-        info!("Started downloading url: '{}'", url);
-        let info_json = extract_info(download_options, url, true, true).await?;
-
-        // Try to work around yt-dlp type weirdness
-        let result_string = if let Some(filename) = info_json.filename {
-            filename
-        } else {
-            info_json.requested_downloads.expect("This must exist")[0]
-                .filename
-                .clone()
-        };
+            let result = self.prepare_info_json(&result, py)?;
 
-        out_paths.push(result_string);
-        info!("Finished downloading url: '{}'", url);
+            Ok(result)
+        })
     }
 
-    Ok(out_paths)
-}
-
-fn json_map_to_py_dict<'a>(
-    map: &Map<String, Value>,
-    py: Python<'a>,
-) -> PyResult<Bound<'a, PyDict>> {
-    let json_string = serde_json::to_string(&map).expect("This must always work");
+    /// Close this [`YoutubeDL`] instance, and stop all currently running downloads.
+    ///
+    /// # Errors
+    /// If python operations fail.
+    pub fn close(&self) -> Result<(), close::Error> {
+        Python::with_gil(|py| {
+            debug!("Closing YoutubeDL.");
 
-    let python_dict = json_loads(py, json_string)?;
+            let inner = self
+                .inner
+                .bind(py)
+                .getattr(intern!(py, "close"))
+                .wrap_exc(py)?;
 
-    Ok(python_dict)
-}
+            inner.call0().wrap_exc(py)?;
 
-fn json_dumps(py: Python<'_>, input: Bound<'_, PyAny>) -> PyResult<String> {
-    //     json.dumps(yt_dlp.sanitize_info(input))
-
-    let yt_dlp = get_yt_dlp(py, PyDict::new(py))?;
-    let sanitized_result = yt_dlp.call_method1("sanitize_info", (input,))?;
+            Ok(())
+        })
+    }
 
-    let json = PyModule::import(py, "json")?;
-    let dumps = json.getattr("dumps")?;
+    fn prepare_info_json<'py>(
+        &self,
+        info: &Bound<'py, PyDict>,
+        py: Python<'py>,
+    ) -> Result<InfoJson, prepare::Error> {
+        let sanitize = self
+            .inner
+            .bind(py)
+            .getattr(intern!(py, "sanitize_info"))
+            .wrap_exc(py)?;
 
-    let output = dumps.call1((sanitized_result,))?;
+        let value = sanitize.call((info,), None).wrap_exc(py)?;
 
-    let output_str = output.extract::<String>()?;
+        let result = value.downcast::<PyDict>().expect("This should stay a dict");
 
-    Ok(output_str)
+        Ok(json_dumps(result))
+    }
 }
 
-fn json_loads_str<T: Serialize>(py: Python<'_>, input: T) -> PyResult<Bound<'_, PyDict>> {
-    let string = serde_json::to_string(&input).expect("Correct json must be pased");
+#[allow(missing_docs)]
+pub mod close {
+    use crate::python_error::PythonError;
 
-    json_loads(py, string)
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
+    }
 }
+#[allow(missing_docs)]
+pub mod process_ie_result {
+    use crate::{prepare, python_error::PythonError};
 
-fn json_loads(py: Python<'_>, input: String) -> PyResult<Bound<'_, PyDict>> {
-    //     json.loads(input)
-
-    let json = PyModule::import(py, "json")?;
-    let dumps = json.getattr("loads")?;
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
 
-    let output = dumps.call1((input,))?;
-
-    Ok(output
-        .downcast::<PyDict>()
-        .expect("This should always be a PyDict")
-        .clone())
+        #[error("Failed to prepare the info json")]
+        InfoJsonPrepare(#[from] prepare::Error),
+    }
 }
+#[allow(missing_docs)]
+pub mod extract_info {
+    use crate::{prepare, python_error::PythonError};
 
-fn get_yt_dlp_utils(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
-    let yt_dlp = PyModule::import(py, "yt_dlp")?;
-    let utils = yt_dlp.getattr("utils")?;
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
 
-    Ok(utils)
+        #[error("Failed to prepare the info json")]
+        InfoJsonPrepare(#[from] prepare::Error),
+    }
 }
-fn get_yt_dlp<'a>(py: Python<'a>, opts: Bound<'a, PyDict>) -> PyResult<Bound<'a, PyAny>> {
-    // Unconditionally set a logger
-    let opts = add_logger_and_sig_handler(opts, py)?;
-    let opts = add_hooks(opts, py)?;
-
-    let yt_dlp = PyModule::import(py, "yt_dlp")?;
-    let youtube_dl = yt_dlp.call_method1("YoutubeDL", (opts,))?;
-
-    Ok(youtube_dl)
+#[allow(missing_docs)]
+pub mod prepare {
+    use crate::python_error::PythonError;
+
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
+    }
 }
diff --git a/crates/yt_dlp/src/logging.rs b/crates/yt_dlp/src/logging.rs
deleted file mode 100644
index e731502..0000000
--- a/crates/yt_dlp/src/logging.rs
+++ /dev/null
@@ -1,133 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-// This file is taken from: https://github.com/dylanbstorey/pyo3-pylogger/blob/d89e0d6820ebc4f067647e3b74af59dbc4941dd5/src/lib.rs
-// It is licensed under the Apache 2.0 License, copyright up to 2024 by Dylan Storey
-// It was modified by Benedikt Peetz 2024
-
-// The pyo3 `pyfunction` proc-macros call unsafe functions internally, which trigger this lint.
-#![allow(unsafe_op_in_unsafe_fn)]
-
-use std::ffi::CString;
-
-use log::{Level, MetadataBuilder, Record, logger};
-use pyo3::{
-    Bound, PyAny, PyResult, Python,
-    prelude::{PyAnyMethods, PyListMethods, PyModuleMethods},
-    pyfunction, wrap_pyfunction,
-};
-
-/// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead.
-#[allow(clippy::needless_pass_by_value)]
-#[pyfunction]
-fn host_log(record: Bound<'_, PyAny>, rust_target: &str) -> PyResult<()> {
-    let level = record.getattr("levelno")?;
-    let message = record.getattr("getMessage")?.call0()?.to_string();
-    let pathname = record.getattr("pathname")?.to_string();
-    let lineno = record
-        .getattr("lineno")?
-        .to_string()
-        .parse::<u32>()
-        .expect("This should always be a u32");
-
-    let logger_name = record.getattr("name")?.to_string();
-
-    let full_target: Option<String> = if logger_name.trim().is_empty() || logger_name == "root" {
-        None
-    } else {
-        // Libraries (ex: tracing_subscriber::filter::Directive) expect rust-style targets like foo::bar,
-        // and may not deal well with "." as a module separator:
-        let logger_name = logger_name.replace('.', "::");
-        Some(format!("{rust_target}::{logger_name}"))
-    };
-
-    let target = full_target.as_deref().unwrap_or(rust_target);
-
-    // error
-    let error_metadata = if level.ge(40u8)? {
-        MetadataBuilder::new()
-            .target(target)
-            .level(Level::Error)
-            .build()
-    } else if level.ge(30u8)? {
-        MetadataBuilder::new()
-            .target(target)
-            .level(Level::Warn)
-            .build()
-    } else if level.ge(20u8)? {
-        MetadataBuilder::new()
-            .target(target)
-            .level(Level::Info)
-            .build()
-    } else if level.ge(10u8)? {
-        MetadataBuilder::new()
-            .target(target)
-            .level(Level::Debug)
-            .build()
-    } else {
-        MetadataBuilder::new()
-            .target(target)
-            .level(Level::Trace)
-            .build()
-    };
-
-    logger().log(
-        &Record::builder()
-            .metadata(error_metadata)
-            .args(format_args!("{}", &message))
-            .line(Some(lineno))
-            .file(None)
-            .module_path(Some(&pathname))
-            .build(),
-    );
-
-    Ok(())
-}
-
-/// Registers the `host_log` function in rust as the event handler for Python's logging logger
-/// This function needs to be called from within a pyo3 context as early as possible to ensure logging messages
-/// arrive to the rust consumer.
-///
-/// # Panics
-/// Only if internal assertions fail.
-#[allow(clippy::module_name_repetitions)]
-pub fn setup_logging(py: Python<'_>, target: &str) -> PyResult<()> {
-    let logging = py.import("logging")?;
-
-    logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?;
-
-    py.run(
-        CString::new(format!(
-            r#"
-class HostHandler(Handler):
-    def __init__(self, level=0):
-        super().__init__(level=level)
-
-    def emit(self, record):
-        host_log(record,"{target}")
-
-oldBasicConfig = basicConfig
-def basicConfig(*pargs, **kwargs):
-    if "handlers" not in kwargs:
-        kwargs["handlers"] = [HostHandler()]
-    return oldBasicConfig(*pargs, **kwargs)
-"#
-        ))
-        .expect("This is hardcoded")
-        .as_c_str(),
-        Some(&logging.dict()),
-        None,
-    )?;
-
-    let all = logging.index()?;
-    all.append("HostHandler")?;
-
-    Ok(())
-}
diff --git a/crates/yt_dlp/src/options.rs b/crates/yt_dlp/src/options.rs
new file mode 100644
index 0000000..ad30301
--- /dev/null
+++ b/crates/yt_dlp/src/options.rs
@@ -0,0 +1,207 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::sync;
+
+use pyo3::{
+    Bound, IntoPyObjectExt, PyAny, PyResult, Python, intern,
+    types::{PyAnyMethods, PyCFunction, PyDict, PyTuple},
+};
+use pyo3_pylogger::setup_logging;
+
+use crate::{
+    YoutubeDL, json_loads, post_processors, py_kw_args,
+    python_error::{IntoPythonError, PythonError},
+};
+
+pub type ProgressHookFunction = fn(py: Python<'_>) -> PyResult<Bound<'_, PyCFunction>>;
+pub type PostProcessorFunction = fn(py: Python<'_>) -> PyResult<Bound<'_, PyAny>>;
+
+/// Options, that are used to customize the download behaviour.
+///
+/// In the future, this might get a Builder api.
+///
+/// See `help(yt_dlp.YoutubeDL())` from python for a full list of available options.
+#[derive(Default, Debug)]
+pub struct YoutubeDLOptions {
+    options: serde_json::Map<String, serde_json::Value>,
+    progress_hook: Option<ProgressHookFunction>,
+    post_processors: Vec<PostProcessorFunction>,
+}
+
+impl YoutubeDLOptions {
+    #[must_use]
+    pub fn new() -> Self {
+        let me = Self {
+            options: serde_json::Map::new(),
+            progress_hook: None,
+            post_processors: vec![],
+        };
+
+        me.with_post_processor(post_processors::dearrow::process)
+    }
+
+    #[must_use]
+    pub fn set(self, key: impl Into<String>, value: impl Into<serde_json::Value>) -> Self {
+        let mut options = self.options;
+        options.insert(key.into(), value.into());
+
+        Self { options, ..self }
+    }
+
+    #[must_use]
+    pub fn with_progress_hook(self, progress_hook: ProgressHookFunction) -> Self {
+        if let Some(_previous_hook) = self.progress_hook {
+            todo!()
+        } else {
+            Self {
+                progress_hook: Some(progress_hook),
+                ..self
+            }
+        }
+    }
+
+    #[must_use]
+    pub fn with_post_processor(mut self, pp: PostProcessorFunction) -> Self {
+        self.post_processors.push(pp);
+        self
+    }
+
+    /// # Errors
+    /// If the underlying [`YoutubeDL::from_options`] errors.
+    pub fn build(self) -> Result<YoutubeDL, build::Error> {
+        YoutubeDL::from_options(self)
+    }
+
+    #[must_use]
+    pub fn from_json_options(options: serde_json::Map<String, serde_json::Value>) -> Self {
+        Self {
+            options,
+            ..Self::new()
+        }
+    }
+
+    #[must_use]
+    pub fn get(&self, key: &str) -> Option<&serde_json::Value> {
+        self.options.get(key)
+    }
+}
+
+impl YoutubeDL {
+    /// Construct this instance from options.
+    ///
+    /// # Panics
+    /// If `yt_dlp` changed their interface.
+    ///
+    /// # Errors
+    /// If a python call fails.
+    #[allow(clippy::too_many_lines)]
+    pub fn from_options(options: YoutubeDLOptions) -> Result<Self, build::Error> {
+        pyo3::prepare_freethreaded_python();
+
+        let output_options = options.options.clone();
+
+        let yt_dlp_module = Python::with_gil(|py| {
+            let opts = json_loads(options.options, py);
+
+            {
+                static CALL_ONCE: sync::Once = sync::Once::new();
+
+                CALL_ONCE.call_once(|| {
+                    py.run(
+                        c"
+import signal
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+              ",
+                        None,
+                        None,
+                    )
+                    .unwrap_or_else(|err| {
+                        panic!("Failed to disable python signal handling: {err}")
+                    });
+                });
+            }
+
+            {
+                // Setup the progress hook
+                if let Some(ph) = options.progress_hook {
+                    opts.set_item(intern!(py, "progress_hooks"), vec![ph(py).wrap_exc(py)?])
+                        .wrap_exc(py)?;
+                }
+            }
+
+            {
+                // Unconditionally set a logger.
+                // Otherwise, yt_dlp will log to stderr.
+
+                let ytdl_logger = setup_logging(py, "yt_dlp").wrap_exc(py)?;
+
+                opts.set_item(intern!(py, "logger"), ytdl_logger)
+                    .wrap_exc(py)?;
+            }
+
+            let inner = {
+                let p_params = opts.into_bound_py_any(py).wrap_exc(py)?;
+                let p_auto_init = true.into_bound_py_any(py).wrap_exc(py)?;
+
+                py.import(intern!(py, "yt_dlp.YoutubeDL"))
+                    .wrap_exc(py)?
+                    .getattr(intern!(py, "YoutubeDL"))
+                    .wrap_exc(py)?
+                    .call1(
+                        PyTuple::new(
+                            py,
+                            [
+                                p_params.into_bound_py_any(py).wrap_exc(py)?,
+                                p_auto_init.into_bound_py_any(py).wrap_exc(py)?,
+                            ],
+                        )
+                        .wrap_exc(py)?,
+                    )
+                    .wrap_exc(py)?
+            };
+
+            {
+                // Setup the post processors
+                let add_post_processor_fun = inner
+                    .getattr(intern!(py, "add_post_processor"))
+                    .wrap_exc(py)?;
+
+                for pp in options.post_processors {
+                    add_post_processor_fun
+                        .call(
+                            (pp(py).wrap_exc(py)?.into_bound_py_any(py).wrap_exc(py)?,),
+                            // "when" can take any value in yt_dlp.utils.POSTPROCESS_WHEN
+                            py_kw_args!(py => when = "pre_process"),
+                        )
+                        .wrap_exc(py)?;
+                }
+            }
+
+            Ok::<_, PythonError>(inner.unbind())
+        })?;
+
+        Ok(Self {
+            inner: yt_dlp_module,
+            options: output_options,
+        })
+    }
+}
+
+#[allow(missing_docs)]
+pub mod build {
+    use crate::python_error::PythonError;
+
+    #[derive(Debug, thiserror::Error)]
+    pub enum Error {
+        #[error(transparent)]
+        Python(#[from] PythonError),
+    }
+}
diff --git a/crates/yt_dlp/src/post_processors/dearrow.rs b/crates/yt_dlp/src/post_processors/dearrow.rs
new file mode 100644
index 0000000..f35f301
--- /dev/null
+++ b/crates/yt_dlp/src/post_processors/dearrow.rs
@@ -0,0 +1,247 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use curl::easy::Easy;
+use log::{error, info, trace, warn};
+use pyo3::{
+    Bound, PyAny, PyErr, PyResult, Python, exceptions, intern, pyfunction,
+    types::{PyAnyMethods, PyDict, PyModule},
+    wrap_pyfunction,
+};
+use serde::{Deserialize, Serialize};
+
+use crate::{
+    pydict_cast, pydict_get,
+    python_error::{IntoPythonError, PythonError},
+};
+
+/// # Errors
+/// - If the underlying function returns an error.
+/// - If python operations fail.
+pub fn process(py: Python<'_>) -> PyResult<Bound<'_, PyAny>> {
+    #[pyfunction]
+    fn actual_processor(info_json: Bound<'_, PyDict>) -> PyResult<Bound<'_, PyDict>> {
+        let output = match unwrapped_process(info_json) {
+            Ok(ok) => ok,
+            Err(err) => {
+                return Err(PyErr::new::<exceptions::PyRuntimeError, _>(err.to_string()));
+            }
+        };
+        Ok(output)
+    }
+
+    let module = PyModule::new(py, "rust_post_processors")?;
+    let scope = PyDict::new(py);
+    scope.set_item(
+        intern!(py, "actual_processor"),
+        wrap_pyfunction!(actual_processor, module)?,
+    )?;
+    py.run(
+        c"
+import yt_dlp
+
+class DeArrow(yt_dlp.postprocessor.PostProcessor):
+    def run(self, info):
+        info = actual_processor(info)
+        return [], info
+
+inst = DeArrow()
+",
+        Some(&scope),
+        None,
+    )?;
+
+    Ok(scope.get_item(intern!(py, "inst"))?.downcast_into()?)
+}
+
+/// # Errors
+/// If the API access fails.
+pub fn unwrapped_process(info: Bound<'_, PyDict>) -> Result<Bound<'_, PyDict>, Error> {
+    if pydict_get!(info, "extractor_key", String).as_str() != "Youtube" {
+        return Ok(info);
+    }
+
+    let mut retry_num = 3;
+    let mut output: DeArrowApi = {
+        loop {
+            let output_bytes = {
+                let mut dst = Vec::new();
+
+                let mut easy = Easy::new();
+                easy.url(
+                    format!(
+                        "https://sponsor.ajay.app/api/branding?videoID={}",
+                        pydict_get!(info, "id", String)
+                    )
+                    .as_str(),
+                )?;
+
+                let mut transfer = easy.transfer();
+                transfer.write_function(|data| {
+                    dst.extend_from_slice(data);
+                    Ok(data.len())
+                })?;
+                transfer.perform()?;
+                drop(transfer);
+
+                dst
+            };
+
+            match serde_json::from_slice(&output_bytes) {
+                Ok(ok) => break ok,
+                Err(err) => {
+                    if retry_num > 0 {
+                        trace!(
+                            "DeArrow: Api access failed, trying again ({retry_num} retries left)"
+                        );
+                        retry_num -= 1;
+                    } else {
+                        let err: serde_json::Error = err;
+                        return Err(err.into());
+                    }
+                }
+            }
+        }
+    };
+
+    // We pop the titles, so we need this vector reversed.
+    output.titles.reverse();
+
+    let title_len = output.titles.len();
+    let mut iterator = output.titles.clone();
+    let selected = loop {
+        let Some(title) = iterator.pop() else {
+            break false;
+        };
+
+        if (title.locked || title.votes < 1) && title_len > 1 {
+            info!(
+                "DeArrow: Skipping title {:#?}, as it is not good enough",
+                title.value
+            );
+            // Skip titles that are not “good” enough.
+            continue;
+        }
+
+        update_title(&info, &title.value).wrap_exc(info.py())?;
+
+        break true;
+    };
+
+    if !selected && title_len != 0 {
+        // No title was selected, even though we had some titles.
+        // Just pick the first one in this case.
+        update_title(&info, &output.titles[0].value).wrap_exc(info.py())?;
+    }
+
+    Ok(info)
+}
+
+#[derive(thiserror::Error, Debug)]
+pub enum Error {
+    #[error(transparent)]
+    Python(#[from] PythonError),
+
+    #[error("Failed to access the DeArrow api: {0}")]
+    Get(#[from] curl::Error),
+
+    #[error("Failed to deserialize a api json return object: {0}")]
+    Deserialize(#[from] serde_json::Error),
+}
+
+fn update_title(info: &Bound<'_, PyDict>, new_title: &str) -> PyResult<()> {
+    let py = info.py();
+
+    assert!(!info.contains(intern!(py, "original_title"))?);
+
+    if let Ok(old_title) = info.get_item(intern!(py, "title")) {
+        warn!(
+            "DeArrow: Updating title from {:#?} to {:#?}",
+            pydict_cast!(old_title, &str),
+            new_title
+        );
+
+        info.set_item(intern!(py, "original_title"), old_title)
+            .expect("We checked, it is a new key");
+    } else {
+        warn!("DeArrow: Setting title to {new_title:#?}");
+    }
+
+    let cleaned_title = {
+        // NOTE(@bpeetz): DeArrow uses `>` as a “Don't format the next word” mark.
+        // They should be removed, if one does not use a auto-formatter. <2025-06-16>
+        new_title.replace('>', "")
+    };
+
+    info.set_item(intern!(py, "title"), cleaned_title)
+        .expect("This should work?");
+
+    Ok(())
+}
+
+#[derive(Serialize, Deserialize)]
+/// See: <https://wiki.sponsor.ajay.app/w/API_Docs/DeArrow>
+struct DeArrowApi {
+    titles: Vec<Title>,
+    thumbnails: Vec<Thumbnail>,
+
+    #[serde(alias = "randomTime")]
+    random_time: Option<f64>,
+
+    #[serde(alias = "videoDuration")]
+    video_duration: Option<f64>,
+
+    #[serde(alias = "casualVotes")]
+    casual_votes: Vec<CasualVote>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct CasualVote {
+    id: String,
+    count: u32,
+    title: String,
+}
+
+#[derive(Serialize, Deserialize, Clone)]
+struct Title {
+    /// Note: Titles will sometimes contain > before a word.
+    /// This tells the auto-formatter to not format a word.
+    /// If you have no auto-formatter, you can ignore this and replace it with an empty string
+    #[serde(alias = "title")]
+    value: String,
+
+    original: bool,
+    votes: u64,
+    locked: bool,
+
+    #[serde(alias = "UUID")]
+    uuid: String,
+
+    /// only present if requested
+    #[serde(alias = "userID")]
+    user_id: Option<String>,
+}
+
+#[derive(Serialize, Deserialize)]
+struct Thumbnail {
+    // null if original is true
+    timestamp: Option<f64>,
+
+    original: bool,
+    votes: u64,
+    locked: bool,
+
+    #[serde(alias = "UUID")]
+    uuid: String,
+
+    /// only present if requested
+    #[serde(alias = "userID")]
+    user_id: Option<String>,
+}
diff --git a/crates/yt_dlp/src/post_processors/mod.rs b/crates/yt_dlp/src/post_processors/mod.rs
new file mode 100644
index 0000000..d9be3f5
--- /dev/null
+++ b/crates/yt_dlp/src/post_processors/mod.rs
@@ -0,0 +1,48 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+pub mod dearrow;
+
+#[macro_export]
+macro_rules! pydict_get {
+    ($value:expr, $name:literal, $into:ty) => {{
+        let item = $value.get_item(pyo3::intern!($value.py(), $name));
+        match &item {
+            Ok(val) => $crate::pydict_cast!(val, $into),
+            Err(_) => panic!(
+                concat!(
+                    "Expected '",
+                    $name,
+                    "' to be a key for the'",
+                    stringify!($value),
+                    "' py dictionary: {:#?}"
+                ),
+                $value
+            ),
+        }
+    }};
+}
+
+#[macro_export]
+macro_rules! pydict_cast {
+    ($value:expr, $into:ty) => {{
+        match $value.extract::<$into>() {
+            Ok(result) => result,
+            Err(val) => panic!(
+                concat!(
+                    "Expected to be able to extract ",
+                    stringify!($into),
+                    " from value ({:#?})."
+                ),
+                val
+            ),
+        }
+    }};
+}
diff --git a/crates/yt_dlp/src/progress_hook.rs b/crates/yt_dlp/src/progress_hook.rs
new file mode 100644
index 0000000..7e5f8a5
--- /dev/null
+++ b/crates/yt_dlp/src/progress_hook.rs
@@ -0,0 +1,67 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+#[macro_export]
+macro_rules! wrap_progress_hook {
+    ($name:ident, $new_name:ident) => {
+        pub(crate) fn $new_name(
+            py: yt_dlp::progress_hook::__priv::pyo3::Python<'_>,
+        ) -> yt_dlp::progress_hook::__priv::pyo3::PyResult<
+            yt_dlp::progress_hook::__priv::pyo3::Bound<
+                '_,
+                yt_dlp::progress_hook::__priv::pyo3::types::PyCFunction,
+            >,
+        > {
+            #[yt_dlp::progress_hook::__priv::pyo3::pyfunction]
+            #[pyo3(crate = "yt_dlp::progress_hook::__priv::pyo3")]
+            fn inner(
+                input: yt_dlp::progress_hook::__priv::pyo3::Bound<
+                    '_,
+                    yt_dlp::progress_hook::__priv::pyo3::types::PyDict,
+                >,
+            ) -> yt_dlp::progress_hook::__priv::pyo3::PyResult<()> {
+                let processed_input = {
+                    let new_dict = yt_dlp::progress_hook::__priv::pyo3::types::PyDict::new(input.py());
+
+                    input
+                        .into_iter()
+                        .filter_map(|(name, value)| {
+                            let real_name = yt_dlp::progress_hook::__priv::pyo3::types::PyAnyMethods::extract::<String>(&name).expect("Should always be a string");
+
+                            if real_name.starts_with('_') {
+                                None
+                            } else {
+                                Some((real_name, value))
+                            }
+                        })
+                        .for_each(|(key, value)| {
+                            yt_dlp::progress_hook::__priv::pyo3::types::PyDictMethods::set_item(&new_dict, &key, value)
+                                .expect("This is a transpositions, should always be valid");
+                        });
+                    yt_dlp::progress_hook::__priv::json_dumps(&new_dict)
+                };
+
+                $name(processed_input)?;
+
+                Ok(())
+            }
+
+            let module = yt_dlp::progress_hook::__priv::pyo3::types::PyModule::new(py, "progress_hook")?;
+            let fun = yt_dlp::progress_hook::__priv::pyo3::wrap_pyfunction!(inner, module)?;
+
+            Ok(fun)
+        }
+    };
+}
+
+pub mod __priv {
+    pub use crate::info_json::{json_dumps, json_loads};
+    pub use pyo3;
+}
diff --git a/crates/yt_dlp/src/python_error.rs b/crates/yt_dlp/src/python_error.rs
new file mode 100644
index 0000000..0c442b3
--- /dev/null
+++ b/crates/yt_dlp/src/python_error.rs
@@ -0,0 +1,55 @@
+// yt - A fully featured command line YouTube client
+//
+// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+// SPDX-License-Identifier: GPL-3.0-or-later
+//
+// This file is part of Yt.
+//
+// You should have received a copy of the License along with this program.
+// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+
+use std::fmt::{self, Display};
+
+use log::{Level, debug, log_enabled};
+use pyo3::{PyErr, Python, types::PyTracebackMethods};
+
+#[derive(thiserror::Error, Debug)]
+pub struct PythonError(pub String);
+
+pub(crate) trait IntoPythonError<T>: Sized {
+    fn wrap_exc(self, py: Python<'_>) -> Result<T, PythonError>;
+}
+
+impl<T> IntoPythonError<T> for Result<T, PyErr> {
+    fn wrap_exc(self, py: Python<'_>) -> Result<T, PythonError> {
+        self.map_err(|exc| PythonError::from_exception(py, &exc))
+    }
+}
+
+impl Display for PythonError {
+    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+        write!(f, "Python threw an exception: {}", self.0)
+    }
+}
+
+impl PythonError {
+    pub(super) fn from_exception(py: Python<'_>, exc: &PyErr) -> Self {
+        let buffer = process_exception(py, exc);
+        Self(buffer)
+    }
+}
+
+pub(super) fn process_exception(py: Python<'_>, err: &PyErr) -> String {
+    if log_enabled!(Level::Debug) {
+        let mut output = err.to_string();
+
+        if let Some(tb) = err.traceback(py) {
+            output.push('\n');
+            output.push_str(&tb.format().unwrap());
+        }
+
+        debug!("Python threw an exception: {output}");
+    }
+
+    err.to_string()
+}
diff --git a/crates/yt_dlp/src/python_json_decode_failed.error_msg b/crates/yt_dlp/src/python_json_decode_failed.error_msg
deleted file mode 100644
index d10688e..0000000
--- a/crates/yt_dlp/src/python_json_decode_failed.error_msg
+++ /dev/null
@@ -1,5 +0,0 @@
-Failed to decode yt-dlp's response: {}
-
-This is probably a bug.
-Try running the command again with the `YT_STORE_INFO_JSON=yes` environment variable set
-and maybe debug it further via `yt check info-json output.info.json`.
diff --git a/crates/yt_dlp/src/tests.rs b/crates/yt_dlp/src/tests.rs
deleted file mode 100644
index 91b6626..0000000
--- a/crates/yt_dlp/src/tests.rs
+++ /dev/null
@@ -1,89 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::sync::LazyLock;
-
-use serde_json::{Value, json};
-use url::Url;
-
-static YT_OPTS: LazyLock<serde_json::Map<String, Value>> = LazyLock::new(|| {
-    match json!({
-        "playliststart": 1,
-        "playlistend": 10,
-        "noplaylist": false,
-        "extract_flat": false,
-    }) {
-        Value::Object(obj) => obj,
-        _ => unreachable!("This json is hardcoded"),
-    }
-});
-
-#[tokio::test]
-#[ignore = "This test hangs forever"]
-async fn test_extract_info_video() {
-    let info = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://www.youtube.com/watch?v=dbjPnXaacAU").expect("Is valid."),
-        false,
-        false,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{info:#?}");
-}
-
-#[tokio::test]
-#[ignore = "This test hangs forever"]
-async fn test_extract_info_url() {
-    let err = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://google.com").expect("Is valid."),
-        false,
-        false,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{err:#?}");
-}
-
-#[tokio::test]
-#[ignore = "This test hangs forever"]
-async fn test_extract_info_playlist() {
-    let err = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://www.youtube.com/@TheGarriFrischer/videos").expect("Is valid."),
-        false,
-        true,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{err:#?}");
-}
-#[tokio::test]
-#[ignore = "This test hangs forever"]
-async fn test_extract_info_playlist_full() {
-    let err = crate::extract_info(
-        &YT_OPTS,
-        &Url::parse("https://www.youtube.com/@NixOS-Foundation/videos").expect("Is valid."),
-        false,
-        true,
-    )
-    .await
-    .map_err(|err| format!("Encountered error: '{err}'"))
-    .unwrap();
-
-    println!("{err:#?}");
-}
diff --git a/crates/yt_dlp/src/wrapper/info_json.rs b/crates/yt_dlp/src/wrapper/info_json.rs
deleted file mode 100644
index a2c00df..0000000
--- a/crates/yt_dlp/src/wrapper/info_json.rs
+++ /dev/null
@@ -1,824 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-// `yt_dlp` named them like this.
-#![allow(clippy::pub_underscore_fields)]
-
-use std::{collections::HashMap, path::PathBuf};
-
-use pyo3::{Bound, PyResult, Python, types::PyDict};
-use serde::{Deserialize, Deserializer, Serialize};
-use serde_json::Value;
-use url::Url;
-
-use crate::json_loads_str;
-
-type Todo = String;
-type Extractor = String;
-type ExtractorKey = String;
-
-// TODO: Change this to map `_type` to a structure of values, instead of the options <2024-05-27>
-// And replace all the strings with better types (enums or urls)
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub struct InfoJson {
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub __files_to_move: Option<FilesToMove>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub __last_playlist_index: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub __post_extractor: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub __x_forwarded_for_ip: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub _filename: Option<PathBuf>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub _format_sort_fields: Option<Vec<String>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub _has_drm: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub _type: Option<InfoType>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub _version: Option<Version>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub abr: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub acodec: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub age_limit: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub artists: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub aspect_ratio: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub asr: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub audio_channels: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub audio_ext: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub automatic_captions: Option<HashMap<String, Vec<Caption>>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub availability: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub average_rating: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub categories: Option<Vec<String>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub channel: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub channel_follower_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub channel_id: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub channel_is_verified: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub channel_url: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub chapters: Option<Vec<Chapter>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub comment_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub comments: Option<Vec<Comment>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub concurrent_view_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub container: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub description: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub direct: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub display_id: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub downloader_options: Option<DownloaderOptions>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub duration: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub duration_string: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub dynamic_range: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub entries: Option<Vec<InfoJson>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub episode: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub episode_number: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub epoch: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub ext: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub extractor: Option<Extractor>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub extractor_key: Option<ExtractorKey>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub filename: Option<PathBuf>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub filesize: Option<u64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub filesize_approx: Option<u64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub format: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub format_id: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub format_index: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub format_note: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub formats: Option<Vec<Format>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub fps: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub fulltitle: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub genre: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub genres: Option<Vec<String>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub has_drm: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub heatmap: Option<Vec<HeatMapEntry>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub height: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub hls_aes: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub http_headers: Option<HttpHeader>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub id: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub ie_key: Option<ExtractorKey>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub is_live: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub language: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub language_preference: Option<i32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub license: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub like_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub live_status: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub location: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub manifest_url: Option<Url>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub media_type: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub modified_date: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub n_entries: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub original_url: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playable_in_embed: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_autonumber: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_channel: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_channel_id: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_id: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_index: Option<u64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_title: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_uploader: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_uploader_id: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub playlist_webpage_url: Option<Url>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub preference: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub protocol: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub quality: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub release_date: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub release_timestamp: Option<u64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub release_year: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub repost_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub requested_downloads: Option<Vec<RequestedDownloads>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub requested_entries: Option<Vec<u32>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub requested_formats: Option<Vec<Format>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub requested_subtitles: Option<HashMap<String, Subtitle>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub resolution: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub season: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub season_number: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub series: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub source_preference: Option<i32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub stretched_ratio: Option<Todo>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub subtitles: Option<HashMap<String, Vec<Caption>>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub tags: Option<Vec<String>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub tbr: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub thumbnail: Option<Url>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub thumbnails: Option<Vec<ThumbNail>>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub timestamp: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub title: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub upload_date: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub uploader: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub uploader_id: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub uploader_url: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub url: Option<Url>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub vbr: Option<f64>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub vcodec: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub video_ext: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub view_count: Option<u32>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub was_live: Option<bool>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub webpage_url: Option<Url>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub webpage_url_basename: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub webpage_url_domain: Option<String>,
-
-    #[serde(skip_serializing_if = "Option::is_none")]
-    pub width: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-#[allow(missing_copy_implementations)]
-pub struct FilesToMove {}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub struct RequestedDownloads {
-    pub __files_to_merge: Option<Vec<Todo>>,
-    pub __finaldir: PathBuf,
-    pub __infojson_filename: PathBuf,
-    pub __postprocessors: Vec<Todo>,
-    pub __real_download: bool,
-    pub __write_download_archive: bool,
-    pub _filename: PathBuf,
-    pub _type: InfoType,
-    pub _version: Version,
-    pub abr: f64,
-    pub acodec: String,
-    pub aspect_ratio: Option<f64>,
-    pub asr: Option<u32>,
-    pub audio_channels: Option<u32>,
-    pub audio_ext: Option<String>,
-    pub chapters: Option<Vec<SponsorblockChapter>>,
-    pub duration: Option<f64>,
-    pub dynamic_range: Option<String>,
-    pub ext: String,
-    pub filename: PathBuf,
-    pub filepath: PathBuf,
-    pub filesize_approx: Option<u64>,
-    pub format: String,
-    pub format_id: String,
-    pub format_note: Option<String>,
-    pub fps: Option<f64>,
-    pub has_drm: Option<bool>,
-    pub height: Option<u32>,
-    pub http_headers: Option<HttpHeader>,
-    pub infojson_filename: PathBuf,
-    pub language: Option<String>,
-    pub manifest_url: Option<Url>,
-    pub protocol: String,
-    pub quality: Option<i64>,
-    pub requested_formats: Option<Vec<Format>>,
-    pub resolution: String,
-    pub tbr: f64,
-    pub url: Option<Url>,
-    pub vbr: f64,
-    pub vcodec: String,
-    pub video_ext: Option<String>,
-    pub width: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Subtitle {
-    pub ext: SubtitleExt,
-    pub filepath: PathBuf,
-    pub filesize: Option<u64>,
-    pub fragment_base_url: Option<Url>,
-    pub fragments: Option<Vec<Fragment>>,
-    pub manifest_url: Option<Url>,
-    pub name: Option<String>,
-    pub protocol: Option<Todo>,
-    pub url: Url,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-pub enum SubtitleExt {
-    #[serde(alias = "vtt")]
-    Vtt,
-
-    #[serde(alias = "mp4")]
-    Mp4,
-
-    #[serde(alias = "json")]
-    Json,
-    #[serde(alias = "json3")]
-    Json3,
-
-    #[serde(alias = "ttml")]
-    Ttml,
-
-    #[serde(alias = "srv1")]
-    Srv1,
-    #[serde(alias = "srv2")]
-    Srv2,
-    #[serde(alias = "srv3")]
-    Srv3,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Caption {
-    pub ext: SubtitleExt,
-    pub filepath: Option<PathBuf>,
-    pub filesize: Option<u64>,
-    pub fragments: Option<Vec<SubtitleFragment>>,
-    pub fragment_base_url: Option<Url>,
-    pub manifest_url: Option<Url>,
-    pub name: Option<String>,
-    pub protocol: Option<String>,
-    pub url: String,
-    pub video_id: Option<String>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct SubtitleFragment {
-    path: PathBuf,
-    duration: Option<f64>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Chapter {
-    pub end_time: f64,
-    pub start_time: f64,
-    pub title: String,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub struct SponsorblockChapter {
-    /// This is an utterly useless field, and should thus be ignored
-    pub _categories: Option<Vec<Vec<Value>>>,
-
-    pub categories: Option<Vec<SponsorblockChapterCategory>>,
-    pub category: Option<SponsorblockChapterCategory>,
-    pub category_names: Option<Vec<String>>,
-    pub end_time: f64,
-    pub name: Option<String>,
-    pub r#type: Option<SponsorblockChapterType>,
-    pub start_time: f64,
-    pub title: String,
-}
-
-pub fn get_none<'de, D, T>(_: D) -> Result<Option<T>, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    Ok(None)
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub enum SponsorblockChapterType {
-    #[serde(alias = "skip")]
-    Skip,
-
-    #[serde(alias = "chapter")]
-    Chapter,
-
-    #[serde(alias = "poi")]
-    Poi,
-}
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub enum SponsorblockChapterCategory {
-    #[serde(alias = "filler")]
-    Filler,
-
-    #[serde(alias = "interaction")]
-    Interaction,
-
-    #[serde(alias = "music_offtopic")]
-    MusicOfftopic,
-
-    #[serde(alias = "poi_highlight")]
-    PoiHighlight,
-
-    #[serde(alias = "preview")]
-    Preview,
-
-    #[serde(alias = "sponsor")]
-    Sponsor,
-
-    #[serde(alias = "selfpromo")]
-    SelfPromo,
-
-    #[serde(alias = "chapter")]
-    Chapter,
-
-    #[serde(alias = "intro")]
-    Intro,
-
-    #[serde(alias = "outro")]
-    Outro,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-#[allow(missing_copy_implementations)]
-pub struct HeatMapEntry {
-    pub start_time: f64,
-    pub end_time: f64,
-    pub value: f64,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub enum InfoType {
-    #[serde(alias = "playlist")]
-    #[serde(rename(serialize = "playlist"))]
-    Playlist,
-
-    #[serde(alias = "url")]
-    #[serde(rename(serialize = "url"))]
-    Url,
-
-    #[serde(alias = "video")]
-    #[serde(rename(serialize = "video"))]
-    Video,
-}
-
-#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-pub struct Version {
-    pub current_git_head: Option<String>,
-    pub release_git_head: String,
-    pub repository: String,
-    pub version: String,
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(from = "String")]
-#[serde(deny_unknown_fields)]
-pub enum Parent {
-    Root,
-    Id(String),
-}
-
-impl Parent {
-    #[must_use]
-    pub fn id(&self) -> Option<&str> {
-        if let Self::Id(id) = self {
-            Some(id)
-        } else {
-            None
-        }
-    }
-}
-
-impl From<String> for Parent {
-    fn from(value: String) -> Self {
-        if value == "root" {
-            Self::Root
-        } else {
-            Self::Id(value)
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(from = "String")]
-#[serde(deny_unknown_fields)]
-pub struct Id {
-    pub id: String,
-}
-impl From<String> for Id {
-    fn from(value: String) -> Self {
-        Self {
-            // Take the last element if the string is split with dots, otherwise take the full id
-            id: value.split('.').last().unwrap_or(&value).to_owned(),
-        }
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-#[allow(clippy::struct_excessive_bools)]
-pub struct Comment {
-    pub id: Id,
-    pub text: String,
-    #[serde(default = "zero")]
-    pub like_count: u32,
-    pub is_pinned: bool,
-    pub author_id: String,
-    #[serde(default = "unknown")]
-    pub author: String,
-    pub author_is_verified: bool,
-    pub author_thumbnail: Url,
-    pub parent: Parent,
-    #[serde(deserialize_with = "edited_from_time_text", alias = "_time_text")]
-    pub edited: bool,
-    // Can't also be deserialized, as it's already used in 'edited'
-    // _time_text: String,
-    pub timestamp: i64,
-    pub author_url: Option<Url>,
-    pub author_is_uploader: bool,
-    pub is_favorited: bool,
-}
-fn unknown() -> String {
-    "<Unknown>".to_string()
-}
-fn zero() -> u32 {
-    0
-}
-fn edited_from_time_text<'de, D>(d: D) -> Result<bool, D::Error>
-where
-    D: Deserializer<'de>,
-{
-    let s = String::deserialize(d)?;
-    if s.contains(" (edited)") {
-        Ok(true)
-    } else {
-        Ok(false)
-    }
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, Eq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-pub struct ThumbNail {
-    pub id: Option<String>,
-    pub preference: Option<i32>,
-    /// in the form of "[`height`]x[`width`]"
-    pub resolution: Option<String>,
-    pub url: Url,
-    pub width: Option<u32>,
-    pub height: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Format {
-    pub __needs_testing: Option<bool>,
-    pub __working: Option<bool>,
-    pub abr: Option<f64>,
-    pub acodec: Option<String>,
-    pub aspect_ratio: Option<f64>,
-    pub asr: Option<f64>,
-    pub audio_channels: Option<u32>,
-    pub audio_ext: Option<String>,
-    pub columns: Option<u32>,
-    pub container: Option<String>,
-    pub downloader_options: Option<DownloaderOptions>,
-    pub dynamic_range: Option<String>,
-    pub ext: String,
-    pub filepath: Option<PathBuf>,
-    pub filesize: Option<u64>,
-    pub filesize_approx: Option<u64>,
-    pub format: Option<String>,
-    pub format_id: String,
-    pub format_index: Option<String>,
-    pub format_note: Option<String>,
-    pub fps: Option<f64>,
-    pub fragment_base_url: Option<Todo>,
-    pub fragments: Option<Vec<Fragment>>,
-    pub has_drm: Option<bool>,
-    pub height: Option<u32>,
-    pub http_headers: Option<HttpHeader>,
-    pub is_dash_periods: Option<bool>,
-    pub is_live: Option<bool>,
-    pub language: Option<String>,
-    pub language_preference: Option<i32>,
-    pub manifest_stream_number: Option<u32>,
-    pub manifest_url: Option<Url>,
-    pub preference: Option<i32>,
-    pub protocol: Option<String>,
-    pub quality: Option<f64>,
-    pub resolution: Option<String>,
-    pub rows: Option<u32>,
-    pub source_preference: Option<i32>,
-    pub tbr: Option<f64>,
-    pub url: Url,
-    pub vbr: Option<f64>,
-    pub vcodec: String,
-    pub video_ext: Option<String>,
-    pub width: Option<u32>,
-}
-
-#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-#[allow(missing_copy_implementations)]
-pub struct DownloaderOptions {
-    http_chunk_size: u64,
-}
-
-#[derive(Debug, Deserialize, Serialize, Eq, PartialEq, PartialOrd, Ord)]
-#[serde(deny_unknown_fields)]
-pub struct HttpHeader {
-    #[serde(alias = "User-Agent")]
-    pub user_agent: Option<String>,
-
-    #[serde(alias = "Accept")]
-    pub accept: Option<String>,
-
-    #[serde(alias = "X-Forwarded-For")]
-    pub x_forwarded_for: Option<String>,
-
-    #[serde(alias = "Accept-Language")]
-    pub accept_language: Option<String>,
-
-    #[serde(alias = "Sec-Fetch-Mode")]
-    pub sec_fetch_mode: Option<String>,
-}
-
-#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd)]
-#[serde(deny_unknown_fields)]
-pub struct Fragment {
-    pub duration: Option<f64>,
-    pub fragment_count: Option<usize>,
-    pub path: Option<PathBuf>,
-    pub url: Option<Url>,
-}
-
-impl InfoJson {
-    pub fn to_py_dict(self, py: Python<'_>) -> PyResult<Bound<'_, PyDict>> {
-        let output: Bound<'_, PyDict> = json_loads_str(py, self)?;
-        Ok(output)
-    }
-}
diff --git a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
deleted file mode 100644
index 25595b5..0000000
--- a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
+++ /dev/null
@@ -1,62 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use pyo3::{Bound, PyResult, Python, types::PyDict};
-use serde::Serialize;
-
-use crate::json_loads;
-
-#[derive(Serialize, Clone)]
-pub struct YtDlpOptions {
-    pub playliststart: u32,
-    pub playlistend: u32,
-    pub noplaylist: bool,
-    pub extract_flat: ExtractFlat,
-    // pub extractor_args: ExtractorArgs,
-    // pub format: String,
-    // pub fragment_retries: u32,
-    // #[serde(rename(serialize = "getcomments"))]
-    // pub get_comments: bool,
-    // #[serde(rename(serialize = "ignoreerrors"))]
-    // pub ignore_errors: bool,
-    // pub retries: u32,
-    // #[serde(rename(serialize = "writeinfojson"))]
-    // pub write_info_json: bool,
-    // pub postprocessors: Vec<serde_json::Map<String, serde_json::Value>>,
-}
-
-#[derive(Serialize, Copy, Clone)]
-pub enum ExtractFlat {
-    #[serde(rename(serialize = "in_playlist"))]
-    InPlaylist,
-
-    #[serde(rename(serialize = "discard_in_playlist"))]
-    DiscardInPlaylist,
-}
-
-#[derive(Serialize, Clone)]
-pub struct ExtractorArgs {
-    pub youtube: YoutubeExtractorArgs,
-}
-
-#[derive(Serialize, Clone)]
-pub struct YoutubeExtractorArgs {
-    comment_sort: Vec<String>,
-    max_comments: Vec<String>,
-}
-
-impl YtDlpOptions {
-    pub fn to_py_dict(self, py: Python) -> PyResult<Bound<PyDict>> {
-        let string = serde_json::to_string(&self).expect("This should always work");
-
-        let output: Bound<PyDict> = json_loads(py, string)?;
-        Ok(output)
-    }
-}
diff --git a/crates/yt_dlp/update.sh b/crates/yt_dlp/update.sh
index c1a0215..ab03b62 100755
--- a/crates/yt_dlp/update.sh
+++ b/crates/yt_dlp/update.sh
@@ -10,6 +10,4 @@
 # You should have received a copy of the License along with this program.
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
-cd "$(dirname "$0")" || exit 1
-[ "$1" = "upgrade" ] && cargo upgrade --incompatible
-cargo update
+./crates/pyo3-pylogger/update.sh "$@"
diff --git a/flake.lock b/flake.lock
index d5590d5..8d3d09d 100644
--- a/flake.lock
+++ b/flake.lock
@@ -1,61 +1,27 @@
 {
   "nodes": {
-    "flake-utils": {
-      "inputs": {
-        "systems": "systems"
-      },
-      "locked": {
-        "lastModified": 1731533236,
-        "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
-        "type": "github"
-      },
-      "original": {
-        "owner": "numtide",
-        "repo": "flake-utils",
-        "type": "github"
-      }
-    },
     "nixpkgs": {
       "locked": {
-        "lastModified": 1739866667,
-        "narHash": "sha256-EO1ygNKZlsAC9avfcwHkKGMsmipUk1Uc0TbrEZpkn64=",
+        "lastModified": 1752735392,
+        "narHash": "sha256-6MVfahGRB3quXdoepoHP8AvMDGOxAGzSjF+PyR8qFR0=",
         "owner": "NixOS",
         "repo": "nixpkgs",
-        "rev": "73cf49b8ad837ade2de76f87eb53fc85ed5d4680",
+        "rev": "994618699f294e93b840df6f44b089d6fedb7e4a",
         "type": "github"
       },
       "original": {
         "owner": "NixOS",
-        "ref": "nixos-unstable",
+        "ref": "nixos-unstable-small",
         "repo": "nixpkgs",
         "type": "github"
       }
     },
     "root": {
       "inputs": {
-        "flake-utils": "flake-utils",
         "nixpkgs": "nixpkgs",
         "treefmt-nix": "treefmt-nix"
       }
     },
-    "systems": {
-      "locked": {
-        "lastModified": 1681028828,
-        "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
-        "owner": "nix-systems",
-        "repo": "default",
-        "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
-        "type": "github"
-      },
-      "original": {
-        "owner": "nix-systems",
-        "repo": "default",
-        "type": "github"
-      }
-    },
     "treefmt-nix": {
       "inputs": {
         "nixpkgs": [
@@ -63,11 +29,11 @@
         ]
       },
       "locked": {
-        "lastModified": 1739829690,
-        "narHash": "sha256-mL1szCeIsjh6Khn3nH2cYtwO5YXG6gBiTw1A30iGeDU=",
+        "lastModified": 1752055615,
+        "narHash": "sha256-19m7P4O/Aw/6+CzncWMAJu89JaKeMh3aMle1CNQSIwM=",
         "owner": "numtide",
         "repo": "treefmt-nix",
-        "rev": "3d0579f5cc93436052d94b73925b48973a104204",
+        "rev": "c9d477b5d5bd7f26adddd3f96cfd6a904768d4f9",
         "type": "github"
       },
       "original": {
diff --git a/flake.nix b/flake.nix
index 18a5f62..770105e 100644
--- a/flake.nix
+++ b/flake.nix
@@ -11,9 +11,8 @@
   description = "yt";
 
   inputs = {
-    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
+    nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable-small";
 
-    flake-utils.url = "github:numtide/flake-utils";
     treefmt-nix = {
       url = "github:numtide/treefmt-nix";
       inputs = {
@@ -25,50 +24,53 @@
   outputs = {
     self,
     nixpkgs,
-    flake-utils,
     treefmt-nix,
-  }: (flake-utils.lib.eachDefaultSystem (system: let
+  }: let
+    system = "x86_64-linux";
     pkgs = nixpkgs.legacyPackages."${system}";
 
-    python = pkgs.python3.withPackages (ps: [
-      ps.yt-dlp
-    ]);
-
     buildInputs = with pkgs; [
       mpv-unwrapped.dev
+      libffi
+      openssl
+      zlib
+      curl.dev
     ];
 
     nativeBuildInputs = with pkgs; [
       llvmPackages_latest.clang-unwrapped.lib
+      pkg-config
 
       # Needed for the tests in `libmpv2`
       SDL2
     ];
 
-    yt = pkgs.callPackage ./package/package.nix {inherit tree-sitter-yts;};
+    yt = pkgs.callPackage ./nix/package.nix {inherit tree-sitter-yts;};
     tree-sitter-yts = pkgs.callPackage ./tree-sitter-yts/package.nix {};
 
     treefmtEval = import ./treefmt.nix {inherit treefmt-nix pkgs;};
   in {
-    packages = {
+    packages."${system}" = {
       inherit yt tree-sitter-yts;
       default = self.packages.${system}.yt;
     };
 
-    checks = {
+    checks."${system}" = {
       inherit yt;
       formatting = treefmtEval.config.build.check self;
     };
 
-    formatter = treefmtEval.config.build.wrapper;
+    formatter."${system}" = treefmtEval.config.build.wrapper;
 
-    devShells.default = pkgs.mkShell {
+    devShells."${system}".default = pkgs.mkShell {
       env = let
         clang_version =
           pkgs.lib.versions.major
           pkgs.llvmPackages_latest.clang-unwrapped.version;
       in {
         FFMPEG_LOCATION = "${pkgs.lib.getExe pkgs.ffmpeg}";
+
+        # These are needed for `libmpv` to compile.
         LIBCLANG_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so";
         LIBCLANG_INCLUDE_PATH = "${pkgs.llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include";
         C_INCLUDE_PATH = "${pkgs.glibc.dev}/include";
@@ -89,6 +91,7 @@
         pkgs.cargo-flamegraph
 
         # Releng
+        pkgs.git-bug
         pkgs.reuse
         pkgs.cocogitto
 
@@ -100,7 +103,8 @@
         pkgs.sqlite-interactive
 
         # yt_dlp
-        python
+        pkgs.python3Packages.yt-dlp
+        pkgs.python3Packages.chardet
         pkgs.jq
         pkgs.ffmpeg
 
@@ -109,5 +113,5 @@
         pkgs.tree-sitter
       ];
     };
-  }));
+  };
 }
diff --git a/nix/package.nix b/nix/package.nix
new file mode 100644
index 0000000..979696b
--- /dev/null
+++ b/nix/package.nix
@@ -0,0 +1,138 @@
+# yt - A fully featured command line YouTube client
+#
+# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
+# SPDX-License-Identifier: GPL-3.0-or-later
+#
+# This file is part of Yt.
+#
+# You should have received a copy of the License along with this program.
+# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
+{
+  lib,
+  rustPlatform,
+  installShellFiles,
+  # buildInputs
+  mpv-unwrapped,
+  ffmpeg,
+  openssl,
+  libffi,
+  zlib,
+  curl,
+  # NativeBuildInputs
+  makeWrapper,
+  llvmPackages_latest,
+  glibc,
+  sqlite,
+  fd,
+  pkg-config,
+  SDL2,
+  python3,
+  # Passthru
+  tree-sitter-yts,
+}: let
+  python = python3.withPackages (ps: [ps.yt-dlp]);
+in
+  rustPlatform.buildRustPackage (finalAttrs: {
+    pname = "yt";
+    inherit
+      ((builtins.fromTOML (builtins.readFile
+            ../Cargo.toml)).workspace.package)
+      version
+      ;
+
+    src = lib.cleanSourceWith {
+      src = lib.cleanSource ./..;
+      filter = name: type:
+        (type == "directory")
+        || (builtins.elem (builtins.baseNameOf name) [
+          "Cargo.toml"
+          "Cargo.lock"
+          "mkdb.sh"
+          "help.str"
+          "raw_error_warning.txt"
+          "golden.txt"
+        ])
+        || (lib.strings.hasSuffix ".rs" (builtins.baseNameOf name))
+        || (lib.strings.hasSuffix ".h" (builtins.baseNameOf name))
+        || (lib.strings.hasSuffix ".sql" (builtins.baseNameOf name));
+    };
+
+    nativeBuildInputs = [
+      installShellFiles
+      makeWrapper
+      sqlite
+      fd
+      pkg-config
+    ];
+
+    buildInputs = [
+      mpv-unwrapped.dev
+      ffmpeg
+      openssl
+      libffi
+      zlib
+      curl.dev
+      python
+    ];
+
+    checkInputs = [
+      # Needed for the tests in `libmpv2`
+      SDL2
+    ];
+
+    env = let
+      clang_version =
+        lib.versions.major
+        llvmPackages_latest.clang-unwrapped.version;
+    in {
+      # Needed for the compile time sqlite checks.
+      DATABASE_URL = "sqlite://database.sqlx";
+
+      # Required by yt_dlp
+      FFMPEG_LOCATION = "${lib.getExe ffmpeg}";
+
+      # Tell pyo3 which python to use.
+      PYO3_PYTHON = lib.getExe python;
+
+      # Needed for the libmpv2.
+      C_INCLUDE_PATH = "${glibc.dev}/include";
+      LIBCLANG_INCLUDE_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include";
+      LIBCLANG_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so";
+    };
+
+    doCheck = true;
+    checkFlags = [
+      # All of these tests try to connect to the internet to download test data.
+      "--skip=select::base::test_base"
+      "--skip=select::file::test_file"
+      "--skip=select::options::test_options"
+      "--skip=subscriptions::import_export::test_import_export"
+      "--skip=subscriptions::naming_subscriptions::test_naming_subscriptions"
+      "--skip=videos::downloading::test_downloading"
+    ];
+
+    prePatch = ''
+      # Generate the sqlite db, so that we can run the comp-time sqlite checks.
+      bash ./scripts/mkdb.sh
+    '';
+
+    passthru = {
+      inherit tree-sitter-yts;
+    };
+
+    cargoLock = {
+      lockFile = ../Cargo.lock;
+    };
+
+    postInstall = ''
+      installShellCompletion --cmd yt \
+        --bash <(COMPLETE=bash $out/bin/yt) \
+        --fish <(COMPLETE=fish $out/bin/yt) \
+        --zsh <(COMPLETE=zsh $out/bin/yt)
+
+      # NOTE: We cannot clear the path, because we need access to the $EDITOR. <2025-04-04>
+      wrapProgram $out/bin/yt \
+        --prefix PATH : ${lib.makeBinPath finalAttrs.buildInputs} \
+        --set YTDLP_NO_PLUGINS 1
+    '';
+  })
diff --git a/package/package.nix b/package/package.nix
deleted file mode 100644
index c3bc338..0000000
--- a/package/package.nix
+++ /dev/null
@@ -1,75 +0,0 @@
-# yt - A fully featured command line YouTube client
-#
-# Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-# SPDX-License-Identifier: GPL-3.0-or-later
-#
-# This file is part of Yt.
-#
-# You should have received a copy of the License along with this program.
-# If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-{
-  ffmpeg,
-  glibc,
-  lib,
-  llvmPackages_latest,
-  makeWrapper,
-  mpv-unwrapped,
-  python3,
-  rustPlatform,
-  sqlite,
-  tree-sitter-yts,
-  fd,
-}: let
-  version = "0.1.0";
-
-  src = ./..;
-
-  buildInputs = [
-    (python3.withPackages (ps: [ps.yt-dlp]))
-    mpv-unwrapped.dev
-    ffmpeg
-  ];
-in
-  rustPlatform.buildRustPackage {
-    inherit version src buildInputs;
-    pname = "yt";
-
-    nativeBuildInputs = [
-      makeWrapper
-      sqlite
-      fd
-    ];
-
-    env = let
-      clang_version =
-        lib.versions.major
-        llvmPackages_latest.clang-unwrapped.version;
-    in {
-      FFMPEG_LOCATION = "${lib.getExe ffmpeg}";
-      PYO3_PYTHON = lib.getExe (python3.withPackages (ps: [ps.yt-dlp]));
-
-      C_INCLUDE_PATH = "${glibc.dev}/include";
-      DATABASE_URL = "sqlite://target/database.sqlx";
-      LIBCLANG_INCLUDE_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/clang/${clang_version}/include";
-      LIBCLANG_PATH = "${llvmPackages_latest.clang-unwrapped.lib}/lib/libclang.so";
-    };
-
-    doCheck = false;
-
-    prePatch = ''
-      bash ./scripts/mkdb.sh
-    '';
-
-    passthru = {
-      inherit tree-sitter-yts;
-    };
-
-    cargoLock = {
-      lockFile = ../Cargo.lock;
-    };
-
-    postInstall = ''
-      wrapProgram $out/bin/yt \
-        --prefix PATH : ${lib.makeBinPath buildInputs}
-    '';
-  }
diff --git a/scripts/mkdb.sh b/scripts/mkdb.sh
index 6bcebaf..6674841 100755
--- a/scripts/mkdb.sh
+++ b/scripts/mkdb.sh
@@ -11,14 +11,19 @@
 # If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
 
 root="$(dirname "$0")/.."
-db="$root/target/database.sqlx"
+db="${DATABASE_URL#sqlite://}"
 
 [ -f "$db" ] && rm "$db"
 [ -d "$root/target" ] || mkdir "$root/target"
 
-fd . "$root/yt/src/storage/migrate/sql" | while read -r sql_file; do
+fd . "$root/crates/yt/src/storage/migrate/sql" | while read -r sql_file; do
     echo "Applying sql migration file: $(basename "$sql_file").."
-    sqlite3 "$db" <"$sql_file"
+    {
+        # NOTE(@bpeetz): The wrapping in a transaction is needed to simulate the rust code. <2025-05-07>
+        echo "BEGIN TRANSACTION;"
+        cat "$sql_file"
+        echo "COMMIT TRANSACTION;"
+    } | sqlite3 "$db"
 done
 
 # vim: ft=sh
diff --git a/update.sh b/update.sh
index 6672566..e90b290 100755
--- a/update.sh
+++ b/update.sh
@@ -15,8 +15,6 @@ nix flake update
 [ "$1" = "upgrade" ] && cargo upgrade --incompatible allow --pinned allow --recursive true
 cargo update --recursive
 
-# TODO: Not all of these update are currently needed. <2024-11-04>
 ./crates/yt_dlp/update.sh "$@"
 ./crates/libmpv2/update.sh "$@"
-./crates/bytes/update.sh "$@"
 # vim: ft=sh
diff --git a/yt/src/cache/mod.rs b/yt/src/cache/mod.rs
deleted file mode 100644
index 83d5ee0..0000000
--- a/yt/src/cache/mod.rs
+++ /dev/null
@@ -1,105 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use anyhow::{Context, Result};
-use log::{debug, info};
-use tokio::fs;
-
-use crate::{
-    app::App,
-    storage::video_database::{
-        Video, VideoStatus, VideoStatusMarker, downloader::set_video_cache_path, get,
-    },
-};
-
-async fn invalidate_video(app: &App, video: &Video, hard: bool) -> Result<()> {
-    info!("Invalidating cache of video: '{}'", video.title);
-
-    if hard {
-        if let VideoStatus::Cached {
-            cache_path: path, ..
-        } = &video.status
-        {
-            info!("Removing cached video at: '{}'", path.display());
-            if let Err(err) = fs::remove_file(path).await.map_err(|err| err.kind()) {
-                match err {
-                    std::io::ErrorKind::NotFound => {
-                        // The path is already gone
-                        debug!(
-                            "Not actually removing path: '{}'. It is already gone.",
-                            path.display()
-                        );
-                    }
-                    err => Err(std::io::Error::from(err)).with_context(|| {
-                        format!(
-                            "Failed to delete video ('{}') cache path: '{}'.",
-                            video.title,
-                            path.display()
-                        )
-                    })?,
-                }
-            }
-        }
-    }
-
-    set_video_cache_path(app, &video.extractor_hash, None).await?;
-
-    Ok(())
-}
-
-pub async fn invalidate(app: &App, hard: bool) -> Result<()> {
-    let all_cached_things = get::videos(app, &[VideoStatusMarker::Cached]).await?;
-
-    info!("Got videos to invalidate: '{}'", all_cached_things.len());
-
-    for video in all_cached_things {
-        invalidate_video(app, &video, hard).await?;
-    }
-
-    Ok(())
-}
-
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn maintain(app: &App, all: bool) -> Result<()> {
-    let domain = if all {
-        VideoStatusMarker::ALL.as_slice()
-    } else {
-        &[VideoStatusMarker::Watch, VideoStatusMarker::Cached]
-    };
-
-    let cached_videos = get::videos(app, domain).await?;
-
-    let mut found_focused = 0;
-    for vid in cached_videos {
-        if let VideoStatus::Cached {
-            cache_path: path,
-            is_focused,
-        } = &vid.status
-        {
-            info!("Checking if path ('{}') exists", path.display());
-            if !path.exists() {
-                invalidate_video(app, &vid, false).await?;
-            }
-
-            if *is_focused {
-                found_focused += 1;
-            }
-        }
-    }
-
-    assert!(
-        found_focused <= 1,
-        "Only one video can be focused at a time"
-    );
-
-    Ok(())
-}
diff --git a/yt/src/cli.rs b/yt/src/cli.rs
deleted file mode 100644
index 037f45c..0000000
--- a/yt/src/cli.rs
+++ /dev/null
@@ -1,383 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{path::PathBuf, str::FromStr};
-
-use anyhow::Context;
-use bytes::Bytes;
-use chrono::NaiveDate;
-use clap::{ArgAction, Args, Parser, Subcommand};
-use url::Url;
-
-use crate::{
-    select::selection_file::duration::MaybeDuration,
-    storage::video_database::extractor_hash::LazyExtractorHash,
-};
-
-#[derive(Parser, Debug)]
-#[clap(author, about, long_about = None)]
-#[allow(clippy::module_name_repetitions)]
-/// An command line interface to select, download and watch videos
-pub struct CliArgs {
-    #[command(subcommand)]
-    /// The subcommand to execute [default: select]
-    pub command: Option<Command>,
-
-    /// Show the version and exit
-    #[arg(long, short = 'V', action= ArgAction::SetTrue)]
-    pub version: bool,
-
-    /// Do not perform database migration before starting.
-    /// Setting this could cause runtime database access errors.
-    #[arg(long, short, action=ArgAction::SetTrue, default_value_t = false)]
-    pub no_migrate_db: bool,
-
-    /// Increase message verbosity
-    #[arg(long="verbose", short = 'v', action = ArgAction::Count)]
-    pub verbosity: u8,
-
-    /// Display colors [defaults to true, if the config file has no value]
-    #[arg(long, short = 'C')]
-    pub color: Option<bool>,
-
-    /// Set the path to the videos.db. This overrides the default and the config file.
-    #[arg(long, short)]
-    pub db_path: Option<PathBuf>,
-
-    /// Set the path to the config.toml.
-    /// This overrides the default.
-    #[arg(long, short)]
-    pub config_path: Option<PathBuf>,
-
-    /// Silence all output
-    #[arg(long, short = 'q')]
-    pub quiet: bool,
-}
-
-#[derive(Subcommand, Debug)]
-pub enum Command {
-    /// Download and cache URLs
-    Download {
-        /// Forcefully re-download all cached videos (i.e. delete the cache path, then download).
-        #[arg(short, long)]
-        force: bool,
-
-        /// The maximum size the download dir should have. Beware that the value must be given in
-        /// bytes.
-        #[arg(short, long, value_parser = byte_parser)]
-        max_cache_size: Option<u64>,
-    },
-
-    /// Select, download and watch in one command.
-    Sedowa {},
-    /// Download and watch in one command.
-    Dowa {},
-
-    /// Work with single videos
-    Videos {
-        #[command(subcommand)]
-        cmd: VideosCommand,
-    },
-
-    /// Watch the already cached (and selected) videos
-    Watch {},
-
-    /// Visualize the current playlist
-    Playlist {
-        /// Linger and display changes
-        #[arg(short, long)]
-        watch: bool,
-    },
-
-    /// Show, which videos have been selected to be watched (and their cache status)
-    Status {},
-
-    /// Show, the configuration options in effect
-    Config {},
-
-    /// Perform various tests
-    Check {
-        #[command(subcommand)]
-        command: CheckCommand,
-    },
-
-    /// Display the comments of the currently playing video
-    Comments {},
-    /// Display the description of the currently playing video
-    Description {},
-
-    /// Manipulate the video cache in the database
-    #[command(visible_alias = "db")]
-    Database {
-        #[command(subcommand)]
-        command: CacheCommand,
-    },
-
-    /// Change the state of videos in the database (the default)
-    Select {
-        #[command(subcommand)]
-        cmd: Option<SelectCommand>,
-    },
-
-    /// Update the video database
-    Update {
-        #[arg(short, long)]
-        /// The number of videos to updating
-        max_backlog: Option<usize>,
-
-        #[arg(short, long)]
-        /// The subscriptions to update (can be given multiple times)
-        subscriptions: Vec<String>,
-    },
-
-    /// Manipulate subscription
-    #[command(visible_alias = "subs")]
-    Subscriptions {
-        #[command(subcommand)]
-        cmd: SubscriptionCommand,
-    },
-}
-
-fn byte_parser(input: &str) -> Result<u64, anyhow::Error> {
-    Ok(input
-        .parse::<Bytes>()
-        .with_context(|| format!("Failed to parse '{input}' as bytes!"))?
-        .as_u64())
-}
-
-impl Default for Command {
-    fn default() -> Self {
-        Self::Select {
-            cmd: Some(SelectCommand::default()),
-        }
-    }
-}
-
-#[derive(Subcommand, Clone, Debug)]
-pub enum VideosCommand {
-    /// List the videos in the database
-    #[command(visible_alias = "ls")]
-    List {
-        /// An optional search query to limit the results
-        #[arg(action = ArgAction::Append)]
-        search_query: Option<String>,
-
-        /// The number of videos to show
-        #[arg(short, long)]
-        limit: Option<usize>,
-    },
-
-    /// Get detailed information about a video
-    Info {
-        /// The short hash of the video
-        hash: LazyExtractorHash,
-    },
-}
-
-#[derive(Subcommand, Clone, Debug)]
-pub enum SubscriptionCommand {
-    /// Subscribe to an URL
-    Add {
-        #[arg(short, long)]
-        /// The human readable name of the subscription
-        name: Option<String>,
-
-        /// The URL to listen to
-        url: Url,
-    },
-
-    /// Unsubscribe from an URL
-    Remove {
-        /// The human readable name of the subscription
-        name: String,
-    },
-
-    /// Import a bunch of URLs as subscriptions.
-    Import {
-        /// The file containing the URLs. Will use Stdin otherwise.
-        file: Option<PathBuf>,
-
-        /// Remove any previous subscriptions
-        #[arg(short, long)]
-        force: bool,
-    },
-    /// Write all subscriptions in an format understood by `import`
-    Export {},
-
-    /// List all subscriptions
-    List {},
-}
-
-#[derive(Clone, Debug, Args)]
-#[command(infer_subcommands = true)]
-/// Mark the video given by the hash to be watched
-pub struct SharedSelectionCommandArgs {
-    /// The ordering priority (higher means more at the top)
-    #[arg(short, long)]
-    pub priority: Option<i64>,
-
-    /// The subtitles to download (e.g. 'en,de,sv')
-    #[arg(short = 'l', long)]
-    pub subtitle_langs: Option<String>,
-
-    /// The speed to set mpv to
-    #[arg(short, long)]
-    pub speed: Option<f64>,
-
-    /// The short extractor hash
-    pub hash: LazyExtractorHash,
-
-    pub title: Option<String>,
-
-    pub date: Option<OptionalNaiveDate>,
-
-    pub publisher: Option<OptionalPublisher>,
-
-    pub duration: Option<MaybeDuration>,
-
-    pub url: Option<Url>,
-}
-#[derive(Clone, Debug, Copy)]
-pub struct OptionalNaiveDate {
-    pub date: Option<NaiveDate>,
-}
-impl FromStr for OptionalNaiveDate {
-    type Err = anyhow::Error;
-    fn from_str(v: &str) -> Result<Self, Self::Err> {
-        if v == "[No release date]" {
-            Ok(Self { date: None })
-        } else {
-            Ok(Self {
-                date: Some(NaiveDate::from_str(v)?),
-            })
-        }
-    }
-}
-#[derive(Clone, Debug)]
-pub struct OptionalPublisher {
-    pub publisher: Option<String>,
-}
-impl FromStr for OptionalPublisher {
-    type Err = anyhow::Error;
-    fn from_str(v: &str) -> Result<Self, Self::Err> {
-        if v == "[No author]" {
-            Ok(Self { publisher: None })
-        } else {
-            Ok(Self {
-                publisher: Some(v.to_owned()),
-            })
-        }
-    }
-}
-
-#[derive(Subcommand, Clone, Debug)]
-// NOTE: Keep this in sync with the [`constants::HELP_STR`] constant. <2024-08-20>
-// NOTE: Also keep this in sync with the `tree-sitter-yts/grammar.js`. <2024-11-04>
-pub enum SelectCommand {
-    /// Open a `git rebase` like file to select the videos to watch (the default)
-    File {
-        /// Include done (watched, dropped) videos
-        #[arg(long, short)]
-        done: bool,
-
-        /// Use the last selection file (useful if you've spend time on it and want to get it again)
-        #[arg(long, short, conflicts_with = "done")]
-        use_last_selection: bool,
-    },
-
-    /// Add a video to the database
-    ///
-    /// This optionally supports to add a playlist.
-    /// When a playlist is added, the `start` and `stop` arguments can be used to select which
-    /// playlist entries to include.
-    #[command(visible_alias = "a")]
-    Add {
-        urls: Vec<Url>,
-
-        /// Start adding playlist entries at this playlist index (zero based and inclusive)
-        #[arg(short = 's', long)]
-        start: Option<usize>,
-
-        /// Stop adding playlist entries at this playlist index (zero based and inclusive)
-        #[arg(short = 'e', long)]
-        stop: Option<usize>,
-    },
-
-    /// Mark the video given by the hash to be watched
-    #[command(visible_alias = "w")]
-    Watch {
-        #[command(flatten)]
-        shared: SharedSelectionCommandArgs,
-    },
-
-    /// Mark the video given by the hash to be dropped
-    #[command(visible_alias = "d")]
-    Drop {
-        #[command(flatten)]
-        shared: SharedSelectionCommandArgs,
-    },
-
-    /// Mark the video given by the hash as already watched
-    #[command(visible_alias = "wd")]
-    Watched {
-        #[command(flatten)]
-        shared: SharedSelectionCommandArgs,
-    },
-
-    /// Open the video URL in Firefox's `timesinks.youtube` profile
-    #[command(visible_alias = "u")]
-    Url {
-        #[command(flatten)]
-        shared: SharedSelectionCommandArgs,
-    },
-
-    /// Reset the videos status to 'Pick'
-    #[command(visible_alias = "p")]
-    Pick {
-        #[command(flatten)]
-        shared: SharedSelectionCommandArgs,
-    },
-}
-impl Default for SelectCommand {
-    fn default() -> Self {
-        Self::File {
-            done: false,
-            use_last_selection: false,
-        }
-    }
-}
-
-#[derive(Subcommand, Clone, Debug)]
-pub enum CheckCommand {
-    /// Check if the given `*.info.json` file is deserializable.
-    InfoJson { path: PathBuf },
-}
-
-#[derive(Subcommand, Clone, Copy, Debug)]
-pub enum CacheCommand {
-    /// Invalidate all cache entries
-    Invalidate {
-        /// Also delete the cache path
-        #[arg(short, long)]
-        hard: bool,
-    },
-
-    /// Perform basic maintenance operations on the database.
-    /// This helps recovering from invalid db states after a crash (or force exit via <CTRL-C>).
-    ///
-    /// 1. Check every path for validity (removing all invalid cache entries)
-    #[command(verbatim_doc_comment)]
-    Maintain {
-        /// Check every video (otherwise only the videos to be watched are checked)
-        #[arg(short, long)]
-        all: bool,
-    },
-}
diff --git a/yt/src/comments/comment.rs b/yt/src/comments/comment.rs
deleted file mode 100644
index 6b8cf73..0000000
--- a/yt/src/comments/comment.rs
+++ /dev/null
@@ -1,65 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use yt_dlp::wrapper::info_json::Comment;
-
-#[derive(Debug, Clone)]
-#[allow(clippy::module_name_repetitions)]
-pub struct CommentExt {
-    pub value: Comment,
-    pub replies: Vec<CommentExt>,
-}
-
-#[derive(Debug, Default)]
-pub struct Comments {
-    pub(super) vec: Vec<CommentExt>,
-}
-
-impl Comments {
-    pub fn new() -> Self {
-        Self::default()
-    }
-    pub fn push(&mut self, value: CommentExt) {
-        self.vec.push(value);
-    }
-    pub fn get_mut(&mut self, key: &str) -> Option<&mut CommentExt> {
-        self.vec.iter_mut().filter(|c| c.value.id.id == key).last()
-    }
-    pub fn insert(&mut self, key: &str, value: CommentExt) {
-        let parent = self
-            .vec
-            .iter_mut()
-            .filter(|c| c.value.id.id == key)
-            .last()
-            .expect("One of these should exist");
-        parent.push_reply(value);
-    }
-}
-impl CommentExt {
-    pub fn push_reply(&mut self, value: CommentExt) {
-        self.replies.push(value);
-    }
-    pub fn get_mut_reply(&mut self, key: &str) -> Option<&mut CommentExt> {
-        self.replies
-            .iter_mut()
-            .filter(|c| c.value.id.id == key)
-            .last()
-    }
-}
-
-impl From<Comment> for CommentExt {
-    fn from(value: Comment) -> Self {
-        Self {
-            replies: vec![],
-            value,
-        }
-    }
-}
diff --git a/yt/src/comments/description.rs b/yt/src/comments/description.rs
deleted file mode 100644
index d22a40f..0000000
--- a/yt/src/comments/description.rs
+++ /dev/null
@@ -1,44 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use crate::{
-    App,
-    comments::output::display_fmt_and_less,
-    storage::video_database::{Video, get},
-    unreachable::Unreachable,
-};
-
-use anyhow::{Result, bail};
-use yt_dlp::wrapper::info_json::InfoJson;
-
-pub async fn description(app: &App) -> Result<()> {
-    let description = get(app).await?;
-    display_fmt_and_less(description).await?;
-
-    Ok(())
-}
-
-pub async fn get(app: &App) -> Result<String> {
-    let currently_playing_video: Video =
-        if let Some(video) = get::currently_focused_video(app).await? {
-            video
-        } else {
-            bail!("Could not find a currently playing video!");
-        };
-
-    let info_json: InfoJson = get::video_info_json(&currently_playing_video)?.unreachable(
-        "A currently *playing* must be cached. And thus the info.json should be available",
-    );
-
-    Ok(info_json
-        .description
-        .unwrap_or("<No description>".to_owned()))
-}
diff --git a/yt/src/comments/mod.rs b/yt/src/comments/mod.rs
deleted file mode 100644
index daecf8d..0000000
--- a/yt/src/comments/mod.rs
+++ /dev/null
@@ -1,165 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::mem;
-
-use anyhow::{Context, Result, bail};
-use comment::{CommentExt, Comments};
-use output::display_fmt_and_less;
-use regex::Regex;
-use yt_dlp::wrapper::info_json::{Comment, InfoJson, Parent};
-
-use crate::{
-    app::App,
-    storage::video_database::{Video, get},
-    unreachable::Unreachable,
-};
-
-mod comment;
-mod display;
-pub mod output;
-
-pub mod description;
-pub use description::*;
-
-#[allow(clippy::too_many_lines)]
-pub async fn get(app: &App) -> Result<Comments> {
-    let currently_playing_video: Video =
-        if let Some(video) = get::currently_focused_video(app).await? {
-            video
-        } else {
-            bail!("Could not find a currently playing video!");
-        };
-
-    let mut info_json: InfoJson = get::video_info_json(&currently_playing_video)?.unreachable(
-        "A currently *playing* must be cached. And thus the info.json should be available",
-    );
-
-    let base_comments = mem::take(&mut info_json.comments).with_context(|| {
-        format!(
-            "The video ('{}') does not have comments!",
-            info_json
-                .title
-                .as_ref()
-                .unwrap_or(&("<No Title>".to_owned()))
-        )
-    })?;
-    drop(info_json);
-
-    let mut comments = Comments::new();
-    for c in base_comments {
-        if let Parent::Id(id) = &c.parent {
-            comments.insert(&(id.clone()), CommentExt::from(c));
-        } else {
-            comments.push(CommentExt::from(c));
-        }
-    }
-
-    comments.vec.iter_mut().for_each(|comment| {
-       let replies = mem::take(&mut comment.replies);
-       let mut output_replies: Vec<CommentExt>  = vec![];
-
-       let re = Regex::new(r"\u{200b}?(@[^\t\s]+)\u{200b}?").unreachable("This is hardcoded");
-       for reply in replies {
-           if let Some(replyee_match) =  re.captures(&reply.value.text){
-               let full_match = replyee_match.get(0).unreachable("This will always exist");
-               let text = reply.
-                   value.
-                   text[0..full_match.start()]
-                   .to_owned()
-                   +
-                   &reply
-                   .value
-                   .text[full_match.end()..];
-               let text: &str = text.trim().trim_matches('\u{200b}');
-
-               let replyee = replyee_match.get(1).unreachable("This should also exist").as_str();
-
-
-               if let Some(parent) = output_replies
-                   .iter_mut()
-                   // .rev()
-                   .flat_map(|com| &mut com.replies)
-                   .flat_map(|com| &mut com.replies)
-                   .flat_map(|com| &mut com.replies)
-                   .filter(|com| com.value.author == replyee)
-                   .last()
-               {
-                   parent.replies.push(CommentExt::from(Comment {
-                       text: text.to_owned(),
-                       ..reply.value
-                   }));
-               } else if let Some(parent) = output_replies
-                   .iter_mut()
-                   // .rev()
-                   .flat_map(|com| &mut com.replies)
-                   .flat_map(|com| &mut com.replies)
-                   .filter(|com| com.value.author == replyee)
-                   .last()
-               {
-                   parent.replies.push(CommentExt::from(Comment {
-                       text: text.to_owned(),
-                       ..reply.value
-                   }));
-               } else if let Some(parent) = output_replies
-                   .iter_mut()
-                   // .rev()
-                   .flat_map(|com| &mut com.replies)
-                   .filter(|com| com.value.author == replyee)
-                   .last()
-               {
-                   parent.replies.push(CommentExt::from(Comment {
-                       text: text.to_owned(),
-                       ..reply.value
-                   }));
-               } else if let Some(parent) = output_replies.iter_mut()
-                   // .rev()
-                   .filter(|com| com.value.author == replyee)
-                   .last()
-               {
-                   parent.replies.push(CommentExt::from(Comment {
-                       text: text.to_owned(),
-                       ..reply.value
-                   }));
-               } else {
-                   eprintln!(
-                   "Failed to find a parent for ('{}') both directly and via replies! The reply text was:\n'{}'\n",
-                   replyee,
-                   reply.value.text
-               );
-                   output_replies.push(reply);
-               }
-           } else {
-               output_replies.push(reply);
-           }
-       }
-       comment.replies = output_replies;
-    });
-
-    Ok(comments)
-}
-
-pub async fn comments(app: &App) -> Result<()> {
-    let comments = get(app).await?;
-
-    display_fmt_and_less(comments.render(true)).await?;
-
-    Ok(())
-}
-
-#[cfg(test)]
-mod test {
-    #[test]
-    fn test_string_replacement() {
-        let s = "A \n\nB\n\nC".to_owned();
-        assert_eq!("A \n  \n  B\n  \n  C", s.replace('\n', "\n  "));
-    }
-}
diff --git a/yt/src/config/default.rs b/yt/src/config/default.rs
deleted file mode 100644
index a1d327a..0000000
--- a/yt/src/config/default.rs
+++ /dev/null
@@ -1,110 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::PathBuf;
-
-use anyhow::{Context, Result};
-
-fn get_runtime_path(name: &'static str) -> Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
-    xdg_dirs
-        .place_runtime_file(name)
-        .with_context(|| format!("Failed to place runtime file: '{name}'"))
-}
-fn get_data_path(name: &'static str) -> Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
-    xdg_dirs
-        .place_data_file(name)
-        .with_context(|| format!("Failed to place data file: '{name}'"))
-}
-fn get_config_path(name: &'static str) -> Result<PathBuf> {
-    let xdg_dirs = xdg::BaseDirectories::with_prefix(PREFIX)?;
-    xdg_dirs
-        .place_config_file(name)
-        .with_context(|| format!("Failed to place config file: '{name}'"))
-}
-
-pub(super) fn create_path(path: PathBuf) -> Result<PathBuf> {
-    if !path.exists() {
-        if let Some(parent) = path.parent() {
-            std::fs::create_dir_all(parent)
-                .with_context(|| format!("Failed to create the '{}' directory", path.display()))?;
-        }
-    }
-
-    Ok(path)
-}
-
-pub(crate) const PREFIX: &str = "yt";
-
-pub(crate) mod global {
-    pub(crate) fn display_colors() -> bool {
-        // TODO: This should probably check if the output is a tty and otherwise return `false` <2025-02-14>
-        true
-    }
-}
-
-pub(crate) mod select {
-    pub(crate) fn playback_speed() -> f64 {
-        2.7
-    }
-    pub(crate) fn subtitle_langs() -> &'static str {
-        ""
-    }
-}
-
-pub(crate) mod watch {
-    pub(crate) fn local_displays_length() -> usize {
-        1000
-    }
-}
-
-pub(crate) mod update {
-    pub(crate) fn max_backlog() -> usize {
-        20
-    }
-}
-
-pub(crate) mod paths {
-    use std::{env::temp_dir, path::PathBuf};
-
-    use anyhow::Result;
-
-    use super::{PREFIX, create_path, get_config_path, get_data_path, get_runtime_path};
-
-    // We download to the temp dir to avoid taxing the disk
-    pub(crate) fn download_dir() -> Result<PathBuf> {
-        let temp_dir = temp_dir();
-
-        create_path(temp_dir.join(PREFIX))
-    }
-    pub(crate) fn mpv_config_path() -> Result<PathBuf> {
-        get_config_path("mpv.conf")
-    }
-    pub(crate) fn mpv_input_path() -> Result<PathBuf> {
-        get_config_path("mpv.input.conf")
-    }
-    pub(crate) fn database_path() -> Result<PathBuf> {
-        get_data_path("videos.sqlite")
-    }
-    pub(crate) fn config_path() -> Result<PathBuf> {
-        get_config_path("config.toml")
-    }
-    pub(crate) fn last_selection_path() -> Result<PathBuf> {
-        get_runtime_path("selected.yts")
-    }
-}
-
-pub(crate) mod download {
-    pub(crate) fn max_cache_size() -> &'static str {
-        "3 GiB"
-    }
-}
diff --git a/yt/src/config/definitions.rs b/yt/src/config/definitions.rs
deleted file mode 100644
index ce8c0d4..0000000
--- a/yt/src/config/definitions.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::PathBuf;
-
-use serde::Deserialize;
-
-#[derive(Debug, Deserialize, PartialEq)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct ConfigFile {
-    pub global: Option<GlobalConfig>,
-    pub select: Option<SelectConfig>,
-    pub watch: Option<WatchConfig>,
-    pub paths: Option<PathsConfig>,
-    pub download: Option<DownloadConfig>,
-    pub update: Option<UpdateConfig>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct GlobalConfig {
-    pub display_colors: Option<bool>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct UpdateConfig {
-    pub max_backlog: Option<usize>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct DownloadConfig {
-    /// This will then be converted to an u64
-    pub max_cache_size: Option<String>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct SelectConfig {
-    pub playback_speed: Option<f64>,
-    pub subtitle_langs: Option<String>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone, Copy)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct WatchConfig {
-    pub local_displays_length: Option<usize>,
-}
-
-#[derive(Debug, Deserialize, PartialEq, Clone)]
-#[serde(deny_unknown_fields)]
-pub(crate) struct PathsConfig {
-    pub download_dir: Option<PathBuf>,
-    pub mpv_config_path: Option<PathBuf>,
-    pub mpv_input_path: Option<PathBuf>,
-    pub database_path: Option<PathBuf>,
-    pub last_selection_path: Option<PathBuf>,
-}
diff --git a/yt/src/config/file_system.rs b/yt/src/config/file_system.rs
deleted file mode 100644
index 2463e9d..0000000
--- a/yt/src/config/file_system.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use crate::config::{DownloadConfig, PathsConfig, SelectConfig, WatchConfig};
-
-use super::{
-    Config, GlobalConfig, UpdateConfig,
-    default::{create_path, download, global, paths, select, update, watch},
-};
-
-use std::{fs::read_to_string, path::PathBuf};
-
-use anyhow::{Context, Result};
-use bytes::Bytes;
-
-macro_rules! get {
-    ($default:path, $config:expr, $key_one:ident, $($keys:ident),*) => {
-        {
-            let maybe_value = get!{@option $config, $key_one, $($keys),*};
-            if let Some(value) = maybe_value {
-                value
-            } else {
-                $default().to_owned()
-            }
-        }
-    };
-
-    (@option $config:expr, $key_one:ident, $($keys:ident),*) => {
-        if let Some(key) = $config.$key_one.clone() {
-            get!{@option key, $($keys),*}
-        } else {
-            None
-        }
-    };
-    (@option $config:expr, $key_one:ident) => {
-        $config.$key_one
-    };
-
-    (@path_if_none $config:expr, $option_default:expr, $default:path, $key_one:ident, $($keys:ident),*) => {
-        {
-            let maybe_download_dir: Option<PathBuf> =
-                get! {@option $config, $key_one, $($keys),*};
-
-            let down_dir = if let Some(dir) = maybe_download_dir {
-                PathBuf::from(dir)
-            } else {
-                if let Some(path) = $option_default {
-                    path
-                } else {
-                    $default()
-                        .with_context(|| format!("Failed to get default path for: '{}.{}'", stringify!($key_one), stringify!($($keys),*)))?
-                }
-            };
-            create_path(down_dir)?
-        }
-    };
-    (@path $config:expr, $default:path, $key_one:ident, $($keys:ident),*) => {
-        get! {@path_if_none $config, None, $default, $key_one, $($keys),*}
-    };
-}
-
-impl Config {
-    pub fn from_config_file(
-        db_path: Option<PathBuf>,
-        config_path: Option<PathBuf>,
-        display_colors: Option<bool>,
-    ) -> Result<Self> {
-        let config_file_path =
-            config_path.map_or_else(|| -> Result<_> { paths::config_path() }, Ok)?;
-
-        let config: super::definitions::ConfigFile =
-            toml::from_str(&read_to_string(config_file_path).unwrap_or(String::new()))
-                .context("Failed to parse the config file as toml")?;
-
-        Ok(Self {
-            global: GlobalConfig {
-                display_colors: {
-                    let config_value: Option<bool> = get! {@option config, global, display_colors};
-
-                    display_colors.unwrap_or(config_value.unwrap_or_else(global::display_colors))
-                },
-            },
-            select: SelectConfig {
-                playback_speed: get! {select::playback_speed, config, select, playback_speed},
-                subtitle_langs: get! {select::subtitle_langs, config, select, subtitle_langs},
-            },
-            watch: WatchConfig {
-                local_displays_length: get! {watch::local_displays_length, config, watch, local_displays_length},
-            },
-            update: UpdateConfig {
-                max_backlog: get! {update::max_backlog, config, update, max_backlog},
-            },
-            paths: PathsConfig {
-                download_dir: get! {@path config, paths::download_dir, paths, download_dir},
-                mpv_config_path: get! {@path config, paths::mpv_config_path, paths, mpv_config_path},
-                mpv_input_path: get! {@path config, paths::mpv_input_path, paths, mpv_input_path},
-                database_path: get! {@path_if_none config, db_path, paths::database_path, paths, database_path},
-                last_selection_path: get! {@path config, paths::last_selection_path, paths, last_selection_path},
-            },
-            download: DownloadConfig {
-                max_cache_size: {
-                    let bytes_str: String =
-                        get! {download::max_cache_size, config, download, max_cache_size};
-                    let number: Bytes = bytes_str
-                        .parse()
-                        .context("Failed to parse max_cache_size")?;
-                    number
-                },
-            },
-        })
-    }
-}
diff --git a/yt/src/config/mod.rs b/yt/src/config/mod.rs
deleted file mode 100644
index a10f7c2..0000000
--- a/yt/src/config/mod.rs
+++ /dev/null
@@ -1,76 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-#![allow(clippy::module_name_repetitions)]
-
-use std::path::PathBuf;
-
-use bytes::Bytes;
-use serde::Serialize;
-
-mod default;
-mod definitions;
-pub mod file_system;
-
-#[derive(Serialize, Debug)]
-pub struct Config {
-    pub global: GlobalConfig,
-    pub select: SelectConfig,
-    pub watch: WatchConfig,
-    pub paths: PathsConfig,
-    pub download: DownloadConfig,
-    pub update: UpdateConfig,
-}
-// These structures could get non-copy fields in the future.
-
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct GlobalConfig {
-    pub display_colors: bool,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct UpdateConfig {
-    pub max_backlog: usize,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct DownloadConfig {
-    pub max_cache_size: Bytes,
-}
-#[derive(Serialize, Debug)]
-pub struct SelectConfig {
-    pub playback_speed: f64,
-    pub subtitle_langs: String,
-}
-#[derive(Serialize, Debug)]
-#[allow(missing_copy_implementations)]
-pub struct WatchConfig {
-    pub local_displays_length: usize,
-}
-#[derive(Serialize, Debug)]
-pub struct PathsConfig {
-    pub download_dir: PathBuf,
-    pub mpv_config_path: PathBuf,
-    pub mpv_input_path: PathBuf,
-    pub database_path: PathBuf,
-    pub last_selection_path: PathBuf,
-}
-
-// pub fn status_path() -> anyhow::Result<PathBuf> {
-//     const STATUS_PATH: &str = "running.info.json";
-//     get_runtime_path(STATUS_PATH)
-// }
-
-// pub fn subscriptions() -> anyhow::Result<PathBuf> {
-//     const SUBSCRIPTIONS: &str = "subscriptions.json";
-//     get_data_path(SUBSCRIPTIONS)
-// }
diff --git a/yt/src/download/download_options.rs b/yt/src/download/download_options.rs
deleted file mode 100644
index 8f5a609..0000000
--- a/yt/src/download/download_options.rs
+++ /dev/null
@@ -1,113 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use serde_json::{Value, json};
-
-use crate::{app::App, storage::video_database::YtDlpOptions};
-
-#[must_use]
-pub fn download_opts(app: &App, additional_opts: &YtDlpOptions) -> serde_json::Map<String, Value> {
-    match json!({
-      "extract_flat": "in_playlist",
-      "extractor_args": {
-        "youtube": {
-          "comment_sort": [
-            "top"
-          ],
-          "max_comments": [
-            "150",
-            "all",
-            "100"
-          ]
-        }
-      },
-
-      "prefer_free_formats": true,
-      "ffmpeg_location": env!("FFMPEG_LOCATION"),
-      "format": "bestvideo[height<=?1080]+bestaudio/best",
-      "fragment_retries": 10,
-      "getcomments": true,
-      "ignoreerrors": false,
-      "retries": 10,
-
-      "writeinfojson": true,
-      // NOTE: This results in a constant warning message.  <2025-01-04>
-      // "writeannotations": true,
-      "writesubtitles": true,
-      "writeautomaticsub": true,
-
-      "outtmpl": {
-        "default": app.config.paths.download_dir.join("%(channel)s/%(title)s.%(ext)s"),
-        "chapter": "%(title)s - %(section_number)03d %(section_title)s [%(id)s].%(ext)s"
-      },
-      "compat_opts": {},
-      "forceprint": {},
-      "print_to_file": {},
-      "windowsfilenames": false,
-      "restrictfilenames": false,
-      "trim_file_names": false,
-      "postprocessors": [
-        {
-          "api": "https://sponsor.ajay.app",
-          "categories": [
-            "interaction",
-            "intro",
-            "music_offtopic",
-            "sponsor",
-            "outro",
-            "poi_highlight",
-            "preview",
-            "selfpromo",
-            "filler",
-            "chapter"
-          ],
-          "key": "SponsorBlock",
-          "when": "after_filter"
-        },
-        {
-          "force_keyframes": false,
-          "key": "ModifyChapters",
-          "remove_chapters_patterns": [],
-          "remove_ranges": [],
-          "remove_sponsor_segments": [
-            "sponsor"
-          ],
-          "sponsorblock_chapter_title": "[SponsorBlock]: %(category_names)l"
-        },
-        {
-          "add_chapters": true,
-          "add_infojson": null,
-          "add_metadata": false,
-          "key": "FFmpegMetadata"
-        },
-        {
-          "key": "FFmpegConcat",
-          "only_multi_video": true,
-          "when": "playlist"
-        }
-      ]
-    }) {
-        Value::Object(mut obj) => {
-            obj.insert(
-                "subtitleslangs".to_owned(),
-                Value::Array(
-                    additional_opts
-                        .subtitle_langs
-                        .split(',')
-                        .map(|val| Value::String(val.to_owned()))
-                        .collect::<Vec<_>>(),
-                ),
-            );
-            obj
-        }
-        _ => unreachable!("This is an object"),
-    }
-}
diff --git a/yt/src/download/mod.rs b/yt/src/download/mod.rs
deleted file mode 100644
index 984d400..0000000
--- a/yt/src/download/mod.rs
+++ /dev/null
@@ -1,363 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{collections::HashMap, io, str::FromStr, sync::Arc, time::Duration};
-
-use crate::{
-    app::App,
-    download::download_options::download_opts,
-    storage::video_database::{
-        Video, YtDlpOptions,
-        downloader::{get_next_uncached_video, set_video_cache_path},
-        extractor_hash::ExtractorHash,
-        get::get_video_yt_dlp_opts,
-        notify::wait_for_cache_reduction,
-    },
-    unreachable::Unreachable,
-};
-
-use anyhow::{Context, Result, bail};
-use bytes::Bytes;
-use futures::{FutureExt, future::BoxFuture};
-use log::{debug, error, info, warn};
-use tokio::{fs, task::JoinHandle, time};
-
-#[allow(clippy::module_name_repetitions)]
-pub mod download_options;
-
-#[derive(Debug)]
-#[allow(clippy::module_name_repetitions)]
-pub struct CurrentDownload {
-    task_handle: JoinHandle<Result<()>>,
-    extractor_hash: ExtractorHash,
-}
-
-impl CurrentDownload {
-    fn new_from_video(app: Arc<App>, video: Video) -> Self {
-        let extractor_hash = video.extractor_hash;
-
-        let task_handle = tokio::spawn(async move {
-            Downloader::actually_cache_video(&app, &video)
-                .await
-                .with_context(|| format!("Failed to cache video: '{}'", video.title))?;
-            Ok(())
-        });
-
-        Self {
-            task_handle,
-            extractor_hash,
-        }
-    }
-}
-
-enum CacheSizeCheck {
-    /// The video can be downloaded
-    Fits,
-
-    /// The video and the current cache size together would exceed the size
-    TooLarge,
-
-    /// The video would not even fit into the empty cache
-    ExceedsMaxCacheSize,
-}
-
-#[derive(Debug)]
-pub struct Downloader {
-    current_download: Option<CurrentDownload>,
-    video_size_cache: HashMap<ExtractorHash, u64>,
-    printed_warning: bool,
-    cached_cache_allocation: Option<Bytes>,
-}
-
-impl Default for Downloader {
-    fn default() -> Self {
-        Self::new()
-    }
-}
-
-impl Downloader {
-    #[must_use]
-    pub fn new() -> Self {
-        Self {
-            current_download: None,
-            video_size_cache: HashMap::new(),
-            printed_warning: false,
-            cached_cache_allocation: None,
-        }
-    }
-
-    /// Check if enough cache is available. Will wait for 10s if it's not.
-    async fn is_enough_cache_available(
-        &mut self,
-        app: &App,
-        max_cache_size: u64,
-        next_video: &Video,
-    ) -> Result<CacheSizeCheck> {
-        if let Some(cdownload) = &self.current_download {
-            if cdownload.extractor_hash == next_video.extractor_hash {
-                // If the video is already being downloaded it will always fit. Otherwise the
-                // download would not have been started.
-                return Ok(CacheSizeCheck::Fits);
-            }
-        }
-        let cache_allocation = Self::get_current_cache_allocation(app).await?;
-        let video_size = self.get_approx_video_size(app, next_video).await?;
-
-        if video_size >= max_cache_size {
-            error!(
-                "The video '{}' ({}) exceeds the maximum cache size ({})! \
-                 Please set a bigger maximum (`--max-cache-size`) or skip it.",
-                next_video.title,
-                Bytes::new(video_size),
-                Bytes::new(max_cache_size)
-            );
-
-            return Ok(CacheSizeCheck::ExceedsMaxCacheSize);
-        }
-
-        if cache_allocation.as_u64() + video_size >= max_cache_size {
-            if !self.printed_warning {
-                warn!(
-                    "Can't download video: '{}' ({}) as it's too large for the cache ({} of {} allocated). \
-                     Waiting for cache size reduction..",
-                    next_video.title,
-                    Bytes::new(video_size),
-                    &cache_allocation,
-                    Bytes::new(max_cache_size)
-                );
-                self.printed_warning = true;
-
-                // Update this value immediately.
-                // This avoids printing the "Current cache size has changed .." warning below.
-                self.cached_cache_allocation = Some(cache_allocation);
-            }
-
-            if let Some(cca) = self.cached_cache_allocation {
-                if cca != cache_allocation {
-                    // Only print the warning if the display string has actually changed.
-                    // Otherwise, we might confuse the user
-                    if cca.to_string() != cache_allocation.to_string() {
-                        warn!(
-                            "Current cache size has changed, it's now: '{}'",
-                            cache_allocation
-                        );
-                    }
-                    debug!(
-                        "Cache size has changed: {} -> {}",
-                        cca.as_u64(),
-                        cache_allocation.as_u64()
-                    );
-                    self.cached_cache_allocation = Some(cache_allocation);
-                }
-            } else {
-                unreachable!(
-                    "The `printed_warning` should be false in this case, \
-                    and thus should have already set the `cached_cache_allocation`."
-                );
-            }
-
-            // Wait and hope, that a large video is deleted from the cache.
-            wait_for_cache_reduction(app).await?;
-            Ok(CacheSizeCheck::TooLarge)
-        } else {
-            self.printed_warning = false;
-            Ok(CacheSizeCheck::Fits)
-        }
-    }
-
-    /// The entry point to the Downloader.
-    /// This Downloader will periodically check if the database has changed, and then also
-    /// change which videos it downloads.
-    /// This will run, until the database doesn't contain any watchable videos
-    pub async fn consume(&mut self, app: Arc<App>, max_cache_size: u64) -> Result<()> {
-        while let Some(next_video) = get_next_uncached_video(&app).await? {
-            match self
-                .is_enough_cache_available(&app, max_cache_size, &next_video)
-                .await?
-            {
-                CacheSizeCheck::Fits => (),
-                CacheSizeCheck::TooLarge => continue,
-                CacheSizeCheck::ExceedsMaxCacheSize => bail!("Giving up."),
-            };
-
-            if self.current_download.is_some() {
-                let current_download = self.current_download.take().unreachable("It is `Some`.");
-
-                if current_download.task_handle.is_finished() {
-                    current_download.task_handle.await??;
-                    continue;
-                }
-
-                if next_video.extractor_hash == current_download.extractor_hash {
-                    // Reset the taken value
-                    self.current_download = Some(current_download);
-                } else {
-                    info!(
-                        "Noticed, that the next video is not the video being downloaded, replacing it ('{}' vs. '{}')!",
-                        next_video.extractor_hash.into_short_hash(&app).await?,
-                        current_download
-                            .extractor_hash
-                            .into_short_hash(&app)
-                            .await?
-                    );
-
-                    // Replace the currently downloading video
-                    // FIXME(@bpeetz): This does not work (probably because of the python part.) <2025-02-21>
-                    current_download.task_handle.abort();
-
-                    let new_current_download =
-                        CurrentDownload::new_from_video(Arc::clone(&app), next_video);
-
-                    self.current_download = Some(new_current_download);
-                }
-            } else {
-                info!(
-                    "No video is being downloaded right now, setting it to '{}'",
-                    next_video.title
-                );
-                let new_current_download =
-                    CurrentDownload::new_from_video(Arc::clone(&app), next_video);
-                self.current_download = Some(new_current_download);
-            }
-
-            // TODO(@bpeetz): Why do we sleep here? <2025-02-21>
-            time::sleep(Duration::from_secs(1)).await;
-        }
-
-        info!("Finished downloading!");
-        Ok(())
-    }
-
-    pub async fn get_current_cache_allocation(app: &App) -> Result<Bytes> {
-        fn dir_size(mut dir: fs::ReadDir) -> BoxFuture<'static, Result<Bytes>> {
-            async move {
-                let mut acc = 0;
-                while let Some(entry) = dir.next_entry().await? {
-                    let size = match entry.metadata().await? {
-                        data if data.is_dir() => {
-                            let path = entry.path();
-                            let read_dir = fs::read_dir(path).await?;
-
-                            dir_size(read_dir).await?.as_u64()
-                        }
-                        data => data.len(),
-                    };
-                    acc += size;
-                }
-                Ok(Bytes::new(acc))
-            }
-            .boxed()
-        }
-
-        let read_dir_result = match fs::read_dir(&app.config.paths.download_dir).await {
-            Ok(ok) => ok,
-            Err(err) => match err.kind() {
-                io::ErrorKind::NotFound => {
-                    fs::create_dir_all(&app.config.paths.download_dir)
-                        .await
-                        .with_context(|| {
-                            format!(
-                                "Failed to create download dir at: '{}'",
-                                &app.config.paths.download_dir.display()
-                            )
-                        })?;
-
-                    info!(
-                        "Created empty download dir at '{}'",
-                        &app.config.paths.download_dir.display(),
-                    );
-
-                    // The new dir should not contain anything (otherwise we would not have had to
-                    // create it)
-                    return Ok(Bytes::new(0));
-                }
-                err => Err(io::Error::from(err)).with_context(|| {
-                    format!(
-                        "Failed to get dir size of download dir at: '{}'",
-                        &app.config.paths.download_dir.display()
-                    )
-                })?,
-            },
-        };
-
-        dir_size(read_dir_result).await
-    }
-
-    async fn get_approx_video_size(&mut self, app: &App, video: &Video) -> Result<u64> {
-        if let Some(value) = self.video_size_cache.get(&video.extractor_hash) {
-            Ok(*value)
-        } else {
-            // the subtitle file size should be negligible
-            let add_opts = YtDlpOptions {
-                subtitle_langs: String::new(),
-            };
-            let opts = &download_opts(app, &add_opts);
-
-            let result = yt_dlp::extract_info(opts, &video.url, false, true)
-                .await
-                .with_context(|| {
-                    format!("Failed to extract video information: '{}'", video.title)
-                })?;
-
-            let size = if let Some(val) = result.filesize {
-                val
-            } else if let Some(val) = result.filesize_approx {
-                val
-            } else if result.duration.is_some() && result.tbr.is_some() {
-                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
-                let duration = result.duration.expect("Is some").ceil() as u64;
-
-                // TODO: yt_dlp gets this from the format
-                #[allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
-                let tbr = result.tbr.expect("Is Some").ceil() as u64;
-
-                duration * tbr * (1000 / 8)
-            } else {
-                let hardcoded_default = Bytes::from_str("250 MiB").expect("This is hardcoded");
-                error!(
-                    "Failed to find a filesize for video: '{}' (Using hardcoded value of {})",
-                    video.title, hardcoded_default
-                );
-                hardcoded_default.as_u64()
-            };
-
-            assert_eq!(
-                self.video_size_cache.insert(video.extractor_hash, size),
-                None
-            );
-
-            Ok(size)
-        }
-    }
-
-    async fn actually_cache_video(app: &App, video: &Video) -> Result<()> {
-        debug!("Download started: {}", &video.title);
-
-        let addional_opts = get_video_yt_dlp_opts(app, &video.extractor_hash).await?;
-
-        let result = yt_dlp::download(&[video.url.clone()], &download_opts(app, &addional_opts))
-            .await
-            .with_context(|| format!("Failed to download video: '{}'", video.title))?;
-
-        assert_eq!(result.len(), 1);
-        let result = &result[0];
-
-        set_video_cache_path(app, &video.extractor_hash, Some(result)).await?;
-
-        info!(
-            "Video '{}' was downlaoded to path: {}",
-            video.title,
-            result.display()
-        );
-
-        Ok(())
-    }
-}
diff --git a/yt/src/main.rs b/yt/src/main.rs
deleted file mode 100644
index ffb3e14..0000000
--- a/yt/src/main.rs
+++ /dev/null
@@ -1,256 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-// `yt` is not a library. Besides, the `anyhow::Result` type is really useless, if you're not going
-// to print it anyways.
-#![allow(clippy::missing_errors_doc)]
-
-use std::{fs, sync::Arc};
-
-use anyhow::{Context, Result, bail};
-use app::App;
-use bytes::Bytes;
-use cache::{invalidate, maintain};
-use clap::Parser;
-use cli::{CacheCommand, CheckCommand, SelectCommand, SubscriptionCommand, VideosCommand};
-use config::Config;
-use log::info;
-use select::cmds::handle_select_cmd;
-use storage::video_database::get::video_by_hash;
-use tokio::{
-    fs::File,
-    io::{BufReader, stdin},
-    task::JoinHandle,
-};
-use yt_dlp::wrapper::info_json::InfoJson;
-
-use crate::{cli::Command, storage::subscriptions};
-
-pub mod app;
-pub mod cli;
-pub mod unreachable;
-
-pub mod cache;
-pub mod comments;
-pub mod config;
-pub mod constants;
-pub mod download;
-pub mod select;
-pub mod status;
-pub mod storage;
-pub mod subscribe;
-pub mod update;
-pub mod version;
-pub mod videos;
-pub mod watch;
-
-#[tokio::main]
-// This is _the_ main function after all. It is not really good, but it sort of works.
-#[allow(clippy::too_many_lines)]
-async fn main() -> Result<()> {
-    let args = cli::CliArgs::parse();
-
-    // The default verbosity is 1 (Warn)
-    let verbosity: u8 = args.verbosity + 1;
-
-    stderrlog::new()
-        .module(module_path!())
-        .modules(&["yt_dlp".to_owned(), "libmpv2".to_owned()])
-        .quiet(args.quiet)
-        .show_module_names(false)
-        .color(stderrlog::ColorChoice::Auto)
-        .verbosity(verbosity as usize)
-        .timestamp(stderrlog::Timestamp::Off)
-        .init()
-        .expect("Let's just hope that this does not panic");
-
-    info!("Using verbosity level: '{} ({})'", verbosity, {
-        match verbosity {
-            0 => "Error",
-            1 => "Warn",
-            2 => "Info",
-            3 => "Debug",
-            4.. => "Trace",
-        }
-    });
-
-    let config = Config::from_config_file(args.db_path, args.config_path, args.color)?;
-    if args.version {
-        version::show(&config).await?;
-        return Ok(());
-    }
-
-    let app = App::new(config, !args.no_migrate_db).await?;
-
-    match args.command.unwrap_or(Command::default()) {
-        Command::Download {
-            force,
-            max_cache_size,
-        } => {
-            let max_cache_size =
-                max_cache_size.unwrap_or(app.config.download.max_cache_size.as_u64());
-            info!("Max cache size: '{}'", Bytes::new(max_cache_size));
-
-            maintain(&app, false).await?;
-            if force {
-                invalidate(&app, true).await?;
-            }
-
-            download::Downloader::new()
-                .consume(Arc::new(app), max_cache_size)
-                .await?;
-        }
-        Command::Select { cmd } => {
-            let cmd = cmd.unwrap_or(SelectCommand::default());
-
-            match cmd {
-                SelectCommand::File {
-                    done,
-                    use_last_selection,
-                } => Box::pin(select::select(&app, done, use_last_selection)).await?,
-                _ => Box::pin(handle_select_cmd(&app, cmd, None)).await?,
-            }
-        }
-        Command::Sedowa {} => {
-            Box::pin(select::select(&app, false, false)).await?;
-
-            let arc_app = Arc::new(app);
-            dowa(arc_app).await?;
-        }
-        Command::Dowa {} => {
-            let arc_app = Arc::new(app);
-            dowa(arc_app).await?;
-        }
-        Command::Videos { cmd } => match cmd {
-            VideosCommand::List {
-                search_query,
-                limit,
-            } => {
-                videos::query(&app, limit, search_query)
-                    .await
-                    .context("Failed to query videos")?;
-            }
-            VideosCommand::Info { hash } => {
-                let video = video_by_hash(&app, &hash.realize(&app).await?).await?;
-
-                print!(
-                    "{}",
-                    &video
-                        .to_info_display(&app)
-                        .await
-                        .context("Failed to format video")?
-                );
-            }
-        },
-        Command::Update {
-            max_backlog,
-            subscriptions,
-        } => {
-            let all_subs = subscriptions::get(&app).await?;
-
-            for sub in &subscriptions {
-                if !all_subs.0.contains_key(sub) {
-                    bail!(
-                        "Your specified subscription to update '{}' is not a subscription!",
-                        sub
-                    )
-                }
-            }
-
-            let max_backlog = max_backlog.unwrap_or(app.config.update.max_backlog);
-
-            update::update(&app, max_backlog, subscriptions).await?;
-        }
-        Command::Subscriptions { cmd } => match cmd {
-            SubscriptionCommand::Add { name, url } => {
-                subscribe::subscribe(&app, name, url)
-                    .await
-                    .context("Failed to add a subscription")?;
-            }
-            SubscriptionCommand::Remove { name } => {
-                subscribe::unsubscribe(&app, name)
-                    .await
-                    .context("Failed to remove a subscription")?;
-            }
-            SubscriptionCommand::List {} => {
-                let all_subs = subscriptions::get(&app).await?;
-
-                for (key, val) in all_subs.0 {
-                    println!("{}: '{}'", key, val.url);
-                }
-            }
-            SubscriptionCommand::Export {} => {
-                let all_subs = subscriptions::get(&app).await?;
-                for val in all_subs.0.values() {
-                    println!("{}", val.url);
-                }
-            }
-            SubscriptionCommand::Import { file, force } => {
-                if let Some(file) = file {
-                    let f = File::open(file).await?;
-
-                    subscribe::import(&app, BufReader::new(f), force).await?;
-                } else {
-                    subscribe::import(&app, BufReader::new(stdin()), force).await?;
-                };
-            }
-        },
-
-        Command::Watch {} => watch::watch(Arc::new(app)).await?,
-        Command::Playlist { watch } => watch::playlist::playlist(&app, watch).await?,
-
-        Command::Status {} => status::show(&app).await?,
-        Command::Config {} => status::config(&app)?,
-
-        Command::Database { command } => match command {
-            CacheCommand::Invalidate { hard } => invalidate(&app, hard).await?,
-            CacheCommand::Maintain { all } => maintain(&app, all).await?,
-        },
-
-        Command::Check { command } => match command {
-            CheckCommand::InfoJson { path } => {
-                let string = fs::read_to_string(&path)
-                    .with_context(|| format!("Failed to read '{}' to string!", path.display()))?;
-
-                drop(
-                    serde_json::from_str::<InfoJson>(&string)
-                        .context("Failed to deserialize value")?,
-                );
-            }
-        },
-        Command::Comments {} => {
-            comments::comments(&app).await?;
-        }
-        Command::Description {} => {
-            comments::description(&app).await?;
-        }
-    }
-
-    Ok(())
-}
-
-async fn dowa(arc_app: Arc<App>) -> Result<()> {
-    let max_cache_size = arc_app.config.download.max_cache_size;
-    info!("Max cache size: '{}'", max_cache_size);
-
-    let arc_app_clone = Arc::clone(&arc_app);
-    let download: JoinHandle<Result<()>> = tokio::spawn(async move {
-        download::Downloader::new()
-            .consume(arc_app_clone, max_cache_size.as_u64())
-            .await?;
-
-        Ok(())
-    });
-
-    watch::watch(arc_app).await?;
-    download.await??;
-    Ok(())
-}
diff --git a/yt/src/select/cmds/mod.rs b/yt/src/select/cmds/mod.rs
deleted file mode 100644
index ea41f99..0000000
--- a/yt/src/select/cmds/mod.rs
+++ /dev/null
@@ -1,106 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use crate::{
-    app::App,
-    cli::{SelectCommand, SharedSelectionCommandArgs},
-    storage::video_database::{
-        Priority, VideoOptions, VideoStatus,
-        get::video_by_hash,
-        set::{set_video_options, video_status},
-    },
-};
-
-use anyhow::{Context, Result, bail};
-
-mod add;
-
-pub async fn handle_select_cmd(
-    app: &App,
-    cmd: SelectCommand,
-    line_number: Option<i64>,
-) -> Result<()> {
-    match cmd {
-        SelectCommand::Pick { shared } => {
-            handle_status_change(app, shared, line_number, VideoStatus::Pick).await?;
-        }
-        SelectCommand::Drop { shared } => {
-            handle_status_change(app, shared, line_number, VideoStatus::Drop).await?;
-        }
-        SelectCommand::Watched { shared } => {
-            handle_status_change(app, shared, line_number, VideoStatus::Watched).await?;
-        }
-        SelectCommand::Add { urls, start, stop } => {
-            Box::pin(add::add(app, urls, start, stop)).await?;
-        }
-        SelectCommand::Watch { shared } => {
-            let hash = shared.hash.clone().realize(app).await?;
-
-            let video = video_by_hash(app, &hash).await?;
-
-            if let VideoStatus::Cached {
-                cache_path,
-                is_focused,
-            } = video.status
-            {
-                handle_status_change(app, shared, line_number, VideoStatus::Cached {
-                    cache_path,
-                    is_focused,
-                })
-                .await?;
-            } else {
-                handle_status_change(app, shared, line_number, VideoStatus::Watch).await?;
-            }
-        }
-
-        SelectCommand::Url { shared } => {
-            let Some(url) = shared.url else {
-                bail!("You need to provide a url to `select url ..`")
-            };
-
-            let mut firefox = std::process::Command::new("firefox");
-            firefox.args(["-P", "timesinks.youtube"]);
-            firefox.arg(url.as_str());
-            let _handle = firefox.spawn().context("Failed to run firefox")?;
-        }
-        SelectCommand::File { .. } => unreachable!("This should have been filtered out"),
-    }
-    Ok(())
-}
-
-async fn handle_status_change(
-    app: &App,
-    shared: SharedSelectionCommandArgs,
-    line_number: Option<i64>,
-    new_status: VideoStatus,
-) -> Result<()> {
-    let hash = shared.hash.realize(app).await?;
-    let video_options = VideoOptions::new(
-        shared
-            .subtitle_langs
-            .unwrap_or(app.config.select.subtitle_langs.clone()),
-        shared.speed.unwrap_or(app.config.select.playback_speed),
-    );
-    let priority = compute_priority(line_number, shared.priority);
-
-    video_status(app, &hash, new_status, priority).await?;
-    set_video_options(app, &hash, &video_options).await?;
-
-    Ok(())
-}
-
-fn compute_priority(line_number: Option<i64>, priority: Option<i64>) -> Option<Priority> {
-    if let Some(pri) = priority {
-        Some(Priority::from(pri))
-    } else {
-        line_number.map(Priority::from)
-    }
-}
diff --git a/yt/src/select/mod.rs b/yt/src/select/mod.rs
deleted file mode 100644
index 54db65c..0000000
--- a/yt/src/select/mod.rs
+++ /dev/null
@@ -1,173 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{
-    env::{self},
-    fs,
-    io::{BufRead, BufReader, BufWriter, Write},
-    string::String,
-};
-
-use crate::{
-    app::App,
-    cli::CliArgs,
-    constants::HELP_STR,
-    storage::video_database::{Video, VideoStatusMarker, get},
-    unreachable::Unreachable,
-};
-
-use anyhow::{Context, Result, bail};
-use clap::Parser;
-use cmds::handle_select_cmd;
-use futures::{TryStreamExt, stream::FuturesOrdered};
-use selection_file::process_line;
-use tempfile::Builder;
-use tokio::process::Command;
-
-pub mod cmds;
-pub mod selection_file;
-
-async fn to_select_file_display_owned(video: Video, app: &App) -> Result<String> {
-    video.to_select_file_display(app).await
-}
-
-pub async fn select(app: &App, done: bool, use_last_selection: bool) -> Result<()> {
-    let temp_file = Builder::new()
-        .prefix("yt_video_select-")
-        .suffix(".yts")
-        .rand_bytes(6)
-        .tempfile()
-        .context("Failed to get tempfile")?;
-
-    if use_last_selection {
-        fs::copy(&app.config.paths.last_selection_path, &temp_file)?;
-    } else {
-        let matching_videos = if done {
-            get::videos(app, VideoStatusMarker::ALL).await?
-        } else {
-            get::videos(app, &[
-                VideoStatusMarker::Pick,
-                //
-                VideoStatusMarker::Watch,
-                VideoStatusMarker::Cached,
-            ])
-            .await?
-        };
-
-        // Warmup the cache for the display rendering of the videos.
-        // Otherwise the futures would all try to warm it up at the same time.
-        if let Some(vid) = matching_videos.first() {
-            drop(vid.to_line_display(app).await?);
-        }
-
-        let mut edit_file = BufWriter::new(&temp_file);
-
-        matching_videos
-            .into_iter()
-            .map(|vid| to_select_file_display_owned(vid, app))
-            .collect::<FuturesOrdered<_>>()
-            .try_collect::<Vec<String>>()
-            .await?
-            .into_iter()
-            .try_for_each(|line| -> Result<()> {
-                edit_file
-                    .write_all(line.as_bytes())
-                    .context("Failed to write to `edit_file`")?;
-
-                Ok(())
-            })?;
-
-        edit_file.write_all(HELP_STR.as_bytes())?;
-        edit_file.flush().context("Failed to flush edit file")?;
-    };
-
-    {
-        let editor = env::var("EDITOR").unwrap_or("nvim".to_owned());
-
-        let mut nvim = Command::new(editor);
-        nvim.arg(temp_file.path());
-        let status = nvim.status().await.context("Falied to run nvim")?;
-        if !status.success() {
-            bail!("nvim exited with error status: {}", status)
-        }
-    }
-
-    let read_file = temp_file.reopen()?;
-    fs::copy(temp_file.path(), &app.config.paths.last_selection_path)
-        .context("Failed to persist selection file")?;
-
-    let reader = BufReader::new(&read_file);
-
-    let mut line_number = 0;
-    for line in reader.lines() {
-        let line = line.context("Failed to read a line")?;
-
-        if let Some(line) = process_line(&line)? {
-            line_number -= 1;
-
-            // debug!(
-            //     "Parsed command: `{}`",
-            //     line.iter()
-            //         .map(|val| format!("\"{}\"", val))
-            //         .collect::<Vec<String>>()
-            //         .join(" ")
-            // );
-
-            let arg_line = ["yt", "select"]
-                .into_iter()
-                .chain(line.iter().map(String::as_str));
-
-            let args = CliArgs::parse_from(arg_line);
-
-            let crate::cli::Command::Select { cmd } = args
-                .command
-                .unreachable("This will be some, as we constructed it above.")
-            else {
-                unreachable!("This is checked in the `filter_line` function")
-            };
-
-            Box::pin(handle_select_cmd(
-                app,
-                cmd.unreachable(
-                    "This value should always be some \
-                    here, as it would otherwise thrown an error above.",
-                ),
-                Some(line_number),
-            ))
-            .await?;
-        }
-    }
-
-    Ok(())
-}
-
-// // FIXME: There should be no reason why we need to re-run yt, just to get the help string. But I've
-// // yet to find a way to do it without the extra exec <2024-08-20>
-// async fn get_help() -> Result<String> {
-//     let binary_name = current_exe()?;
-//     let cmd = Command::new(binary_name)
-//         .args(&["select", "--help"])
-//         .output()
-//         .await?;
-//
-//     assert_eq!(cmd.status.code(), Some(0));
-//
-//     let output = String::from_utf8(cmd.stdout).expect("Our help output was not utf8?");
-//
-//     let out = output
-//         .lines()
-//         .map(|line| format!("# {}\n", line))
-//         .collect::<String>();
-//
-//     debug!("Returning help: '{}'", &out);
-//
-//     Ok(out)
-// }
diff --git a/yt/src/select/selection_file/duration.rs b/yt/src/select/selection_file/duration.rs
deleted file mode 100644
index 77c4fc5..0000000
--- a/yt/src/select/selection_file/duration.rs
+++ /dev/null
@@ -1,185 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::str::FromStr;
-use std::time::Duration;
-
-use anyhow::{Context, Result};
-
-const SECOND: u64 = 1;
-const MINUTE: u64 = 60 * SECOND;
-const HOUR: u64 = 60 * MINUTE;
-const DAY: u64 = 24 * HOUR;
-
-#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
-pub struct MaybeDuration {
-    time: Option<Duration>,
-}
-
-impl MaybeDuration {
-    #[must_use]
-    pub fn from_std(d: Duration) -> Self {
-        Self { time: Some(d) }
-    }
-
-    #[must_use]
-    pub fn from_secs_f64(d: f64) -> Self {
-        Self {
-            time: Some(Duration::from_secs_f64(d)),
-        }
-    }
-    #[must_use]
-    pub fn from_maybe_secs_f64(d: Option<f64>) -> Self {
-        Self {
-            time: d.map(Duration::from_secs_f64),
-        }
-    }
-    #[must_use]
-    pub fn from_secs(d: u64) -> Self {
-        Self {
-            time: Some(Duration::from_secs(d)),
-        }
-    }
-
-    #[must_use]
-    pub fn zero() -> Self {
-        Self {
-            time: Some(Duration::default()),
-        }
-    }
-
-    /// Try to return the current duration encoded as seconds.
-    #[must_use]
-    pub fn as_secs(&self) -> Option<u64> {
-        self.time.map(|v| v.as_secs())
-    }
-
-    /// Try to return the current duration encoded as seconds and nanoseconds.
-    #[must_use]
-    pub fn as_secs_f64(&self) -> Option<f64> {
-        self.time.map(|v| v.as_secs_f64())
-    }
-}
-
-impl FromStr for MaybeDuration {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> Result<Self, Self::Err> {
-        fn parse_num(str: &str, suffix: char) -> Result<u64> {
-            str.strip_suffix(suffix)
-                .with_context(|| format!("Failed to strip suffix '{suffix}' of number: '{str}'"))?
-                .parse::<u64>()
-                .with_context(|| format!("Failed to parse '{suffix}'"))
-        }
-
-        if s == "[No duration]" {
-            return Ok(Self { time: None });
-        }
-
-        let buf: Vec<_> = s.split(' ').collect();
-
-        let days;
-        let hours;
-        let minutes;
-        let seconds;
-
-        assert_eq!(buf.len(), 2, "Other lengths should not happen");
-
-        if buf[0].ends_with('d') {
-            days = parse_num(buf[0], 'd')?;
-            hours = parse_num(buf[1], 'h')?;
-            minutes = parse_num(buf[2], 'm')?;
-            seconds = 0;
-        } else if buf[0].ends_with('h') {
-            days = 0;
-            hours = parse_num(buf[0], 'h')?;
-            minutes = parse_num(buf[1], 'm')?;
-            seconds = 0;
-        } else if buf[0].ends_with('m') {
-            days = 0;
-            hours = 0;
-            minutes = parse_num(buf[0], 'm')?;
-            seconds = parse_num(buf[1], 's')?;
-        } else {
-            unreachable!(
-                "The first part always ends with 'h' or 'm', but was: {:#?}",
-                buf
-            )
-        }
-
-        Ok(Self {
-            time: Some(Duration::from_secs(
-                days * DAY + hours * HOUR + minutes * MINUTE + seconds * SECOND,
-            )),
-        })
-    }
-}
-
-impl std::fmt::Display for MaybeDuration {
-    fn fmt(&self, fmt: &mut std::fmt::Formatter<'_>) -> Result<(), std::fmt::Error> {
-        if let Some(self_seconds) = self.as_secs() {
-            let base_day = self_seconds - (self_seconds % DAY);
-            let base_hour = (self_seconds % DAY) - ((self_seconds % DAY) % HOUR);
-            let base_min = (self_seconds % HOUR) - (((self_seconds % DAY) % HOUR) % MINUTE);
-            let base_sec = ((self_seconds % DAY) % HOUR) % MINUTE;
-
-            let d = base_day / DAY;
-            let h = base_hour / HOUR;
-            let m = base_min / MINUTE;
-            let s = base_sec / SECOND;
-
-            if d > 0 {
-                write!(fmt, "{d}d {h}h {m}m")
-            } else if h > 0 {
-                write!(fmt, "{h}h {m}m")
-            } else {
-                write!(fmt, "{m}m {s}s")
-            }
-        } else {
-            write!(fmt, "[No duration]")
-        }
-    }
-}
-#[cfg(test)]
-mod test {
-    use std::str::FromStr;
-
-    use crate::select::selection_file::duration::{DAY, HOUR, MINUTE};
-
-    use super::MaybeDuration;
-
-    #[test]
-    fn test_display_duration_1h() {
-        let dur = MaybeDuration::from_secs(HOUR);
-        assert_eq!("1h 0m".to_owned(), dur.to_string());
-    }
-    #[test]
-    fn test_display_duration_30min() {
-        let dur = MaybeDuration::from_secs(MINUTE * 30);
-        assert_eq!("30m 0s".to_owned(), dur.to_string());
-    }
-    #[test]
-    fn test_display_duration_1d() {
-        let dur = MaybeDuration::from_secs(DAY + MINUTE * 30 + HOUR * 2);
-        assert_eq!("1d 2h 30m".to_owned(), dur.to_string());
-    }
-
-    #[test]
-    fn test_display_duration_roundtrip() {
-        let dur = MaybeDuration::zero();
-        let dur_str = dur.to_string();
-
-        assert_eq!(
-            MaybeDuration::zero(),
-            MaybeDuration::from_str(&dur_str).unwrap()
-        );
-    }
-}
diff --git a/yt/src/select/selection_file/mod.rs b/yt/src/select/selection_file/mod.rs
deleted file mode 100644
index abd26c4..0000000
--- a/yt/src/select/selection_file/mod.rs
+++ /dev/null
@@ -1,32 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! The data structures needed to express the file, which the user edits
-
-use anyhow::{Context, Result};
-use trinitry::Trinitry;
-
-pub mod duration;
-
-pub fn process_line(line: &str) -> Result<Option<Vec<String>>> {
-    // Filter out comments and empty lines
-    if line.starts_with('#') || line.trim().is_empty() {
-        Ok(None)
-    } else {
-        let tri = Trinitry::new(line).with_context(|| format!("Failed to parse line '{line}'"))?;
-
-        let mut vec = Vec::with_capacity(tri.arguments().len() + 1);
-        vec.push(tri.command().to_owned());
-        vec.extend(tri.arguments().to_vec());
-
-        Ok(Some(vec))
-    }
-}
diff --git a/yt/src/status/mod.rs b/yt/src/status/mod.rs
deleted file mode 100644
index bc45cfb..0000000
--- a/yt/src/status/mod.rs
+++ /dev/null
@@ -1,120 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::time::Duration;
-
-use crate::{
-    app::App,
-    download::Downloader,
-    select::selection_file::duration::MaybeDuration,
-    storage::{
-        subscriptions,
-        video_database::{VideoStatusMarker, get},
-    },
-};
-
-use anyhow::{Context, Result};
-use bytes::Bytes;
-
-macro_rules! get {
-    ($videos:expr, $status:ident) => {
-        $videos
-            .iter()
-            .filter(|vid| vid.status.as_marker() == VideoStatusMarker::$status)
-            .count()
-    };
-
-    (@collect $videos:expr, $status:ident) => {
-        $videos
-            .iter()
-            .filter(|vid| vid.status.as_marker() == VideoStatusMarker::$status)
-            .collect()
-    };
-}
-
-pub async fn show(app: &App) -> Result<()> {
-    let all_videos = get::videos(app, VideoStatusMarker::ALL).await?;
-
-    // lengths
-    let picked_videos_len = get!(all_videos, Pick);
-
-    let watch_videos_len = get!(all_videos, Watch);
-    let cached_videos_len = get!(all_videos, Cached);
-    let watched_videos_len = get!(all_videos, Watched);
-    let watched_videos: Vec<_> = get!(@collect all_videos, Watched);
-
-    let drop_videos_len = get!(all_videos, Drop);
-    let dropped_videos_len = get!(all_videos, Dropped);
-
-    let subscriptions = subscriptions::get(app).await?;
-    let subscriptions_len = subscriptions.0.len();
-
-    let watchtime_status = {
-        let total_watch_time_raw = watched_videos
-            .iter()
-            .fold(Duration::default(), |acc, vid| acc + vid.watch_progress);
-
-        // Most things are watched at a speed of s (which is defined in the config file).
-        // Thus
-        //      y = x * s -> y / s = x
-        let total_watch_time = Duration::from_secs_f64(
-            (total_watch_time_raw.as_secs_f64()) / app.config.select.playback_speed,
-        );
-
-        let speed = app.config.select.playback_speed;
-
-        // Do not print the adjusted time, if the user has keep the speed level at 1.
-        #[allow(clippy::float_cmp)]
-        if speed == 1.0 {
-            format!(
-                "Total Watchtime: {}\n",
-                MaybeDuration::from_std(total_watch_time_raw)
-            )
-        } else {
-            format!(
-                "Total Watchtime: {} (at {speed} speed: {})\n",
-                MaybeDuration::from_std(total_watch_time_raw),
-                MaybeDuration::from_std(total_watch_time),
-            )
-        }
-    };
-
-    let cache_usage_raw = Downloader::get_current_cache_allocation(app)
-        .await
-        .context("Failed to get current cache allocation")?;
-    let cache_usage: Bytes = cache_usage_raw;
-    println!(
-        "\
-Picked   Videos: {picked_videos_len}
-
-Watch    Videos: {watch_videos_len}
-Cached   Videos: {cached_videos_len}
-Watched  Videos: {watched_videos_len}
-
-Drop     Videos: {drop_videos_len}
-Dropped  Videos: {dropped_videos_len}
-
-{watchtime_status}
-
-  Subscriptions: {subscriptions_len}
-    Cache usage: {cache_usage}"
-    );
-
-    Ok(())
-}
-
-pub fn config(app: &App) -> Result<()> {
-    let config_str = toml::to_string(&app.config)?;
-
-    print!("{config_str}");
-
-    Ok(())
-}
diff --git a/yt/src/storage/subscriptions.rs b/yt/src/storage/subscriptions.rs
deleted file mode 100644
index 3673eee..0000000
--- a/yt/src/storage/subscriptions.rs
+++ /dev/null
@@ -1,144 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! Handle subscriptions
-
-use std::collections::HashMap;
-
-use anyhow::Result;
-use log::debug;
-use serde_json::{Value, json};
-use sqlx::query;
-use url::Url;
-use yt_dlp::wrapper::info_json::InfoType;
-
-use crate::{app::App, unreachable::Unreachable};
-
-#[derive(Clone, Debug)]
-pub struct Subscription {
-    /// The human readable name of this subscription
-    pub name: String,
-
-    /// The URL this subscription subscribes to
-    pub url: Url,
-}
-
-impl Subscription {
-    #[must_use]
-    pub fn new(name: String, url: Url) -> Self {
-        Self { name, url }
-    }
-}
-
-/// Check whether an URL could be used as a subscription URL
-pub async fn check_url(url: &Url) -> Result<bool> {
-    let Value::Object(yt_opts) = json!( {
-        "playliststart": 1,
-        "playlistend": 10,
-        "noplaylist": false,
-        "extract_flat": "in_playlist",
-    }) else {
-        unreachable!("This is hardcoded");
-    };
-
-    let info = yt_dlp::extract_info(&yt_opts, url, false, false).await?;
-
-    debug!("{:#?}", info);
-
-    Ok(info._type == Some(InfoType::Playlist))
-}
-
-#[derive(Default, Debug)]
-pub struct Subscriptions(pub(crate) HashMap<String, Subscription>);
-
-/// Remove all subscriptions
-pub async fn remove_all(app: &App) -> Result<()> {
-    query!(
-        "
-        DELETE FROM subscriptions;
-    ",
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-/// Get a list of subscriptions
-pub async fn get(app: &App) -> Result<Subscriptions> {
-    let raw_subs = query!(
-        "
-        SELECT *
-        FROM subscriptions;
-    "
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    let subscriptions: HashMap<String, Subscription> = raw_subs
-        .into_iter()
-        .map(|sub| {
-            (
-                sub.name.clone(),
-                Subscription::new(
-                    sub.name,
-                    Url::parse(&sub.url).unreachable("It was an URL, when we inserted it."),
-                ),
-            )
-        })
-        .collect();
-
-    Ok(Subscriptions(subscriptions))
-}
-
-pub async fn add_subscription(app: &App, sub: &Subscription) -> Result<()> {
-    let url = sub.url.to_string();
-
-    query!(
-        "
-        INSERT INTO subscriptions (
-            name,
-            url
-        ) VALUES (?, ?);
-    ",
-        sub.name,
-        url
-    )
-    .execute(&app.database)
-    .await?;
-
-    println!("Subscribed to '{}' at '{}'", sub.name, sub.url);
-    Ok(())
-}
-
-/// # Panics
-/// Only if assertions fail
-pub async fn remove_subscription(app: &App, sub: &Subscription) -> Result<()> {
-    let output = query!(
-        "
-        DELETE FROM subscriptions
-        WHERE name = ?
-    ",
-        sub.name,
-    )
-    .execute(&app.database)
-    .await?;
-
-    assert_eq!(
-        output.rows_affected(),
-        1,
-        "The remove subscriptino query did effect more (or less) than one row. This is a bug."
-    );
-
-    println!("Unsubscribed from '{}' at '{}'", sub.name, sub.url);
-
-    Ok(())
-}
diff --git a/yt/src/storage/video_database/downloader.rs b/yt/src/storage/video_database/downloader.rs
deleted file mode 100644
index a95081e..0000000
--- a/yt/src/storage/video_database/downloader.rs
+++ /dev/null
@@ -1,130 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::{Path, PathBuf};
-
-use anyhow::Result;
-use log::debug;
-use sqlx::query;
-
-use crate::{
-    app::App,
-    storage::video_database::{VideoStatus, VideoStatusMarker},
-    unreachable::Unreachable,
-    video_from_record,
-};
-
-use super::{ExtractorHash, Video};
-
-/// Returns to next video which should be downloaded. This respects the priority assigned by select.
-/// It does not return videos, which are already cached.
-///
-/// # Panics
-/// Only if assertions fail.
-pub async fn get_next_uncached_video(app: &App) -> Result<Option<Video>> {
-    let status = VideoStatus::Watch.as_marker().as_db_integer();
-
-    // NOTE: The ORDER BY statement should be the same as the one in [`get::videos`].<2024-08-22>
-    let result = query!(
-        r#"
-        SELECT *
-        FROM videos
-        WHERE status = ? AND cache_path IS NULL
-        ORDER BY priority DESC, publish_date DESC
-        LIMIT 1;
-    "#,
-        status
-    )
-    .fetch_one(&app.database)
-    .await;
-
-    if let Err(sqlx::Error::RowNotFound) = result {
-        Ok(None)
-    } else {
-        let base = result?;
-
-        Ok(Some(video_from_record! {base}))
-    }
-}
-
-/// Update the cached path of a video. Will be set to NULL if the path is None
-/// This will also set the status to `Cached` when path is Some, otherwise it set's the status to
-/// `Watch`.
-pub async fn set_video_cache_path(
-    app: &App,
-    video: &ExtractorHash,
-    path: Option<&Path>,
-) -> Result<()> {
-    if let Some(path) = path {
-        debug!(
-            "Setting cache path from '{}' to '{}'",
-            video.into_short_hash(app).await?,
-            path.display()
-        );
-
-        let path_str = path.display().to_string();
-        let extractor_hash = video.hash().to_string();
-        let status = VideoStatusMarker::Cached.as_db_integer();
-
-        query!(
-            r#"
-            UPDATE videos
-            SET cache_path = ?, status = ?
-            WHERE extractor_hash = ?;
-        "#,
-            path_str,
-            status,
-            extractor_hash
-        )
-        .execute(&app.database)
-        .await?;
-
-        Ok(())
-    } else {
-        debug!(
-            "Setting cache path from '{}' to NULL",
-            video.into_short_hash(app).await?,
-        );
-
-        let extractor_hash = video.hash().to_string();
-        let status = VideoStatus::Watch.as_marker().as_db_integer();
-
-        query!(
-            r#"
-            UPDATE videos
-            SET cache_path = NULL, status = ?
-            WHERE extractor_hash = ?;
-        "#,
-            status,
-            extractor_hash
-        )
-        .execute(&app.database)
-        .await?;
-
-        Ok(())
-    }
-}
-
-/// Returns the number of cached videos
-pub async fn get_allocated_cache(app: &App) -> Result<u32> {
-    let count = query!(
-        r#"
-        SELECT COUNT(cache_path) as count
-        FROM videos
-        WHERE cache_path IS NOT NULL;
-"#,
-    )
-    .fetch_one(&app.database)
-    .await?;
-
-    Ok(u32::try_from(count.count)
-        .unreachable("The value should be strictly positive (and bolow `u32::Max`)"))
-}
diff --git a/yt/src/storage/video_database/extractor_hash.rs b/yt/src/storage/video_database/extractor_hash.rs
deleted file mode 100644
index df545d7..0000000
--- a/yt/src/storage/video_database/extractor_hash.rs
+++ /dev/null
@@ -1,163 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{collections::HashSet, fmt::Display, str::FromStr};
-
-use anyhow::{Context, Result, bail};
-use blake3::Hash;
-use log::debug;
-use tokio::sync::OnceCell;
-
-use crate::{app::App, storage::video_database::get::get_all_hashes, unreachable::Unreachable};
-
-static EXTRACTOR_HASH_LENGTH: OnceCell<usize> = OnceCell::const_new();
-
-#[derive(Debug, Clone, PartialEq, Eq, Hash, Copy)]
-pub struct ExtractorHash {
-    hash: Hash,
-}
-
-impl Display for ExtractorHash {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.hash.fmt(f)
-    }
-}
-
-#[derive(Debug, Clone)]
-pub struct ShortHash(String);
-
-impl Display for ShortHash {
-    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        self.0.fmt(f)
-    }
-}
-
-#[derive(Debug, Clone)]
-#[allow(clippy::module_name_repetitions)]
-pub struct LazyExtractorHash {
-    value: ShortHash,
-}
-
-impl FromStr for LazyExtractorHash {
-    type Err = anyhow::Error;
-
-    fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
-        // perform some cheap validation
-        if s.len() > 64 {
-            bail!("A hash can only contain 64 bytes!");
-        }
-
-        Ok(Self {
-            value: ShortHash(s.to_owned()),
-        })
-    }
-}
-
-impl LazyExtractorHash {
-    /// Turn the [`LazyExtractorHash`] into the [`ExtractorHash`]
-    pub async fn realize(self, app: &App) -> Result<ExtractorHash> {
-        ExtractorHash::from_short_hash(app, &self.value).await
-    }
-}
-
-impl ExtractorHash {
-    #[must_use]
-    pub fn from_hash(hash: Hash) -> Self {
-        Self { hash }
-    }
-    pub async fn from_short_hash(app: &App, s: &ShortHash) -> Result<Self> {
-        Ok(Self {
-            hash: Self::short_hash_to_full_hash(app, s).await?,
-        })
-    }
-
-    #[must_use]
-    pub fn hash(&self) -> &Hash {
-        &self.hash
-    }
-
-    pub async fn into_short_hash(&self, app: &App) -> Result<ShortHash> {
-        let needed_chars = if let Some(needed_chars) = EXTRACTOR_HASH_LENGTH.get() {
-            *needed_chars
-        } else {
-            let needed_chars = self
-                .get_needed_char_len(app)
-                .await
-                .context("Failed to calculate needed char length")?;
-            EXTRACTOR_HASH_LENGTH.set(needed_chars).unreachable(
-                "This should work at this stage, as we checked above that it is empty.",
-            );
-
-            needed_chars
-        };
-
-        Ok(ShortHash(
-            self.hash()
-                .to_hex()
-                .chars()
-                .take(needed_chars)
-                .collect::<String>(),
-        ))
-    }
-
-    async fn short_hash_to_full_hash(app: &App, s: &ShortHash) -> Result<Hash> {
-        let all_hashes = get_all_hashes(app)
-            .await
-            .context("Failed to fetch all extractor -hashesh from database")?;
-
-        let needed_chars = s.0.len();
-
-        for hash in all_hashes {
-            if hash.to_hex()[..needed_chars] == s.0 {
-                return Ok(hash);
-            }
-        }
-
-        bail!("Your shortend hash, does not match a real hash (this is probably a bug)!");
-    }
-
-    async fn get_needed_char_len(&self, app: &App) -> Result<usize> {
-        debug!("Calculating the needed hash char length");
-        let all_hashes = get_all_hashes(app)
-            .await
-            .context("Failed to fetch all extractor -hashesh from database")?;
-
-        let all_char_vec_hashes = all_hashes
-            .into_iter()
-            .map(|hash| hash.to_hex().chars().collect::<Vec<char>>())
-            .collect::<Vec<Vec<_>>>();
-
-        // This value should be updated later, if not rust will panic in the assertion.
-        let mut needed_chars: usize = 1000;
-        'outer: for i in 1..64 {
-            let i_chars: Vec<String> = all_char_vec_hashes
-                .iter()
-                .map(|vec| vec.iter().take(i).collect::<String>())
-                .collect();
-
-            let mut uniqnes_hashmap: HashSet<String> = HashSet::new();
-            for ch in i_chars {
-                if !uniqnes_hashmap.insert(ch) {
-                    // The key was already in the hash map, thus we have a duplicated char and need
-                    // at least one char more
-                    continue 'outer;
-                }
-            }
-
-            needed_chars = i;
-            break 'outer;
-        }
-
-        assert!(needed_chars <= 64, "Hashes are only 64 bytes long");
-
-        Ok(needed_chars)
-    }
-}
diff --git a/yt/src/storage/video_database/get/mod.rs b/yt/src/storage/video_database/get/mod.rs
deleted file mode 100644
index 6a4220e..0000000
--- a/yt/src/storage/video_database/get/mod.rs
+++ /dev/null
@@ -1,303 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! These functions interact with the storage db in a read-only way. They are added on-demand (as
-//! you could theoretically just could do everything with the `get_videos` function), as
-//! performance or convince requires.
-use std::{fs::File, path::PathBuf};
-
-use anyhow::{Context, Result, bail};
-use blake3::Hash;
-use log::{debug, trace};
-use sqlx::query;
-use yt_dlp::wrapper::info_json::InfoJson;
-
-use crate::{
-    app::App,
-    storage::{
-        subscriptions::Subscription,
-        video_database::{Video, extractor_hash::ExtractorHash},
-    },
-    unreachable::Unreachable,
-};
-
-use super::{MpvOptions, VideoOptions, VideoStatus, VideoStatusMarker, YtDlpOptions};
-
-mod playlist;
-pub use playlist::*;
-
-#[macro_export]
-macro_rules! video_from_record {
-    ($record:expr) => {
-        Video {
-            description: $record.description.clone(),
-            duration: $crate::storage::video_database::MaybeDuration::from_maybe_secs_f64(
-                $record.duration,
-            ),
-            extractor_hash:
-                $crate::storage::video_database::extractor_hash::ExtractorHash::from_hash(
-                    $record
-                        .extractor_hash
-                        .parse()
-                        .expect("The db hash should be a valid blake3 hash"),
-                ),
-            last_status_change: $crate::storage::video_database::TimeStamp::from_secs(
-                $record.last_status_change,
-            ),
-            parent_subscription_name: $record.parent_subscription_name.clone(),
-            publish_date: $record
-                .publish_date
-                .map(|pd| $crate::storage::video_database::TimeStamp::from_secs(pd)),
-            status: {
-                let marker = $crate::storage::video_database::VideoStatusMarker::from_db_integer(
-                    $record.status,
-                );
-
-                let optional = if let Some(cache_path) = &$record.cache_path {
-                    Some((
-                        PathBuf::from(cache_path),
-                        if $record.is_focused == 1 { true } else { false },
-                    ))
-                } else {
-                    None
-                };
-
-                $crate::storage::video_database::VideoStatus::from_marker(marker, optional)
-            },
-            thumbnail_url: if let Some(url) = &$record.thumbnail_url {
-                Some(url::Url::parse(&url).expect("Parsing this as url should always work"))
-            } else {
-                None
-            },
-            title: $record.title.clone(),
-            url: url::Url::parse(&$record.url).expect("Parsing this as url should always work"),
-            priority: $crate::storage::video_database::Priority::from($record.priority),
-
-            watch_progress: std::time::Duration::from_secs(
-                u64::try_from($record.watch_progress).expect("The record is positive i64"),
-            ),
-        }
-    };
-}
-
-/// Returns the videos that are in the `allowed_states`.
-///
-/// # Panics
-/// Only, if assertions fail.
-pub async fn videos(app: &App, allowed_states: &[VideoStatusMarker]) -> Result<Vec<Video>> {
-    fn test(all_states: &[VideoStatusMarker], check: VideoStatusMarker) -> Option<i64> {
-        if all_states.contains(&check) {
-            trace!("State '{check:?}' marked as active");
-            Some(check.as_db_integer())
-        } else {
-            trace!("State '{check:?}' marked as inactive");
-            None
-        }
-    }
-    fn states_to_string(allowed_states: &[VideoStatusMarker]) -> String {
-        let mut states = allowed_states
-            .iter()
-            .fold(String::from("&["), |mut acc, state| {
-                acc.push_str(state.as_str());
-                acc.push_str(", ");
-                acc
-            });
-        states = states.trim().to_owned();
-        states = states.trim_end_matches(',').to_owned();
-        states.push(']');
-        states
-    }
-
-    debug!(
-        "Fetching videos in the states: '{}'",
-        states_to_string(allowed_states)
-    );
-    let active_pick: Option<i64> = test(allowed_states, VideoStatusMarker::Pick);
-    let active_watch: Option<i64> = test(allowed_states, VideoStatusMarker::Watch);
-    let active_cached: Option<i64> = test(allowed_states, VideoStatusMarker::Cached);
-    let active_watched: Option<i64> = test(allowed_states, VideoStatusMarker::Watched);
-    let active_drop: Option<i64> = test(allowed_states, VideoStatusMarker::Drop);
-    let active_dropped: Option<i64> = test(allowed_states, VideoStatusMarker::Dropped);
-
-    let videos = query!(
-        r"
-        SELECT *
-          FROM videos
-          WHERE status IN (?,?,?,?,?,?)
-          ORDER BY priority DESC, publish_date DESC;
-          ",
-        active_pick,
-        active_watch,
-        active_cached,
-        active_watched,
-        active_drop,
-        active_dropped,
-    )
-    .fetch_all(&app.database)
-    .await
-    .with_context(|| {
-        format!(
-            "Failed to query videos with states: '{}'",
-            states_to_string(allowed_states)
-        )
-    })?;
-
-    let real_videos: Vec<Video> = videos
-        .iter()
-        .map(|base| -> Video {
-            video_from_record! {base}
-        })
-        .collect();
-
-    Ok(real_videos)
-}
-
-pub fn video_info_json(video: &Video) -> Result<Option<InfoJson>> {
-    if let VideoStatus::Cached { mut cache_path, .. } = video.status.clone() {
-        if !cache_path.set_extension("info.json") {
-            bail!(
-                "Failed to change path extension to 'info.json': {}",
-                cache_path.display()
-            );
-        }
-        let info_json_string = File::open(cache_path)?;
-        let info_json: InfoJson = serde_json::from_reader(&info_json_string)?;
-
-        Ok(Some(info_json))
-    } else {
-        Ok(None)
-    }
-}
-
-pub async fn video_by_hash(app: &App, hash: &ExtractorHash) -> Result<Video> {
-    let ehash = hash.hash().to_string();
-
-    let raw_video = query!(
-        "
-        SELECT * FROM videos WHERE extractor_hash = ?;
-        ",
-        ehash
-    )
-    .fetch_one(&app.database)
-    .await?;
-
-    Ok(video_from_record! {raw_video})
-}
-
-pub async fn get_all_hashes(app: &App) -> Result<Vec<Hash>> {
-    let hashes_hex = query!(
-        r#"
-        SELECT extractor_hash
-        FROM videos;
-    "#
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    Ok(hashes_hex
-        .iter()
-        .map(|hash| {
-            Hash::from_hex(&hash.extractor_hash).unreachable(
-                "These values started as blake3 hashes, they should stay blake3 hashes",
-            )
-        })
-        .collect())
-}
-
-pub async fn get_video_hashes(app: &App, subs: &Subscription) -> Result<Vec<Hash>> {
-    let hashes_hex = query!(
-        r#"
-        SELECT extractor_hash
-        FROM videos
-        WHERE parent_subscription_name = ?;
-    "#,
-        subs.name
-    )
-    .fetch_all(&app.database)
-    .await?;
-
-    Ok(hashes_hex
-        .iter()
-        .map(|hash| {
-            Hash::from_hex(&hash.extractor_hash).unreachable(
-                "These values started as blake3 hashes, they should stay blake3 hashes",
-            )
-        })
-        .collect())
-}
-
-pub async fn get_video_yt_dlp_opts(app: &App, hash: &ExtractorHash) -> Result<YtDlpOptions> {
-    let ehash = hash.hash().to_string();
-
-    let yt_dlp_options = query!(
-        r#"
-        SELECT subtitle_langs
-        FROM video_options
-        WHERE extractor_hash = ?;
-    "#,
-        ehash
-    )
-    .fetch_one(&app.database)
-    .await
-    .with_context(|| {
-        format!("Failed to fetch the `yt_dlp_video_opts` for video with hash: '{hash}'",)
-    })?;
-
-    Ok(YtDlpOptions {
-        subtitle_langs: yt_dlp_options.subtitle_langs,
-    })
-}
-pub async fn video_mpv_opts(app: &App, hash: &ExtractorHash) -> Result<MpvOptions> {
-    let ehash = hash.hash().to_string();
-
-    let mpv_options = query!(
-        r#"
-        SELECT playback_speed
-        FROM video_options
-        WHERE extractor_hash = ?;
-    "#,
-        ehash
-    )
-    .fetch_one(&app.database)
-    .await
-    .with_context(|| {
-        format!("Failed to fetch the `mpv_video_opts` for video with hash: '{hash}'")
-    })?;
-
-    Ok(MpvOptions {
-        playback_speed: mpv_options.playback_speed,
-    })
-}
-
-pub async fn get_video_opts(app: &App, hash: &ExtractorHash) -> Result<VideoOptions> {
-    let ehash = hash.hash().to_string();
-
-    let opts = query!(
-        r#"
-        SELECT playback_speed, subtitle_langs
-        FROM video_options
-        WHERE extractor_hash = ?;
-    "#,
-        ehash
-    )
-    .fetch_one(&app.database)
-    .await
-    .with_context(|| format!("Failed to fetch the `video_opts` for video with hash: '{hash}'"))?;
-
-    let mpv = MpvOptions {
-        playback_speed: opts.playback_speed,
-    };
-    let yt_dlp = YtDlpOptions {
-        subtitle_langs: opts.subtitle_langs,
-    };
-
-    Ok(VideoOptions { yt_dlp, mpv })
-}
diff --git a/yt/src/storage/video_database/get/playlist/iterator.rs b/yt/src/storage/video_database/get/playlist/iterator.rs
deleted file mode 100644
index 4c45bf7..0000000
--- a/yt/src/storage/video_database/get/playlist/iterator.rs
+++ /dev/null
@@ -1,101 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{
-    collections::VecDeque,
-    path::{Path, PathBuf},
-};
-
-use crate::storage::video_database::{Video, VideoStatus};
-
-use super::Playlist;
-
-/// Turn a cached video into it's `cache_path`
-fn to_cache_video(video: Video) -> PathBuf {
-    if let VideoStatus::Cached { cache_path, .. } = video.status {
-        cache_path
-    } else {
-        unreachable!("ALl of these videos should be cached.")
-    }
-}
-
-#[derive(Debug)]
-pub struct PlaylistIterator {
-    paths: VecDeque<PathBuf>,
-}
-
-impl Iterator for PlaylistIterator {
-    type Item = <Playlist as IntoIterator>::Item;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        self.paths.pop_front()
-    }
-}
-
-impl DoubleEndedIterator for PlaylistIterator {
-    fn next_back(&mut self) -> Option<Self::Item> {
-        self.paths.pop_back()
-    }
-}
-
-impl IntoIterator for Playlist {
-    type Item = PathBuf;
-
-    type IntoIter = PlaylistIterator;
-
-    fn into_iter(self) -> Self::IntoIter {
-        let paths = self.videos.into_iter().map(to_cache_video).collect();
-        Self::IntoIter { paths }
-    }
-}
-
-#[derive(Debug)]
-pub struct PlaylistIteratorBorrowed<'a> {
-    paths: Vec<&'a Path>,
-    index: usize,
-}
-
-impl<'a> Iterator for PlaylistIteratorBorrowed<'a> {
-    type Item = <&'a Playlist as IntoIterator>::Item;
-
-    fn next(&mut self) -> Option<Self::Item> {
-        let output = self.paths.get(self.index);
-        self.index += 1;
-        output.map(|v| &**v)
-    }
-}
-
-impl<'a> Playlist {
-    #[must_use]
-    pub fn iter(&'a self) -> PlaylistIteratorBorrowed<'a> {
-        <&Self as IntoIterator>::into_iter(self)
-    }
-}
-
-impl<'a> IntoIterator for &'a Playlist {
-    type Item = &'a Path;
-
-    type IntoIter = PlaylistIteratorBorrowed<'a>;
-
-    fn into_iter(self) -> Self::IntoIter {
-        let paths = self
-            .videos
-            .iter()
-            .map(|vid| {
-                if let VideoStatus::Cached { cache_path, .. } = &vid.status {
-                    cache_path.as_path()
-                } else {
-                    unreachable!("ALl of these videos should be cached.")
-                }
-            })
-            .collect();
-        Self::IntoIter { paths, index: 0 }
-    }
-}
diff --git a/yt/src/storage/video_database/get/playlist/mod.rs b/yt/src/storage/video_database/get/playlist/mod.rs
deleted file mode 100644
index f6aadbf..0000000
--- a/yt/src/storage/video_database/get/playlist/mod.rs
+++ /dev/null
@@ -1,167 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! This file contains the getters for the internal playlist
-
-use std::{ops::Add, path::PathBuf};
-
-use crate::{
-    app::App,
-    storage::video_database::{Video, VideoStatusMarker, extractor_hash::ExtractorHash},
-    video_from_record,
-};
-
-use anyhow::Result;
-use sqlx::query;
-
-pub mod iterator;
-
-/// Zero-based index into the internal playlist.
-#[derive(Debug, Clone, Copy)]
-pub struct PlaylistIndex(usize);
-
-impl From<PlaylistIndex> for usize {
-    fn from(value: PlaylistIndex) -> Self {
-        value.0
-    }
-}
-
-impl From<usize> for PlaylistIndex {
-    fn from(value: usize) -> Self {
-        Self(value)
-    }
-}
-
-impl Add<usize> for PlaylistIndex {
-    type Output = Self;
-
-    fn add(self, rhs: usize) -> Self::Output {
-        Self(self.0 + rhs)
-    }
-}
-
-impl Add for PlaylistIndex {
-    type Output = Self;
-
-    fn add(self, rhs: Self) -> Self::Output {
-        Self(self.0 + rhs.0)
-    }
-}
-
-/// A representation of the internal Playlist
-#[derive(Debug)]
-pub struct Playlist {
-    videos: Vec<Video>,
-}
-
-impl Playlist {
-    /// Return the videos of this playlist.
-    #[must_use]
-    pub fn as_videos(&self) -> &[Video] {
-        &self.videos
-    }
-
-    /// Turn this playlist to it's videos
-    #[must_use]
-    pub fn to_videos(self) -> Vec<Video> {
-        self.videos
-    }
-
-    /// Find the index of the video specified by the `video_hash`.
-    ///
-    /// # Panics
-    /// Only if internal assertions fail.
-    #[must_use]
-    pub fn find_index(&self, video_hash: &ExtractorHash) -> Option<PlaylistIndex> {
-        if let Some((index, value)) = self
-            .videos
-            .iter()
-            .enumerate()
-            .find(|(_, other)| other.extractor_hash == *video_hash)
-        {
-            assert_eq!(value.extractor_hash, *video_hash);
-            Some(PlaylistIndex(index))
-        } else {
-            None
-        }
-    }
-
-    /// Select a video based on it's index
-    #[must_use]
-    pub fn get(&self, index: PlaylistIndex) -> Option<&Video> {
-        self.videos.get(index.0)
-    }
-
-    /// Returns the number of videos in the playlist
-    #[must_use]
-    pub fn len(&self) -> usize {
-        self.videos.len()
-    }
-    /// Is the playlist empty?
-    #[must_use]
-    pub fn is_empty(&self) -> bool {
-        self.videos.is_empty()
-    }
-}
-
-/// Return the current playlist index.
-///
-/// This effectively looks for the currently focused video and returns it's index.
-///
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn current_playlist_index(app: &App) -> Result<Option<PlaylistIndex>> {
-    if let Some(focused) = currently_focused_video(app).await? {
-        let playlist = playlist(app).await?;
-        let index = playlist
-            .find_index(&focused.extractor_hash)
-            .expect("All focused videos must also be in the playlist");
-        Ok(Some(index))
-    } else {
-        Ok(None)
-    }
-}
-
-/// Return the video in the playlist at the position `index`.
-pub async fn playlist_entry(app: &App, index: PlaylistIndex) -> Result<Option<Video>> {
-    let playlist = playlist(app).await?;
-
-    if let Some(vid) = playlist.get(index) {
-        Ok(Some(vid.to_owned()))
-    } else {
-        Ok(None)
-    }
-}
-
-pub async fn playlist(app: &App) -> Result<Playlist> {
-    let videos = super::videos(app, &[VideoStatusMarker::Cached]).await?;
-
-    Ok(Playlist { videos })
-}
-
-/// This returns the video with the `is_focused` flag set.
-/// # Panics
-/// Only if assertions fail.
-pub async fn currently_focused_video(app: &App) -> Result<Option<Video>> {
-    let cached_status = VideoStatusMarker::Cached.as_db_integer();
-    let record = query!(
-        "SELECT * FROM videos WHERE is_focused = 1 AND status = ?",
-        cached_status
-    )
-    .fetch_one(&app.database)
-    .await;
-
-    if let Err(sqlx::Error::RowNotFound) = record {
-        Ok(None)
-    } else {
-        let base = record?;
-        Ok(Some(video_from_record! {base}))
-    }
-}
diff --git a/yt/src/storage/video_database/set/mod.rs b/yt/src/storage/video_database/set/mod.rs
deleted file mode 100644
index 4006fde..0000000
--- a/yt/src/storage/video_database/set/mod.rs
+++ /dev/null
@@ -1,341 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-//! These functions change the database. They are added on a demand basis.
-
-use std::path::{Path, PathBuf};
-
-use anyhow::{Context, Result};
-use chrono::Utc;
-use log::{debug, info};
-use sqlx::query;
-use tokio::fs;
-
-use crate::{
-    app::App,
-    storage::video_database::{VideoStatusMarker, extractor_hash::ExtractorHash},
-    video_from_record,
-};
-
-use super::{Priority, Video, VideoOptions, VideoStatus};
-
-mod playlist;
-pub use playlist::*;
-
-/// Set a new status for a video.
-/// This will only update the status time stamp/priority when the status or the priority has changed .
-pub async fn video_status(
-    app: &App,
-    video_hash: &ExtractorHash,
-    new_status: VideoStatus,
-    new_priority: Option<Priority>,
-) -> Result<()> {
-    let video_hash = video_hash.hash().to_string();
-
-    let old = {
-        let base = query!(
-            r#"
-    SELECT *
-    FROM videos
-    WHERE extractor_hash = ?
-    "#,
-            video_hash
-        )
-        .fetch_one(&app.database)
-        .await?;
-
-        video_from_record! {base}
-    };
-
-    let old_marker = old.status.as_marker();
-    let cache_path = {
-        fn cache_path_to_string(path: &Path) -> Result<String> {
-            Ok(path
-                .to_str()
-                .with_context(|| {
-                    format!(
-                        "Failed to parse cache path ('{}') as utf8 string",
-                        path.display()
-                    )
-                })?
-                .to_owned())
-        }
-
-        match (old_marker, &new_status) {
-            (VideoStatusMarker::Cached, VideoStatus::Cached { cache_path, .. }) => {
-                Some(cache_path_to_string(cache_path)?)
-            }
-            (_, VideoStatus::Cached { cache_path, .. }) => Some(cache_path_to_string(cache_path)?),
-
-            (VideoStatusMarker::Cached | _, _) => None,
-        }
-    };
-
-    let new_status = new_status.as_marker();
-
-    if let Some(new_priority) = new_priority {
-        if old_marker == new_status && old.priority == new_priority {
-            return Ok(());
-        }
-
-        let now = Utc::now().timestamp();
-
-        debug!(
-            "Running status change: {:#?} -> {:#?}...",
-            old_marker, new_status,
-        );
-
-        let new_status = new_status.as_db_integer();
-        let new_priority = new_priority.as_db_integer();
-        query!(
-            r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, priority = ?, cache_path = ?
-        WHERE extractor_hash = ?;
-        "#,
-            new_status,
-            now,
-            new_priority,
-            cache_path,
-            video_hash
-        )
-        .execute(&app.database)
-        .await?;
-    } else {
-        if old_marker == new_status {
-            return Ok(());
-        }
-
-        let now = Utc::now().timestamp();
-
-        debug!(
-            "Running status change: {:#?} -> {:#?}...",
-            old_marker, new_status,
-        );
-
-        let new_status = new_status.as_db_integer();
-        query!(
-            r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, cache_path = ?
-        WHERE extractor_hash = ?;
-        "#,
-            new_status,
-            now,
-            cache_path,
-            video_hash
-        )
-        .execute(&app.database)
-        .await?;
-    }
-
-    debug!("Finished status change.");
-    Ok(())
-}
-
-/// Mark a video as watched.
-/// This will both set the status to `Watched` and the `cache_path` to Null.
-///
-/// # Panics
-/// Only if assertions fail.
-pub async fn video_watched(app: &App, video: &ExtractorHash) -> Result<()> {
-    let video_hash = video.hash().to_string();
-    let new_status = VideoStatusMarker::Watched.as_db_integer();
-
-    let old = {
-        let base = query!(
-            r#"
-    SELECT *
-    FROM videos
-    WHERE extractor_hash = ?
-    "#,
-            video_hash
-        )
-        .fetch_one(&app.database)
-        .await?;
-
-        video_from_record! {base}
-    };
-
-    info!("Will set video watched: '{}'", old.title);
-
-    if let VideoStatus::Cached { cache_path, .. } = &old.status {
-        if let Ok(true) = cache_path.try_exists() {
-            fs::remove_file(cache_path).await?;
-        }
-    } else {
-        unreachable!("The video must be marked as Cached before it can be marked Watched");
-    }
-
-    let now = Utc::now().timestamp();
-
-    query!(
-        r#"
-        UPDATE videos
-        SET status = ?, last_status_change = ?, cache_path = NULL
-        WHERE extractor_hash = ?;
-        "#,
-        new_status,
-        now,
-        video_hash
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-pub(crate) async fn video_watch_progress(
-    app: &App,
-    extractor_hash: &ExtractorHash,
-    watch_progress: u32,
-) -> std::result::Result<(), anyhow::Error> {
-    let video_extractor_hash = extractor_hash.hash().to_string();
-
-    query!(
-        r#"
-            UPDATE videos
-            SET watch_progress = ?
-            WHERE extractor_hash = ?;
-        "#,
-        watch_progress,
-        video_extractor_hash,
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-pub async fn set_video_options(
-    app: &App,
-    hash: &ExtractorHash,
-    video_options: &VideoOptions,
-) -> Result<()> {
-    let video_extractor_hash = hash.hash().to_string();
-    let playback_speed = video_options.mpv.playback_speed;
-    let subtitle_langs = &video_options.yt_dlp.subtitle_langs;
-
-    query!(
-        r#"
-            UPDATE video_options
-            SET playback_speed = ?, subtitle_langs = ?
-            WHERE extractor_hash = ?;
-        "#,
-        playback_speed,
-        subtitle_langs,
-        video_extractor_hash,
-    )
-    .execute(&app.database)
-    .await?;
-
-    Ok(())
-}
-
-/// # Panics
-/// Only if internal expectations fail.
-pub async fn add_video(app: &App, video: Video) -> Result<()> {
-    let parent_subscription_name = video.parent_subscription_name;
-
-    let thumbnail_url = video.thumbnail_url.map(|val| val.to_string());
-
-    let url = video.url.to_string();
-    let extractor_hash = video.extractor_hash.hash().to_string();
-
-    let default_subtitle_langs = &app.config.select.subtitle_langs;
-    let default_mpv_playback_speed = app.config.select.playback_speed;
-
-    let status = video.status.as_marker().as_db_integer();
-    let (cache_path, is_focused) = if let VideoStatus::Cached {
-        cache_path,
-        is_focused,
-    } = video.status
-    {
-        (
-            Some(
-                cache_path
-                    .to_str()
-                    .with_context(|| {
-                        format!(
-                            "Failed to prase cache path '{}' as utf-8 string",
-                            cache_path.display()
-                        )
-                    })?
-                    .to_string(),
-            ),
-            is_focused,
-        )
-    } else {
-        (None, false)
-    };
-
-    let duration: Option<f64> = video.duration.as_secs_f64();
-    let last_status_change: i64 = video.last_status_change.as_secs();
-    let publish_date: Option<i64> = video.publish_date.map(|pd| pd.as_secs());
-    let watch_progress: i64 =
-        i64::try_from(video.watch_progress.as_secs()).expect("This should never exceed a u32");
-
-    let mut tx = app.database.begin().await?;
-    query!(
-        r#"
-        INSERT INTO videos (
-            description,
-            duration,
-            extractor_hash,
-            is_focused,
-            last_status_change,
-            parent_subscription_name,
-            publish_date,
-            status,
-            thumbnail_url,
-            title,
-            url,
-            watch_progress,
-            cache_path
-            )
-        VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);
-    "#,
-        video.description,
-        duration,
-        extractor_hash,
-        is_focused,
-        last_status_change,
-        parent_subscription_name,
-        publish_date,
-        status,
-        thumbnail_url,
-        video.title,
-        url,
-        watch_progress,
-        cache_path,
-    )
-    .execute(&mut *tx)
-    .await?;
-
-    query!(
-        r#"
-        INSERT INTO video_options (
-            extractor_hash,
-            subtitle_langs,
-            playback_speed)
-        VALUES (?, ?, ?);
-    "#,
-        extractor_hash,
-        default_subtitle_langs,
-        default_mpv_playback_speed
-    )
-    .execute(&mut *tx)
-    .await?;
-
-    tx.commit().await?;
-
-    Ok(())
-}
diff --git a/yt/src/storage/video_database/set/playlist.rs b/yt/src/storage/video_database/set/playlist.rs
deleted file mode 100644
index 7e97239..0000000
--- a/yt/src/storage/video_database/set/playlist.rs
+++ /dev/null
@@ -1,81 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use anyhow::Result;
-use log::debug;
-use sqlx::query;
-
-use crate::{
-    app::App,
-    storage::video_database::{extractor_hash::ExtractorHash, get},
-};
-
-/// Set a video to be focused.
-/// This optionally takes another video hash, which marks the old focused video.
-/// This will then be disabled.
-///
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn focused(
-    app: &App,
-    new_video_hash: &ExtractorHash,
-    old_video_hash: Option<&ExtractorHash>,
-) -> Result<()> {
-    if let Some(old) = old_video_hash {
-        debug!("Unfocusing video: '{old}'");
-        unfocused(app, old).await?;
-    }
-    debug!("Focusing video: '{new_video_hash}'");
-
-    let new_hash = new_video_hash.hash().to_string();
-    query!(
-        r#"
-            UPDATE videos
-            SET is_focused = 1
-            WHERE extractor_hash = ?;
-        "#,
-        new_hash,
-    )
-    .execute(&app.database)
-    .await?;
-
-    assert_eq!(
-        *new_video_hash,
-        get::currently_focused_video(app)
-            .await?
-            .expect("This is some at this point")
-            .extractor_hash
-    );
-    Ok(())
-}
-
-/// Set a video to be no longer focused.
-///
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn unfocused(app: &App, video_hash: &ExtractorHash) -> Result<()> {
-    let hash = video_hash.hash().to_string();
-    query!(
-        r#"
-            UPDATE videos
-            SET is_focused = 0
-            WHERE extractor_hash = ?;
-        "#,
-        hash
-    )
-    .execute(&app.database)
-    .await?;
-
-    assert!(
-        get::currently_focused_video(app).await?.is_none(),
-        "We assumed that the video we just removed was actually a focused one."
-    );
-    Ok(())
-}
diff --git a/yt/src/subscribe/mod.rs b/yt/src/subscribe/mod.rs
deleted file mode 100644
index 455ccb1..0000000
--- a/yt/src/subscribe/mod.rs
+++ /dev/null
@@ -1,187 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::str::FromStr;
-
-use anyhow::{Context, Result, bail};
-use futures::FutureExt;
-use log::warn;
-use serde_json::{Value, json};
-use tokio::io::{AsyncBufRead, AsyncBufReadExt};
-use url::Url;
-use yt_dlp::wrapper::info_json::InfoType;
-
-use crate::{
-    app::App,
-    storage::subscriptions::{
-        Subscription, add_subscription, check_url, get, remove_all, remove_subscription,
-    },
-    unreachable::Unreachable,
-};
-
-pub async fn unsubscribe(app: &App, name: String) -> Result<()> {
-    let present_subscriptions = get(app).await?;
-
-    if let Some(subscription) = present_subscriptions.0.get(&name) {
-        remove_subscription(app, subscription).await?;
-    } else {
-        bail!("Couldn't find subscription: '{}'", &name);
-    }
-
-    Ok(())
-}
-
-pub async fn import<W: AsyncBufRead + AsyncBufReadExt + Unpin>(
-    app: &App,
-    reader: W,
-    force: bool,
-) -> Result<()> {
-    if force {
-        remove_all(app).await?;
-    }
-
-    let mut lines = reader.lines();
-    while let Some(line) = lines.next_line().await? {
-        let url =
-            Url::from_str(&line).with_context(|| format!("Failed to parse '{line}' as url"))?;
-        match subscribe(app, None, url)
-            .await
-            .with_context(|| format!("Failed to subscribe to: '{line}'"))
-        {
-            Ok(()) => (),
-            Err(err) => eprintln!(
-                "Error while subscribing to '{}': '{}'",
-                line,
-                err.source().unreachable("Should have a source")
-            ),
-        }
-    }
-
-    Ok(())
-}
-
-pub async fn subscribe(app: &App, name: Option<String>, url: Url) -> Result<()> {
-    if !(url.as_str().ends_with("videos")
-        || url.as_str().ends_with("streams")
-        || url.as_str().ends_with("shorts")
-        || url.as_str().ends_with("videos/")
-        || url.as_str().ends_with("streams/")
-        || url.as_str().ends_with("shorts/"))
-        && url.as_str().contains("youtube.com")
-    {
-        warn!(
-            "Your youtbe url does not seem like it actually tracks a channels playlist (videos, streams, shorts). Adding subscriptions for each of them..."
-        );
-
-        let url = Url::parse(&(url.as_str().to_owned() + "/"))
-            .unreachable("This was an url, it should stay one");
-
-        if let Some(name) = name {
-            let out: Result<()> = async move {
-                actual_subscribe(
-                    app,
-                    Some(name.clone() + " {Videos}"),
-                    url.join("videos/")
-                        .unreachable("The url should allow being joined onto"),
-                )
-                .await
-                .with_context(|| {
-                    format!("Failed to subscribe to '{}'", name.clone() + " {Videos}")
-                })?;
-
-                actual_subscribe(
-                    app,
-                    Some(name.clone() + " {Streams}"),
-                    url.join("streams/").unreachable("See above."),
-                )
-                .await
-                .with_context(|| {
-                    format!("Failed to subscribe to '{}'", name.clone() + " {Streams}")
-                })?;
-
-                actual_subscribe(
-                    app,
-                    Some(name.clone() + " {Shorts}"),
-                    url.join("shorts/").unreachable("See above."),
-                )
-                .await
-                .with_context(|| format!("Failed to subscribe to '{}'", name + " {Shorts}"))?;
-
-                Ok(())
-            }
-            .boxed()
-            .await;
-
-            out?;
-        } else {
-            actual_subscribe(app, None, url.join("videos/").unreachable("See above."))
-                .await
-                .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Videos}"))?;
-
-            actual_subscribe(app, None, url.join("streams/").unreachable("See above."))
-                .await
-                .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Streams}"))?;
-
-            actual_subscribe(app, None, url.join("shorts/").unreachable("See above."))
-                .await
-                .with_context(|| format!("Failed to subscribe to the '{}' variant", "{Shorts}"))?;
-        }
-    } else {
-        actual_subscribe(app, name, url).await?;
-    }
-
-    Ok(())
-}
-
-async fn actual_subscribe(app: &App, name: Option<String>, url: Url) -> Result<()> {
-    if !check_url(&url).await? {
-        bail!("The url ('{}') does not represent a playlist!", &url)
-    };
-
-    let name = if let Some(name) = name {
-        name
-    } else {
-        let Value::Object(yt_opts) = json!( {
-            "playliststart": 1,
-            "playlistend": 10,
-            "noplaylist": false,
-            "extract_flat": "in_playlist",
-        }) else {
-            unreachable!("This is hardcoded")
-        };
-
-        let info = yt_dlp::extract_info(&yt_opts, &url, false, false).await?;
-
-        if info._type == Some(InfoType::Playlist) {
-            info.title.expect("This should be some for a playlist")
-        } else {
-            bail!("The url ('{}') does not represent a playlist!", &url)
-        }
-    };
-
-    let present_subscriptions = get(app).await?;
-
-    if let Some(subs) = present_subscriptions.0.get(&name) {
-        bail!(
-            "The subscription '{}' could not be added, \
-                as another one with the same name ('{}') already exists. It links to the Url: '{}'",
-            name,
-            name,
-            subs.url
-        );
-    }
-
-    let sub = Subscription { name, url };
-
-    add_subscription(app, &sub).await?;
-
-    Ok(())
-}
diff --git a/yt/src/unreachable.rs b/yt/src/unreachable.rs
deleted file mode 100644
index 436fbb6..0000000
--- a/yt/src/unreachable.rs
+++ /dev/null
@@ -1,50 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-// This has been taken from: https://gitlab.torproject.org/tpo/core/arti/-/issues/950
-
-// The functions here should be annotated with `#[inline(always)]`.
-#![allow(clippy::inline_always)]
-
-use std::fmt::Debug;
-
-/// Trait for something that can possibly be unwrapped, like a Result or Option.
-/// It provides semantic information, that unwrapping here should never happen.
-pub trait Unreachable<T> {
-    /// Like `expect()`, but does not trigger clippy.
-    ///
-    /// # Usage
-    ///
-    /// This method only exists so that we can use it without hitting
-    /// `clippy::missing_panics_docs`.  Therefore, we should only use it
-    /// for situations where we are certain that the panic cannot occur
-    /// unless something else is very broken.  Consider instead using
-    /// `expect()` and adding a `Panics` section to your function
-    /// documentation.
-    ///
-    /// # Panics
-    ///
-    /// Panics if this is not an object that can be unwrapped, such as
-    /// None or  an Err.
-    fn unreachable(self, msg: &str) -> T;
-}
-impl<T> Unreachable<T> for Option<T> {
-    #[inline(always)]
-    fn unreachable(self, msg: &str) -> T {
-        self.expect(msg)
-    }
-}
-impl<T, E: Debug> Unreachable<T> for Result<T, E> {
-    #[inline(always)]
-    fn unreachable(self, msg: &str) -> T {
-        self.expect(msg)
-    }
-}
diff --git a/yt/src/update/mod.rs b/yt/src/update/mod.rs
deleted file mode 100644
index 7efe0da..0000000
--- a/yt/src/update/mod.rs
+++ /dev/null
@@ -1,196 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{str::FromStr, time::Duration};
-
-use anyhow::{Context, Ok, Result};
-use chrono::{DateTime, Utc};
-use log::{info, warn};
-use url::Url;
-use yt_dlp::{unsmuggle_url, wrapper::info_json::InfoJson};
-
-use crate::{
-    app::App,
-    select::selection_file::duration::MaybeDuration,
-    storage::{
-        subscriptions::{self, Subscription},
-        video_database::{
-            Priority, TimeStamp, Video, VideoStatus, extractor_hash::ExtractorHash,
-            get::get_all_hashes, set::add_video,
-        },
-    },
-};
-
-mod updater;
-use updater::Updater;
-
-pub async fn update(
-    app: &App,
-    max_backlog: usize,
-    subscription_names_to_update: Vec<String>,
-) -> Result<()> {
-    let subscriptions = subscriptions::get(app).await?;
-
-    let urls: Vec<_> = if subscription_names_to_update.is_empty() {
-        subscriptions.0.values().collect()
-    } else {
-        subscriptions
-            .0
-            .values()
-            .filter(|sub| {
-                if subscription_names_to_update.contains(&sub.name) {
-                    true
-                } else {
-                    info!(
-                        "Not updating subscription '{}' as it was not specified",
-                        sub.name
-                    );
-                    false
-                }
-            })
-            .collect()
-    };
-
-    // We can get away with not having to re-fetch the hashes every time, as the returned video
-    // should not contain duplicates.
-    let hashes = get_all_hashes(app).await?;
-
-    {
-        let mut updater = Updater::new(max_backlog, &hashes);
-        updater.update(app, &urls).await?;
-    }
-
-    Ok(())
-}
-
-#[allow(clippy::too_many_lines)]
-pub fn video_entry_to_video(entry: InfoJson, sub: Option<&Subscription>) -> Result<Video> {
-    macro_rules! unwrap_option {
-        ($option:expr) => {
-            match $option {
-                Some(x) => x,
-                None => anyhow::bail!(concat!(
-                    "Expected a value, but '",
-                    stringify!($option),
-                    "' is None!"
-                )),
-            }
-        };
-    }
-    fn fmt_context(date: &str, extended: Option<&str>) -> String {
-        let f = format!(
-            "Failed to parse the `upload_date` of the entry ('{date}'). \
-                    Expected `YYYY-MM-DD`, has the format changed?"
-        );
-        if let Some(date_string) = extended {
-            format!("{f}\nThe parsed '{date_string}' can't be turned to a valid UTC date.'")
-        } else {
-            f
-        }
-    }
-
-    let publish_date = if let Some(date) = &entry.upload_date {
-        let year: u32 = date
-            .chars()
-            .take(4)
-            .collect::<String>()
-            .parse()
-            .with_context(|| fmt_context(date, None))?;
-        let month: u32 = date
-            .chars()
-            .skip(4)
-            .take(2)
-            .collect::<String>()
-            .parse()
-            .with_context(|| fmt_context(date, None))?;
-        let day: u32 = date
-            .chars()
-            .skip(6)
-            .take(2)
-            .collect::<String>()
-            .parse()
-            .with_context(|| fmt_context(date, None))?;
-
-        let date_string = format!("{year:04}-{month:02}-{day:02}T00:00:00Z");
-        Some(
-            DateTime::<Utc>::from_str(&date_string)
-                .with_context(|| fmt_context(date, Some(&date_string)))?
-                .timestamp(),
-        )
-    } else {
-        warn!(
-            "The video '{}' lacks it's upload date!",
-            unwrap_option!(&entry.title)
-        );
-        None
-    };
-
-    let thumbnail_url = match (&entry.thumbnails, &entry.thumbnail) {
-        (None, None) => None,
-        (None, Some(thumbnail)) => Some(thumbnail.to_owned()),
-
-        // TODO: The algorithm is not exactly the best <2024-05-28>
-        (Some(thumbnails), None) => thumbnails.first().map(|thumbnail| thumbnail.url.clone()),
-        (Some(_), Some(thumnail)) => Some(thumnail.to_owned()),
-    };
-
-    let url = {
-        let smug_url: Url = unwrap_option!(entry.webpage_url.clone());
-        unsmuggle_url(&smug_url)?
-    };
-
-    let extractor_hash = blake3::hash(unwrap_option!(entry.id).as_bytes());
-
-    let subscription_name = if let Some(sub) = sub {
-        Some(sub.name.clone())
-    } else if let Some(uploader) = entry.uploader {
-        if entry.webpage_url_domain == Some("youtube.com".to_owned()) {
-            Some(format!("{uploader} - Videos"))
-        } else {
-            Some(uploader.clone())
-        }
-    } else {
-        None
-    };
-
-    let video = Video {
-        description: entry.description.clone(),
-        duration: MaybeDuration::from_maybe_secs_f64(entry.duration),
-        extractor_hash: ExtractorHash::from_hash(extractor_hash),
-        last_status_change: TimeStamp::from_now(),
-        parent_subscription_name: subscription_name,
-        priority: Priority::default(),
-        publish_date: publish_date.map(TimeStamp::from_secs),
-        status: VideoStatus::Pick,
-        thumbnail_url,
-        title: unwrap_option!(entry.title.clone()),
-        url,
-        watch_progress: Duration::default(),
-    };
-    Ok(video)
-}
-
-async fn process_subscription(app: &App, sub: &Subscription, entry: InfoJson) -> Result<()> {
-    let video =
-        video_entry_to_video(entry, Some(sub)).context("Failed to parse search entry as Video")?;
-
-    add_video(app, video.clone())
-        .await
-        .with_context(|| format!("Failed to add video to database: '{}'", video.title))?;
-    println!(
-        "{}",
-        &video
-            .to_line_display(app)
-            .await
-            .with_context(|| format!("Failed to format video: '{}'", video.title))?
-    );
-    Ok(())
-}
diff --git a/yt/src/update/updater.rs b/yt/src/update/updater.rs
deleted file mode 100644
index 2b13378..0000000
--- a/yt/src/update/updater.rs
+++ /dev/null
@@ -1,171 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::io::{Write, stderr};
-
-use anyhow::{Context, Result};
-use blake3::Hash;
-use futures::{
-    StreamExt, TryStreamExt,
-    stream::{self},
-};
-use log::{Level, debug, error, log_enabled};
-use serde_json::json;
-use yt_dlp::{error::YtDlpError, process_ie_result, wrapper::info_json::InfoJson};
-
-use crate::{app::App, storage::subscriptions::Subscription};
-
-use super::process_subscription;
-
-pub(super) struct Updater<'a> {
-    max_backlog: usize,
-    hashes: &'a [Hash],
-}
-
-impl<'a> Updater<'a> {
-    pub(super) fn new(max_backlog: usize, hashes: &'a [Hash]) -> Self {
-        Self {
-            max_backlog,
-            hashes,
-        }
-    }
-
-    pub(super) async fn update(
-        &mut self,
-        app: &App,
-        subscriptions: &[&Subscription],
-    ) -> Result<()> {
-        let mut stream = stream::iter(subscriptions)
-            .map(|sub| self.get_new_entries(sub))
-            .buffer_unordered(100);
-
-        while let Some(output) = stream.next().await {
-            let mut entries = output?;
-
-            if entries.is_empty() {
-                continue;
-            }
-
-            let (sub, entry) = entries.remove(0);
-            process_subscription(app, sub, entry).await?;
-
-            let entry_stream: Result<()> = stream::iter(entries)
-                .map(|(sub, entry)| process_subscription(app, sub, entry))
-                .buffer_unordered(100)
-                .try_collect()
-                .await;
-            entry_stream?;
-        }
-
-        Ok(())
-    }
-
-    async fn get_new_entries(
-        &self,
-        sub: &'a Subscription,
-    ) -> Result<Vec<(&'a Subscription, InfoJson)>> {
-        // ANSI ESCAPE CODES Wrappers {{{
-        // see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
-        const CSI: &str = "\x1b[";
-        fn clear_whole_line() {
-            eprint!("{CSI}2K");
-        }
-        fn move_to_col(x: usize) {
-            eprint!("{CSI}{x}G");
-        }
-        // fn hide_cursor() {
-        //     eprint!("{CSI}?25l");
-        // }
-        // fn show_cursor() {
-        //     eprint!("{CSI}?25h");
-        // }
-        // }}}
-
-        let json = json! {
-            {
-                "playliststart": 1,
-                "playlistend": self.max_backlog,
-                "noplaylist": false,
-                "extractor_args": {"youtubetab": {"approximate_date": [""]}},
-            }
-        };
-        let yt_dlp_opts = json.as_object().expect("This is hardcoded");
-
-        if !log_enabled!(Level::Debug) {
-            clear_whole_line();
-            move_to_col(1);
-            eprint!("Checking playlist {}...", sub.name);
-            move_to_col(1);
-            stderr().flush()?;
-        }
-
-        let info = yt_dlp::extract_info(yt_dlp_opts, &sub.url, false, false)
-            .await
-            .with_context(|| format!("Failed to get playlist '{}'.", sub.name))?;
-
-        let entries = info.entries.unwrap_or(vec![]);
-        let valid_entries: Vec<(&Subscription, InfoJson)> = entries
-            .into_iter()
-            .take(self.max_backlog)
-            .filter_map(|entry| -> Option<(&Subscription, InfoJson)> {
-                let id = entry.id.as_ref().expect("Should exist?");
-                let extractor_hash = blake3::hash(id.as_bytes());
-                if self.hashes.contains(&extractor_hash) {
-                    debug!(
-                        "Skipping entry, as it is already present: '{}'",
-                        extractor_hash
-                    );
-                    None
-                } else {
-                    Some((sub, entry))
-                }
-            })
-            .collect();
-
-        let processed_entries = {
-            let base: Result<Vec<(&Subscription, InfoJson)>, YtDlpError> =
-                stream::iter(valid_entries)
-                    .map(|(sub, entry)| async move {
-                        match process_ie_result(yt_dlp_opts, entry, false).await {
-                            Ok(output) => Ok((sub, output)),
-                            Err(err) => Err(err),
-                        }
-                    })
-                    .buffer_unordered(100)
-                    .try_collect()
-                    .await;
-            match base {
-                Ok(ok) => ok,
-                Err(err) => {
-                    if let YtDlpError::PythonError { error, kind } = &err {
-                        if kind.as_str() == "<class 'yt_dlp.utils.DownloadError'>"
-                            && error.to_string().as_str().contains(
-                                "Join this channel to get access to members-only content ",
-                            )
-                        {
-                            vec![]
-                        } else {
-                            let error_string = error.to_string();
-                            let error = error_string
-                                .strip_prefix("DownloadError: \u{1b}[0;31mERROR:\u{1b}[0m ")
-                                .expect("This prefix should exists");
-                            error!("{error}");
-                            vec![]
-                        }
-                    } else {
-                        Err(err).context("Failed to process new entries.")?
-                    }
-                }
-            }
-        };
-
-        Ok(processed_entries)
-    }
-}
diff --git a/yt/src/videos/display/format_video.rs b/yt/src/videos/display/format_video.rs
deleted file mode 100644
index b97acb1..0000000
--- a/yt/src/videos/display/format_video.rs
+++ /dev/null
@@ -1,94 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use anyhow::Result;
-
-use crate::{app::App, comments::output::format_text, storage::video_database::Video};
-
-impl Video {
-    pub async fn to_info_display(&self, app: &App) -> Result<String> {
-        let cache_path = self.cache_path_fmt(app);
-        let description = self.description_fmt();
-        let duration = self.duration_fmt(app);
-        let extractor_hash = self.extractor_hash_fmt(app).await?;
-        let in_playlist = self.in_playlist_fmt(app);
-        let last_status_change = self.last_status_change_fmt(app);
-        let parent_subscription_name = self.parent_subscription_name_fmt(app);
-        let priority = self.priority_fmt();
-        let publish_date = self.publish_date_fmt(app);
-        let status = self.status_fmt(app);
-        let thumbnail_url = self.thumbnail_url_fmt();
-        let title = self.title_fmt(app);
-        let url = self.url_fmt(app);
-        let watch_progress = self.watch_progress_fmt(app);
-        let video_options = self.video_options_fmt(app).await?;
-
-        let watched_percentage_fmt = {
-            if let Some(duration) = self.duration.as_secs() {
-                format!(
-                    " (watched: {:0.0}%)",
-                    (self.watch_progress.as_secs() / duration) * 100
-                )
-            } else {
-                format!(" {watch_progress}")
-            }
-        };
-
-        let string = format!(
-            "\
-{title} ({extractor_hash})
-| -> {cache_path}
-| -> {duration}{watched_percentage_fmt}
-| -> {parent_subscription_name}
-| -> priority: {priority}
-| -> {publish_date}
-| -> status: {status} since {last_status_change} ({in_playlist})
-| -> {thumbnail_url}
-| -> {url}
-| -> options: {}
-{}\n",
-            video_options.to_string().trim(),
-            format_text(description.to_string().as_str())
-        );
-        Ok(string)
-    }
-
-    pub async fn to_line_display(&self, app: &App) -> Result<String> {
-        let f = format!(
-            "{} {} {} {} {} {}",
-            self.status_fmt(app),
-            self.extractor_hash_fmt(app).await?,
-            self.title_fmt(app),
-            self.publish_date_fmt(app),
-            self.parent_subscription_name_fmt(app),
-            self.duration_fmt(app)
-        );
-
-        Ok(f)
-    }
-
-    pub async fn to_select_file_display(&self, app: &App) -> Result<String> {
-        let f = format!(
-            r#"{}{} {} "{}" "{}" "{}" "{}" "{}"{}"#,
-            self.status_fmt_no_color(),
-            self.video_options_fmt_no_color(app).await?,
-            self.extractor_hash_fmt_no_color(app).await?,
-            self.title_fmt_no_color(),
-            self.publish_date_fmt_no_color(),
-            self.parent_subscription_name_fmt_no_color(),
-            self.duration_fmt_no_color(),
-            self.url_fmt_no_color(),
-            '\n'
-        );
-
-        Ok(f)
-    }
-}
diff --git a/yt/src/videos/display/mod.rs b/yt/src/videos/display/mod.rs
deleted file mode 100644
index 1188569..0000000
--- a/yt/src/videos/display/mod.rs
+++ /dev/null
@@ -1,229 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use owo_colors::OwoColorize;
-use url::Url;
-
-use crate::{
-    app::App,
-    select::selection_file::duration::MaybeDuration,
-    storage::video_database::{TimeStamp, Video, VideoStatus, get::get_video_opts},
-};
-
-use anyhow::{Context, Result};
-
-pub mod format_video;
-
-macro_rules! get {
-    ($value:expr, $key:ident, $name:expr, $code:tt) => {
-        if let Some(value) = &$value.$key {
-            $code(value)
-        } else {
-            concat!("[No ", $name, "]").to_owned()
-        }
-    };
-}
-
-fn maybe_add_color<F>(app: &App, input: String, mut color_fn: F) -> String
-where
-    F: FnMut(String) -> String,
-{
-    if app.config.global.display_colors {
-        color_fn(input)
-    } else {
-        input
-    }
-}
-impl Video {
-    #[must_use]
-    pub fn cache_path_fmt(&self, app: &App) -> String {
-        let cache_path = if let VideoStatus::Cached {
-            cache_path,
-            is_focused: _,
-        } = &self.status
-        {
-            cache_path.to_string_lossy().to_string()
-        } else {
-            "[No Cache Path]".to_owned()
-        };
-        maybe_add_color(app, cache_path, |v| v.blue().bold().to_string())
-    }
-
-    #[must_use]
-    pub fn description_fmt(&self) -> String {
-        get!(
-            self,
-            description,
-            "Description",
-            (|value: &str| value.to_owned())
-        )
-    }
-
-    #[must_use]
-    pub fn duration_fmt_no_color(&self) -> String {
-        self.duration.to_string()
-    }
-    #[must_use]
-    pub fn duration_fmt(&self, app: &App) -> String {
-        let duration = self.duration_fmt_no_color();
-        maybe_add_color(app, duration, |v| v.cyan().bold().to_string())
-    }
-
-    #[must_use]
-    pub fn watch_progress_fmt(&self, app: &App) -> String {
-        maybe_add_color(
-            app,
-            MaybeDuration::from_std(self.watch_progress).to_string(),
-            |v| v.cyan().bold().to_string(),
-        )
-    }
-
-    pub async fn extractor_hash_fmt_no_color(&self, app: &App) -> Result<String> {
-        let hash = self
-            .extractor_hash
-            .into_short_hash(app)
-            .await
-            .with_context(|| {
-                format!(
-                    "Failed to format extractor hash, whilst formatting video: '{}'",
-                    self.title
-                )
-            })?
-            .to_string();
-        Ok(hash)
-    }
-    pub async fn extractor_hash_fmt(&self, app: &App) -> Result<String> {
-        let hash = self.extractor_hash_fmt_no_color(app).await?;
-        Ok(maybe_add_color(app, hash, |v| {
-            v.bright_purple().italic().to_string()
-        }))
-    }
-
-    #[must_use]
-    pub fn in_playlist_fmt(&self, app: &App) -> String {
-        let output = match &self.status {
-            VideoStatus::Pick
-            | VideoStatus::Watch
-            | VideoStatus::Watched
-            | VideoStatus::Drop
-            | VideoStatus::Dropped => "Not in the playlist",
-            VideoStatus::Cached { is_focused, .. } => {
-                if *is_focused {
-                    "In the playlist and focused"
-                } else {
-                    "In the playlist"
-                }
-            }
-        };
-        maybe_add_color(app, output.to_owned(), |v| v.yellow().italic().to_string())
-    }
-    #[must_use]
-    pub fn last_status_change_fmt(&self, app: &App) -> String {
-        maybe_add_color(app, self.last_status_change.to_string(), |v| {
-            v.bright_cyan().to_string()
-        })
-    }
-
-    #[must_use]
-    pub fn parent_subscription_name_fmt_no_color(&self) -> String {
-        get!(
-            self,
-            parent_subscription_name,
-            "author",
-            (|sub: &str| sub.replace('"', "'"))
-        )
-    }
-    #[must_use]
-    pub fn parent_subscription_name_fmt(&self, app: &App) -> String {
-        let psn = self.parent_subscription_name_fmt_no_color();
-        maybe_add_color(app, psn, |v| v.bright_magenta().to_string())
-    }
-
-    #[must_use]
-    pub fn priority_fmt(&self) -> String {
-        self.priority.to_string()
-    }
-
-    #[must_use]
-    pub fn publish_date_fmt_no_color(&self) -> String {
-        get!(
-            self,
-            publish_date,
-            "release date",
-            (|date: &TimeStamp| date.to_string())
-        )
-    }
-    #[must_use]
-    pub fn publish_date_fmt(&self, app: &App) -> String {
-        let date = self.publish_date_fmt_no_color();
-        maybe_add_color(app, date, |v| v.bright_white().bold().to_string())
-    }
-
-    #[must_use]
-    pub fn status_fmt_no_color(&self) -> String {
-        // TODO: We might support `.trim()`ing that, as the extra whitespace could be bad in the
-        // selection file. <2024-10-07>
-        self.status.as_marker().as_command().to_string()
-    }
-    #[must_use]
-    pub fn status_fmt(&self, app: &App) -> String {
-        let status = self.status_fmt_no_color();
-        maybe_add_color(app, status, |v| v.red().bold().to_string())
-    }
-
-    #[must_use]
-    pub fn thumbnail_url_fmt(&self) -> String {
-        get!(
-            self,
-            thumbnail_url,
-            "thumbnail URL",
-            (|url: &Url| url.to_string())
-        )
-    }
-
-    #[must_use]
-    pub fn title_fmt_no_color(&self) -> String {
-        self.title.replace(['"', '„', '”', '“'], "'")
-    }
-    #[must_use]
-    pub fn title_fmt(&self, app: &App) -> String {
-        let title = self.title_fmt_no_color();
-        maybe_add_color(app, title, |v| v.green().bold().to_string())
-    }
-
-    #[must_use]
-    pub fn url_fmt_no_color(&self) -> String {
-        self.url.as_str().replace('"', "\\\"")
-    }
-    #[must_use]
-    pub fn url_fmt(&self, app: &App) -> String {
-        let url = self.url_fmt_no_color();
-        maybe_add_color(app, url, |v| v.italic().to_string())
-    }
-
-    pub async fn video_options_fmt_no_color(&self, app: &App) -> Result<String> {
-        let video_options = {
-            let opts = get_video_opts(app, &self.extractor_hash)
-                .await
-                .with_context(|| {
-                    format!("Failed to get video options for video: '{}'", self.title)
-                })?
-                .to_cli_flags(app);
-            let opts_white = if opts.is_empty() { "" } else { " " };
-            format!("{opts_white}{opts}")
-        };
-        Ok(video_options)
-    }
-    pub async fn video_options_fmt(&self, app: &App) -> Result<String> {
-        let opts = self.video_options_fmt_no_color(app).await?;
-        Ok(maybe_add_color(app, opts, |v| v.bright_green().to_string()))
-    }
-}
diff --git a/yt/src/videos/mod.rs b/yt/src/videos/mod.rs
deleted file mode 100644
index e821772..0000000
--- a/yt/src/videos/mod.rs
+++ /dev/null
@@ -1,67 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use anyhow::Result;
-use futures::{TryStreamExt, stream::FuturesUnordered};
-use nucleo_matcher::{
-    Matcher,
-    pattern::{CaseMatching, Normalization, Pattern},
-};
-
-pub mod display;
-
-use crate::{
-    app::App,
-    storage::video_database::{Video, VideoStatusMarker, get},
-};
-
-async fn to_line_display_owned(video: Video, app: &App) -> Result<String> {
-    video.to_line_display(app).await
-}
-
-pub async fn query(app: &App, limit: Option<usize>, search_query: Option<String>) -> Result<()> {
-    let all_videos = get::videos(app, VideoStatusMarker::ALL).await?;
-
-    // turn one video to a color display, to pre-warm the hash shrinking cache
-    if let Some(val) = all_videos.first() {
-        val.to_line_display(app).await?;
-    }
-
-    let limit = limit.unwrap_or(all_videos.len());
-
-    let all_video_strings: Vec<String> = all_videos
-        .into_iter()
-        .take(limit)
-        .map(|vid| to_line_display_owned(vid, app))
-        .collect::<FuturesUnordered<_>>()
-        .try_collect::<Vec<String>>()
-        .await?;
-
-    if let Some(query) = search_query {
-        let mut matcher = Matcher::new(nucleo_matcher::Config::DEFAULT.match_paths());
-
-        let pattern_matches = Pattern::parse(
-            &query.replace(' ', "\\ "),
-            CaseMatching::Ignore,
-            Normalization::Smart,
-        )
-        .match_list(all_video_strings, &mut matcher);
-
-        pattern_matches
-            .iter()
-            .rev()
-            .for_each(|(val, key)| println!("{val} ({key})"));
-    } else {
-        println!("{}", all_video_strings.join("\n"));
-    }
-
-    Ok(())
-}
diff --git a/yt/src/watch/mod.rs b/yt/src/watch/mod.rs
deleted file mode 100644
index 6827b2c..0000000
--- a/yt/src/watch/mod.rs
+++ /dev/null
@@ -1,172 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2024 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{
-    sync::{
-        Arc,
-        atomic::{AtomicBool, Ordering},
-    },
-    time::Duration,
-};
-
-use anyhow::{Context, Result};
-use libmpv2::{Mpv, events::EventContext};
-use log::{debug, info, trace, warn};
-use playlist_handler::{reload_mpv_playlist, save_watch_progress};
-use tokio::{task, time::sleep};
-
-use self::playlist_handler::Status;
-use crate::{
-    app::App,
-    cache::maintain,
-    storage::video_database::{get, notify::wait_for_db_write},
-};
-
-pub mod playlist;
-pub mod playlist_handler;
-
-fn init_mpv(app: &App) -> Result<(Mpv, EventContext)> {
-    // set some default values, to make things easier (these can be overridden by the config file,
-    // which we load later)
-    let mpv = Mpv::with_initializer(|mpv| {
-        // Enable default key bindings, so the user can actually interact with
-        // the player (and e.g. close the window).
-        mpv.set_property("input-default-bindings", "yes")?;
-        mpv.set_property("input-vo-keyboard", "yes")?;
-
-        // Show the on screen controller.
-        mpv.set_property("osc", "yes")?;
-
-        // Don't automatically advance to the next video (or exit the player)
-        mpv.set_option("keep-open", "always")?;
-
-        // Always display an window, even for non-video playback.
-        // As mpv does not have cli access, no window means no control and no user feedback.
-        mpv.set_option("force-window", "yes")?;
-        Ok(())
-    })
-    .context("Failed to initialize mpv")?;
-
-    let config_path = &app.config.paths.mpv_config_path;
-    if config_path.try_exists()? {
-        info!("Found mpv.conf at '{}'!", config_path.display());
-        mpv.command("load-config-file", &[config_path
-            .to_str()
-            .context("Failed to parse the config path is utf8-stringt")?])?;
-    } else {
-        warn!(
-            "Did not find a mpv.conf file at '{}'",
-            config_path.display()
-        );
-    }
-
-    let input_path = &app.config.paths.mpv_input_path;
-    if input_path.try_exists()? {
-        info!("Found mpv.input.conf at '{}'!", input_path.display());
-        mpv.command("load-input-conf", &[input_path
-            .to_str()
-            .context("Failed to parse the input path as utf8 string")?])?;
-    } else {
-        warn!(
-            "Did not find a mpv.input.conf file at '{}'",
-            input_path.display()
-        );
-    }
-
-    let ev_ctx = EventContext::new(mpv.ctx);
-    ev_ctx.disable_deprecated_events()?;
-
-    Ok((mpv, ev_ctx))
-}
-
-pub async fn watch(app: Arc<App>) -> Result<()> {
-    maintain(&app, false).await?;
-
-    let (mpv, mut ev_ctx) = init_mpv(&app).context("Failed to initialize mpv instance")?;
-    let mpv = Arc::new(mpv);
-    reload_mpv_playlist(&app, &mpv, None, None).await?;
-
-    let should_break = Arc::new(AtomicBool::new(false));
-
-    let local_app = Arc::clone(&app);
-    let local_mpv = Arc::clone(&mpv);
-    let local_should_break = Arc::clone(&should_break);
-    let progress_handle = task::spawn(async move {
-        loop {
-            if local_should_break.load(Ordering::Relaxed) {
-                break;
-            }
-
-            if get::currently_focused_video(&local_app).await?.is_some() {
-                save_watch_progress(&local_app, &local_mpv).await?;
-            }
-
-            sleep(Duration::from_secs(30)).await;
-        }
-
-        Ok::<(), anyhow::Error>(())
-    });
-
-    let mut have_warned = (false, 0);
-    'watchloop: loop {
-        'waitloop: while let Ok(value) = playlist_handler::status(&app).await {
-            match value {
-                Status::NoMoreAvailable => {
-                    break 'watchloop;
-                }
-                Status::NoCached { marked_watch } => {
-                    // try again next time.
-                    if have_warned.0 {
-                        if have_warned.1 != marked_watch {
-                            warn!("Now {} videos are marked as to be watched.", marked_watch);
-                            have_warned.1 = marked_watch;
-                        }
-                    } else {
-                        warn!(
-                            "There is nothing to watch yet, but still {} videos marked as to be watched. \
-                        Will idle, until they become available",
-                            marked_watch
-                        );
-                        have_warned = (true, marked_watch);
-                    }
-                    wait_for_db_write(&app).await?;
-                }
-                Status::Available { newly_available } => {
-                    debug!("Check and found {newly_available} videos!");
-                    have_warned.0 = false;
-
-                    // Something just became available!
-                    break 'waitloop;
-                }
-            }
-        }
-
-        if let Some(ev) = ev_ctx.wait_event(30.) {
-            match ev {
-                Ok(event) => {
-                    trace!("Mpv event triggered: {:#?}", event);
-                    if playlist_handler::handle_mpv_event(&app, &mpv, &event)
-                        .await
-                        .with_context(|| format!("Failed to handle mpv event: '{event:#?}'"))?
-                    {
-                        break;
-                    }
-                }
-                Err(e) => debug!("Mpv Event errored: {}", e),
-            }
-        }
-    }
-
-    should_break.store(true, Ordering::Relaxed);
-    progress_handle.await??;
-
-    Ok(())
-}
diff --git a/yt/src/watch/playlist.rs b/yt/src/watch/playlist.rs
deleted file mode 100644
index 6ac8b12..0000000
--- a/yt/src/watch/playlist.rs
+++ /dev/null
@@ -1,109 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::path::Path;
-
-use crate::{
-    app::App,
-    storage::video_database::{Video, VideoStatus, get, notify::wait_for_db_write},
-};
-
-use anyhow::Result;
-use futures::{TryStreamExt, stream::FuturesOrdered};
-
-/// Extract the values of the [`VideoStatus::Cached`] value from a Video.
-fn cache_values(video: &Video) -> (&Path, bool) {
-    if let VideoStatus::Cached {
-        cache_path,
-        is_focused,
-    } = &video.status
-    {
-        (cache_path, *is_focused)
-    } else {
-        unreachable!("All of these videos should be cached");
-    }
-}
-
-// ANSI ESCAPE CODES Wrappers {{{
-// see: https://en.wikipedia.org/wiki/ANSI_escape_code#Control_Sequence_Introducer_commands
-const CSI: &str = "\x1b[";
-fn erase_in_display_from_cursor() {
-    print!("{CSI}0J");
-}
-fn cursor_up(number: usize) {
-    print!("{CSI}{number}A");
-}
-// }}}
-
-/// # Panics
-/// Only if internal assertions fail.
-pub async fn playlist(app: &App, watch: bool) -> Result<()> {
-    let mut previous_output_length = 0;
-    loop {
-        let playlist = get::playlist(app).await?.to_videos();
-
-        let output = playlist
-            .into_iter()
-            .map(|video| async move {
-                let mut output = String::new();
-
-                let (_, is_focused) = cache_values(&video);
-
-                if is_focused {
-                    output.push_str("🔻 ");
-                } else {
-                    output.push_str("  ");
-                }
-
-                output.push_str(&video.title_fmt(app));
-
-                output.push_str(" (");
-                output.push_str(&video.parent_subscription_name_fmt(app));
-                output.push(')');
-
-                output.push_str(" [");
-                output.push_str(&video.duration_fmt(app));
-
-                if is_focused {
-                    output.push_str(" (");
-                    output.push_str(&if let Some(duration) = video.duration.as_secs() {
-                        format!("{:0.0}%", (video.watch_progress.as_secs() / duration) * 100)
-                    } else {
-                        video.watch_progress_fmt(app)
-                    });
-                    output.push(')');
-                }
-                output.push(']');
-
-                output.push('\n');
-
-                Ok::<String, anyhow::Error>(output)
-            })
-            .collect::<FuturesOrdered<_>>()
-            .try_collect::<String>()
-            .await?;
-
-        // Delete the previous output
-        cursor_up(previous_output_length);
-        erase_in_display_from_cursor();
-
-        previous_output_length = output.chars().filter(|ch| *ch == '\n').count();
-
-        print!("{output}");
-
-        if !watch {
-            break;
-        }
-
-        wait_for_db_write(app).await?;
-    }
-
-    Ok(())
-}
diff --git a/yt/src/watch/playlist_handler/mod.rs b/yt/src/watch/playlist_handler/mod.rs
deleted file mode 100644
index 2672ff5..0000000
--- a/yt/src/watch/playlist_handler/mod.rs
+++ /dev/null
@@ -1,339 +0,0 @@
-// yt - A fully featured command line YouTube client
-//
-// Copyright (C) 2025 Benedikt Peetz <benedikt.peetz@b-peetz.de>
-// SPDX-License-Identifier: GPL-3.0-or-later
-//
-// This file is part of Yt.
-//
-// You should have received a copy of the License along with this program.
-// If not, see <https://www.gnu.org/licenses/gpl-3.0.txt>.
-
-use std::{cmp::Ordering, time::Duration};
-
-use crate::{
-    app::App,
-    storage::video_database::{
-        VideoStatus, VideoStatusMarker,
-        extractor_hash::ExtractorHash,
-        get::{self, Playlist, PlaylistIndex},
-        set,
-    },
-};
-
-use anyhow::{Context, Result};
-use libmpv2::{EndFileReason, Mpv, events::Event};
-use log::{debug, info};
-
-mod client_messages;
-
-#[derive(Debug, Clone, Copy)]
-pub enum Status {
-    /// There are no videos cached and no more marked to be watched.
-    /// Waiting is pointless.
-    NoMoreAvailable,
-
-    /// There are no videos cached, but some (> 0) are marked to be watched.
-    /// So we should wait for them to become available.
-    NoCached { marked_watch: usize },
-
-    /// There are videos cached and ready to be inserted into the playback queue.
-    Available { newly_available: usize },
-}
-
-fn mpv_message(mpv: &Mpv, message: &str, time: Duration) -> Result<()> {
-    mpv.command("show-text", &[
-        message,
-        time.as_millis().to_string().as_str(),
-    ])?;
-    Ok(())
-}
-
-async fn apply_video_options(app: &App, mpv: &Mpv, video: &ExtractorHash) -> Result<()> {
-    let options = get::video_mpv_opts(app, video).await?;
-    let video = get::video_by_hash(app, video).await?;
-
-    mpv.set_property("speed", options.playback_speed)?;
-
-    // We already start at 0, so setting it twice adds a uncomfortable skip sound.
-    if video.watch_progress.as_secs() != 0 {
-        mpv.set_property(
-            "time-pos",
-            i64::try_from(video.watch_progress.as_secs()).expect("This should not overflow"),
-        )?;
-    }
-    Ok(())
-}
-
-async fn mark_video_watched(app: &App, mpv: &Mpv) -> Result<()> {
-    let current_video = get::currently_focused_video(app)
-        .await?
-        .expect("This should be some at this point");
-
-    debug!(
-        "playlist handler will mark video '{}' watched.",
-        current_video.title
-    );
-
-    save_watch_progress(app, mpv).await?;
-
-    set::video_watched(app, &current_video.extractor_hash).await?;
-
-    Ok(())
-}
-
-/// Saves the `watch_progress` of the currently focused video.
-pub(super) async fn save_watch_progress(app: &App, mpv: &Mpv) -> Result<()> {
-    let current_video = get::currently_focused_video(app)
-        .await?
-        .expect("This should be some at this point");
-    let watch_progress = u32::try_from(
-        mpv.get_property::<i64>("time-pos")
-            .context("Failed to get the watchprogress of the currently playling video")?,
-    )
-    .expect("This conversion should never fail as the `time-pos` property is positive");
-
-    debug!(
-        "Setting the watch progress for the current_video '{}' to {watch_progress}s",
-        current_video.title_fmt_no_color()
-    );
-
-    set::video_watch_progress(app, &current_video.extractor_hash, watch_progress).await
-}
-
-/// Sync the mpv playlist with the internal playlist.
-///
-/// This takes an `maybe_playlist` argument, if you have already fetched the playlist and want to
-/// add that.
-pub(super) async fn reload_mpv_playlist(
-    app: &App,
-    mpv: &Mpv,
-    maybe_playlist: Option<Playlist>,
-    maybe_index: Option<PlaylistIndex>,
-) -> Result<()> {
-    fn get_playlist_count(mpv: &Mpv) -> Result<usize> {
-        mpv.get_property::<i64>("playlist/count")
-            .context("Failed to get mpv playlist len")
-            .map(|count| {
-                usize::try_from(count).expect("The playlist_count should always be positive")
-            })
-    }
-
-    if get_playlist_count(mpv)? != 0 {
-        // We could also use `loadlist`, but that would require use to start a unix socket or even
-        // write all the video paths to a file beforehand
-        mpv.command("playlist-clear", &[])?;
-        mpv.command("playlist-remove", &["current"])?;
-    }
-
-    assert_eq!(
-        get_playlist_count(mpv)?,
-        0,
-        "The playlist should be empty at this point."
-    );
-
-    let playlist = if let Some(p) = maybe_playlist {
-        p
-    } else {
-        get::playlist(app).await?
-    };
-
-    debug!("Will add {} videos to playlist.", playlist.len());
-    playlist.into_iter().try_for_each(|cache_path| {
-        mpv.command("loadfile", &[
-            cache_path.to_str().with_context(|| {
-                format!(
-                    "Failed to parse the video cache path ('{}') as valid utf8",
-                    cache_path.display()
-                )
-            })?,
-            "append-play",
-        ])?;
-
-        Ok::<(), anyhow::Error>(())
-    })?;
-
-    let index = if let Some(index) = maybe_index {
-        let index = usize::from(index);
-        let playlist_length = get_playlist_count(mpv)?;
-
-        match index.cmp(&playlist_length) {
-            Ordering::Greater => {
-                unreachable!(
-                    "The index '{index}' execeeds the playlist length '{playlist_length}'."
-                );
-            }
-            Ordering::Less => index,
-            Ordering::Equal => {
-                // The index is pointing to the end of the playlist. We could either go the second
-                // to last entry (i.e., one entry back) or wrap around to the start.
-                // We wrap around:
-                0
-            }
-        }
-    } else {
-        get::current_playlist_index(app)
-            .await?
-            .map_or(0, usize::from)
-    };
-    mpv.set_property("playlist-pos", index.to_string().as_str())?;
-
-    Ok(())
-}
-
-/// Return the status of the playback queue
-pub async fn status(app: &App) -> Result<Status> {
-    let playlist = get::playlist(app).await?;
-
-    let playlist_len = playlist.len();
-    let marked_watch_num = get::videos(app, &[VideoStatusMarker::Watch]).await?.len();
-
-    if playlist_len == 0 && marked_watch_num == 0 {
-        Ok(Status::NoMoreAvailable)
-    } else if playlist_len == 0 && marked_watch_num != 0 {
-        Ok(Status::NoCached {
-            marked_watch: marked_watch_num,
-        })
-    } else if playlist_len != 0 {
-        Ok(Status::Available {
-            newly_available: playlist_len,
-        })
-    } else {
-        unreachable!(
-            "The playlist length is {playlist_len}, but the number of marked watch videos is {marked_watch_num}! This is a bug."
-        );
-    }
-}
-
-/// # Returns
-/// This will return [`true`], if the event handling should be stopped
-///
-/// # Panics
-/// Only if internal assertions fail.
-#[allow(clippy::too_many_lines)]
-pub async fn handle_mpv_event(app: &App, mpv: &Mpv, event: &Event<'_>) -> Result<bool> {
-    match event {
-        Event::EndFile(r) => match r.reason {
-            EndFileReason::Eof => {
-                info!("Mpv reached the end of the current video. Marking it watched.");
-                mark_video_watched(app, mpv).await?;
-                reload_mpv_playlist(app, mpv, None, None).await?;
-            }
-            EndFileReason::Stop => {
-                // This reason is incredibly ambiguous. It _both_ means actually pausing a
-                // video and going to the next one in the playlist.
-                // Oh, and it's also called, when a video is removed from the playlist (at
-                // least via "playlist-remove current")
-                info!("Paused video (or went to next playlist entry); Doing nothing");
-            }
-            EndFileReason::Quit => {
-                info!("Mpv quit. Exiting playback");
-
-                save_watch_progress(app, mpv).await?;
-
-                return Ok(true);
-            }
-            EndFileReason::Error => {
-                unreachable!("This should have been raised as a separate error")
-            }
-            EndFileReason::Redirect => {
-                // TODO: We probably need to handle this somehow <2025-02-17>
-            }
-        },
-        Event::StartFile(_) => {
-            let mpv_pos = usize::try_from(mpv.get_property::<i64>("playlist-pos")?)
-                .expect("The value is strictly positive");
-
-            let next_video = {
-                let yt_pos = get::current_playlist_index(app).await?.map(usize::from);
-
-                if (Some(mpv_pos) != yt_pos) || yt_pos.is_none() {
-                    let playlist = get::playlist(app).await?;
-                    let video = playlist
-                        .get(PlaylistIndex::from(mpv_pos))
-                        .expect("The mpv pos should not be out of bounds");
-
-                    set::focused(
-                        app,
-                        &video.extractor_hash,
-                        get::currently_focused_video(app)
-                            .await?
-                            .as_ref()
-                            .map(|v| &v.extractor_hash),
-                    )
-                    .await?;
-
-                    video.extractor_hash
-                } else {
-                    get::currently_focused_video(app)
-                        .await?
-                        .expect("We have a focused video")
-                        .extractor_hash
-                }
-            };
-
-            apply_video_options(app, mpv, &next_video).await?;
-        }
-        Event::Seek => {
-            save_watch_progress(app, mpv).await?;
-        }
-        Event::ClientMessage(a) => {
-            debug!("Got Client Message event: '{}'", a.join(" "));
-
-            match a.as_slice() {
-                &["yt-comments-external"] => {
-                    client_messages::handle_yt_comments_external(app).await?;
-                }
-                &["yt-comments-local"] => {
-                    client_messages::handle_yt_comments_local(app, mpv).await?;
-                }
-
-                &["yt-description-external"] => {
-                    client_messages::handle_yt_description_external(app).await?;
-                }
-                &["yt-description-local"] => {
-                    client_messages::handle_yt_description_local(app, mpv).await?;
-                }
-
-                &["yt-mark-picked"] => {
-                    let current_video = get::currently_focused_video(app)
-                        .await?
-                        .expect("This should exist at this point");
-                    let current_index = get::current_playlist_index(app)
-                        .await?
-                        .expect("This should exist, as we can mark this video picked");
-
-                    save_watch_progress(app, mpv).await?;
-
-                    set::video_status(
-                        app,
-                        &current_video.extractor_hash,
-                        VideoStatus::Pick,
-                        Some(current_video.priority),
-                    )
-                    .await?;
-
-                    reload_mpv_playlist(app, mpv, None, Some(current_index)).await?;
-                    mpv_message(mpv, "Marked the video as picked", Duration::from_secs(3))?;
-                }
-                &["yt-mark-watched"] => {
-                    let current_index = get::current_playlist_index(app)
-                        .await?
-                        .expect("This should exist, as we can mark this video picked");
-                    mark_video_watched(app, mpv).await?;
-
-                    reload_mpv_playlist(app, mpv, None, Some(current_index)).await?;
-                    mpv_message(mpv, "Marked the video watched", Duration::from_secs(3))?;
-                }
-                &["yt-check-new-videos"] => {
-                    reload_mpv_playlist(app, mpv, None, None).await?;
-                }
-                other => {
-                    debug!("Unknown message: {}", other.join(" "));
-                }
-            }
-        }
-        _ => {}
-    }
-
-    Ok(false)
-}