about summary refs log tree commit diff stats
path: root/crates
diff options
context:
space:
mode:
authorBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-23 13:06:00 +0200
committerBenedikt Peetz <benedikt.peetz@b-peetz.de>2024-08-23 13:06:00 +0200
commit52e99b38eae6b4f3cb991342ff9ba9abbab9e42c (patch)
tree9fa6f0582dfb8b6dc7b49bbd6206ab4b533ff900 /crates
parentrefactor(cli): Replace the byte parser with the one from the `bytes` crate (diff)
downloadyt-52e99b38eae6b4f3cb991342ff9ba9abbab9e42c.tar.gz
yt-52e99b38eae6b4f3cb991342ff9ba9abbab9e42c.zip
refactor(yt_dlp): Also move the `crates` subdirectory
Diffstat (limited to 'crates')
-rw-r--r--crates/yt_dlp/.cargo/config.toml12
-rw-r--r--crates/yt_dlp/.gitignore18
-rw-r--r--crates/yt_dlp/Cargo.lock645
-rw-r--r--crates/yt_dlp/Cargo.lock.license9
-rw-r--r--crates/yt_dlp/Cargo.toml25
-rw-r--r--crates/yt_dlp/README.md24
-rw-r--r--crates/yt_dlp/cog.toml35
-rw-r--r--crates/yt_dlp/src/duration.rs71
-rw-r--r--crates/yt_dlp/src/lib.rs412
-rw-r--r--crates/yt_dlp/src/logging.rs125
-rw-r--r--crates/yt_dlp/src/main.rs96
-rw-r--r--crates/yt_dlp/src/wrapper/info_json.rs550
-rw-r--r--crates/yt_dlp/src/wrapper/mod.rs12
-rw-r--r--crates/yt_dlp/src/wrapper/yt_dlp_options.rs62
-rwxr-xr-xcrates/yt_dlp/update.sh14
15 files changed, 2110 insertions, 0 deletions
diff --git a/crates/yt_dlp/.cargo/config.toml b/crates/yt_dlp/.cargo/config.toml
new file mode 100644
index 0000000..d84f14d
--- /dev/null
+++ b/crates/yt_dlp/.cargo/config.toml
@@ -0,0 +1,12 @@
+# 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>.
+
+[env]
+PYO3_PYTHON = "/nix/store/7xzk119acyws2c4ysygdv66l0grxkr39-python3-3.11.9-env/bin/python3"
diff --git a/crates/yt_dlp/.gitignore b/crates/yt_dlp/.gitignore
new file mode 100644
index 0000000..e7d49e7
--- /dev/null
+++ b/crates/yt_dlp/.gitignore
@@ -0,0 +1,18 @@
+# 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>.
+
+# build
+/target
+/result
+
+/references
+
+# dev env
+.direnv
diff --git a/crates/yt_dlp/Cargo.lock b/crates/yt_dlp/Cargo.lock
new file mode 100644
index 0000000..cb8d7c9
--- /dev/null
+++ b/crates/yt_dlp/Cargo.lock
@@ -0,0 +1,645 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 3
+
+[[package]]
+name = "autocfg"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0"
+
+[[package]]
+name = "bitflags"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
+
+[[package]]
+name = "bytes"
+version = "1.0.0"
+
+[[package]]
+name = "cfg-if"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
+
+[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "icu_collections"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locid"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[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"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "utf16_iter",
+ "utf8_iter",
+ "write16",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516"
+
+[[package]]
+name = "icu_properties"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1f8ac670d7422d7f76b32e17a5db556510825b29ec9154f235977c9caba61036"
+dependencies = [
+ "displaydoc",
+ "icu_collections",
+ "icu_locid_transform",
+ "icu_properties_data",
+ "icu_provider",
+ "tinystr",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569"
+
+[[package]]
+name = "icu_provider"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9"
+dependencies = [
+ "displaydoc",
+ "icu_locid",
+ "icu_provider_macros",
+ "stable_deref_trait",
+ "tinystr",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "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.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4716a3a0933a1d01c2f72450e89596eb51dd34ef3c211ccd875acdf1f8fe47ed"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "indoc"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5"
+
+[[package]]
+name = "itoa"
+version = "1.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
+
+[[package]]
+name = "libc"
+version = "0.2.155"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
+
+[[package]]
+name = "litemap"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "643cb0b8d4fcc284004d5fd0d67ccf61dfffadb7f75e1e71bc420f4688a3a704"
+
+[[package]]
+name = "lock_api"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
+dependencies = [
+ "autocfg",
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
+
+[[package]]
+name = "memoffset"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
+
+[[package]]
+name = "parking_lot"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-targets",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
+
+[[package]]
+name = "portable-atomic"
+version = "1.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.85"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22244ce15aa966053a896d1accb3a6e68469b97c7f33f284b99f0d576879fc23"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a5e00b96a521718e08e03b1a622f01c8a8deb50719335de3f60b3b3950f069d8"
+dependencies = [
+ "cfg-if",
+ "indoc",
+ "libc",
+ "memoffset",
+ "parking_lot",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+ "unindent",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7883df5835fafdad87c0d888b266c8ec0f4c9ca48a5bed6bbb592e8dedee1b50"
+dependencies = [
+ "once_cell",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "01be5843dc60b916ab4dad1dca6d20b9b4e6ddc8e15f50c47fe6d85f1fb97403"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77b34069fc0682e11b31dbd10321cbf94808394c56fd996796ce45217dfac53c"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.21.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08260721f32db5e1a5beae69a55553f56b99bd0e1c3e6e0a5e8851a9d0f5a85c"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c82cf8cff14456045f55ec4241383baeff27af886adb72ffb2162f99911de0fd"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "ryu"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "serde"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.203"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
+dependencies = [
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
+name = "smallvec"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67"
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3"
+
+[[package]]
+name = "syn"
+version = "2.0.66"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42f3f41a2de00b01c0aaad383c5a45241efc8b2d1eda5661812fda5f3cdcff5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.12.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f"
+
+[[package]]
+name = "tinystr"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b"
+
+[[package]]
+name = "unindent"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce"
+
+[[package]]
+name = "url"
+version = "2.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c25da092f0a868cdf09e8674cd3b7ef3a7d92a24253e663a2fb85e2496de56"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[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"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "windows-targets"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb"
+dependencies = [
+ "windows_aarch64_gnullvm",
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_gnullvm",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
+
+[[package]]
+name = "write16"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936"
+
+[[package]]
+name = "writeable"
+version = "0.5.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51"
+
+[[package]]
+name = "yoke"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c5b1314b079b0930c31e3af543d8ee1757b1951ae1e1565ec704403a7240ca5"
+dependencies = [
+ "serde",
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.7.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28cc31741b18cb6f1d5ff12f5b7523e3d6eb0852bbbad19d73905511d9849b95"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "yt_dlp"
+version = "0.1.0"
+dependencies = [
+ "bytes",
+ "log",
+ "pyo3",
+ "serde",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ec111ce797d0e0784a1116d0ddcdbea84322cd79e5d5ad173daeba4f93ab55"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ea7b4a3637ea8669cedf0f1fd5c286a17f3de97b8dd5a70a6c167a1730e63a5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb2cc8827d6c0994478a15c53f374f46fbd41bea663d809b14744bc42e6b109c"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97cf56601ee5052b4417d90c8755c6683473c926039908196cf35d99f893ebe7"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
diff --git a/crates/yt_dlp/Cargo.lock.license b/crates/yt_dlp/Cargo.lock.license
new file mode 100644
index 0000000..d4d410f
--- /dev/null
+++ b/crates/yt_dlp/Cargo.lock.license
@@ -0,0 +1,9 @@
+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>.
diff --git a/crates/yt_dlp/Cargo.toml b/crates/yt_dlp/Cargo.toml
new file mode 100644
index 0000000..7af29d7
--- /dev/null
+++ b/crates/yt_dlp/Cargo.toml
@@ -0,0 +1,25 @@
+# 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>.
+
+[package]
+name = "yt_dlp"
+description = "A wrapper around the python yt_dlp library"
+version = "0.1.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+log = "0.4.21"
+pyo3 = { version = "0.21.2", features = ["auto-initialize", "gil-refs"] }
+serde = { version = "1.0.203", features = ["derive"] }
+serde_json = "1.0.117"
+url = { version = "2.5.0", features = ["serde"] }
+bytes = {path = "../bytes"}
diff --git a/crates/yt_dlp/README.md b/crates/yt_dlp/README.md
new file mode 100644
index 0000000..591ef2e
--- /dev/null
+++ b/crates/yt_dlp/README.md
@@ -0,0 +1,24 @@
+<!--
+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_py
+
+> \[can be empty\]
+
+Some text about the project.
+
+## Licence
+
+This program is free software: you can redistribute it and/or modify it under
+the terms of the GNU General Public License as published by the Free Software
+Foundation, either version 3 of the License, or (at your option) any later
+version.
diff --git a/crates/yt_dlp/cog.toml b/crates/yt_dlp/cog.toml
new file mode 100644
index 0000000..7389072
--- /dev/null
+++ b/crates/yt_dlp/cog.toml
@@ -0,0 +1,35 @@
+# 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>.
+
+tag_prefix = "v"
+branch_whitelist = ["main", "prime"]
+ignore_merge_commits = false
+
+pre_bump_hooks = [
+  "nix flake check",                     # verify the project builds
+  "./scripts/renew_copyright_header.sh", # update the license header in each file
+  "cargo set-version {{version}}",       # bump version in Cargo.toml
+  "nix fmt",                             # format
+]
+post_bump_hooks = [
+  "git push",
+  "cargo publish",
+  "git push origin v{{version}}", # push the new tag to origin
+]
+
+[bump_profiles]
+
+[changelog]
+path = "NEWS.md"
+template = "remote"
+remote = "codeberg.org"
+repository = "yt_py"
+owner = "Benedikt Peetz"
+authors = [{ signature = "Benedikt Peetz", username = "Benedikt Peetz" }]
diff --git a/crates/yt_dlp/src/duration.rs b/crates/yt_dlp/src/duration.rs
new file mode 100644
index 0000000..cd7454b
--- /dev/null
+++ b/crates/yt_dlp/src/duration.rs
@@ -0,0 +1,71 @@
+// 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>
+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 {
+            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/lib.rs b/crates/yt_dlp/src/lib.rs
new file mode 100644
index 0000000..37d0945
--- /dev/null
+++ b/crates/yt_dlp/src/lib.rs
@@ -0,0 +1,412 @@
+// 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::{fs::File, io::Write};
+
+use std::{path::PathBuf, sync::Once};
+
+use crate::{duration::Duration, logging::setup_logging, wrapper::info_json::InfoJson};
+
+use bytes::Bytes;
+use log::{info, warn};
+use pyo3::types::{PyString, PyTuple, PyTupleMethods};
+use pyo3::{
+    pyfunction,
+    types::{PyAnyMethods, PyDict, PyDictMethods, PyList, PyListMethods, PyModule},
+    wrap_pyfunction_bound, Bound, PyAny, PyResult, Python,
+};
+use serde::Serialize;
+use serde_json::{Map, Value};
+use url::Url;
+
+pub mod duration;
+pub mod logging;
+pub mod wrapper;
+
+/// 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
+pub fn add_logger_and_sig_handler<'a>(
+    opts: Bound<'a, PyDict>,
+    py: Python,
+) -> PyResult<Bound<'a, PyDict>> {
+    setup_logging(py, "yt_dlp")?;
+
+    let logging = PyModule::import_bound(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_bound(
+            r#"
+import signal
+signal.signal(signal.SIGINT, signal.SIG_DFL)
+        "#,
+            None,
+            None,
+        )
+        .expect("This code should always work");
+
+        let config_opts = PyDict::new_bound(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");
+    });
+
+    // 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]
+pub fn progress_hook<'a>(py: Python, input: Bound<'_, PyDict>) -> PyResult<()> {
+    let input: serde_json::Map<String, Value> = serde_json::from_str(&json_dumps(
+        py,
+        input
+            .downcast::<PyAny>()
+            .expect("Will always work")
+            .to_owned(),
+    )?)
+    .expect("Python should always produce valid json");
+
+    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",
+                )
+            } else {
+                panic!(
+                    "Value {} => \n{}\n is not of type: {}",
+                    $name,
+                    a,
+                    stringify!($type_fun)
+                );
+            }
+        }};
+
+        ($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
+        }};
+
+        ($type_fun:ident, $get_fun:ident, $name:expr) => {{
+            get! {@interrogate input, $type_fun, $get_fun, $name}
+        }};
+    }
+
+    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}
+        }};
+    }
+
+    macro_rules! c {
+        ($color:expr, $format:expr) => {
+            format!("\x1b[{}m{}\x1b[0m", $color, $format)
+        };
+    }
+
+    fn format_bytes(bytes: u64) -> String {
+        let bytes = Bytes::new(bytes);
+        bytes.to_string()
+    }
+
+    fn format_speed(speed: f64) -> String {
+        let bytes = Bytes::new(speed.floor() as u64);
+        format!("{}/s", bytes)
+    }
+
+    let get_title = |add_extension: bool| -> String {
+        match get! {is_string, as_str, "info_dict", "ext"} {
+            "vtt" => {
+                format!(
+                    "Subtitles ({})",
+                    get! {is_string, as_str, "info_dict", "name"}
+                )
+            }
+            title_extension @ ("webm" | "mp4" | "m4a") => {
+                if add_extension {
+                    format!(
+                        "{} ({})",
+                        default_get! { as_str, "<No title>", "info_dict", "title"},
+                        title_extension
+                    )
+                } else {
+                    default_get! { as_str, "<No title>", "info_dict", "title"}.to_owned()
+                }
+            }
+            other => panic!("The extension '{}' is not yet implemented", other),
+        }
+    };
+
+    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 = {
+                let total_bytes = default_get!(as_u64, 0, "total_bytes");
+                if total_bytes == 0 {
+                    let estimate = default_get!(as_u64, 0, "total_bytes_estimate");
+                    warn!(
+                        "The video does not have a total_byte count, using an estimate of '{}'",
+                        estimate
+                    );
+                    estimate
+                } else {
+                    total_bytes
+                }
+            };
+            let percent: f64 = {
+                if total_bytes == 0 {
+                    100.0
+                } else {
+                    (downloaded_bytes as f64 / total_bytes as f64) * 100.0
+                }
+            };
+
+            print!("\x1b[1F"); // Move one line up, to allow the `println` after it to print a newline
+            print!("\x1b[2K"); // Clear whole line.
+            print!("\x1b[1G"); // Move cursor to column 1.
+
+            println!(
+                "'{}' [{}/{} at {}] -> [{}/{} {}]",
+                c!("34;1", get_title(true)),
+                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", format_bytes(total_bytes)),
+                c!("36;1", format!("{:.02}%", percent))
+            );
+        }
+        "finished" => {
+            println!("Finished downloading: '{}'", c!("34;1", get_title(false)))
+        }
+        "error" => {
+            panic!("Error whilst downloading: {}", get_title(true))
+        }
+        other => panic!("{} is not a valid state!", other),
+    };
+
+    Ok(())
+}
+
+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_bound!(progress_hook, py)?)?;
+
+        opts.set_item("progress_hooks", hooks)?;
+    } else {
+        // No hooks are set yet
+        let hooks_list = PyList::new_bound(py, &[wrap_pyfunction_bound!(progress_hook, py)?]);
+
+        opts.set_item("progress_hooks", hooks_list)?;
+    }
+
+    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:
+/// @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')
+pub async fn extract_info(
+    yt_dlp_opts: &Map<String, Value>,
+    url: &Url,
+    download: bool,
+    process: bool,
+) -> PyResult<InfoJson> {
+    Python::with_gil(|py| {
+        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_bound(py);
+        kwargs.set_item("download", download)?;
+        kwargs.set_item("process", process)?;
+
+        let result = instance.call_method("extract_info", args, Some(&kwargs))?;
+
+        // Remove the `<generator at 0xsome_hex>`, by setting it to null
+        if !process {
+            result.set_item("entries", ())?;
+        }
+
+        let result_str = json_dumps(py, result)?;
+
+        //let mut file = File::create("output.info.json").unwrap();
+        //write!(file, "{}", result_str).unwrap();
+
+        Ok(serde_json::from_str(&result_str)
+            .expect("Python should be able to produce correct json"))
+    })
+}
+
+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)
+    })
+}
+
+/// Download a given list of URLs.
+/// Returns the paths they were downloaded to.
+pub async fn download(
+    urls: &[Url],
+    download_options: &Map<String, Value>,
+) -> PyResult<Vec<PathBuf>> {
+    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).to_owned()
+        };
+
+        out_paths.push(result_string);
+        info!("Finished downloading url: '{}'", url);
+    }
+
+    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");
+
+    let python_dict = json_loads(py, json_string)?;
+
+    Ok(python_dict)
+}
+
+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_bound(py))?;
+    let sanitized_result = yt_dlp.call_method1("sanitize_info", (input,))?;
+
+    let json = PyModule::import_bound(py, "json")?;
+    let dumps = json.getattr("dumps")?;
+
+    let output = dumps.call1((sanitized_result,))?;
+
+    let output_str = output.extract::<String>()?;
+
+    Ok(output_str)
+}
+
+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");
+
+    json_loads(py, string)
+}
+
+fn json_loads(py: Python, input: String) -> PyResult<Bound<PyDict>> {
+    //     json.loads(input)
+
+    let json = PyModule::import_bound(py, "json")?;
+    let dumps = json.getattr("loads")?;
+
+    let output = dumps.call1((input,))?;
+
+    Ok(output
+        .downcast::<PyDict>()
+        .expect("This should always be a PyDict")
+        .clone())
+}
+
+fn get_yt_dlp_utils<'a>(py: Python<'a>) -> PyResult<Bound<'a, PyAny>> {
+    let yt_dlp = PyModule::import_bound(py, "yt_dlp")?;
+    let utils = yt_dlp.getattr("utils")?;
+
+    Ok(utils)
+}
+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_bound(py, "yt_dlp")?;
+    let youtube_dl = yt_dlp.call_method1("YoutubeDL", (opts,))?;
+
+    Ok(youtube_dl)
+}
diff --git a/crates/yt_dlp/src/logging.rs b/crates/yt_dlp/src/logging.rs
new file mode 100644
index 0000000..cca917c
--- /dev/null
+++ b/crates/yt_dlp/src/logging.rs
@@ -0,0 +1,125 @@
+// 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
+
+use log::{logger, Level, MetadataBuilder, Record};
+use pyo3::{
+    prelude::{PyAnyMethods, PyListMethods, PyModuleMethods},
+    pyfunction, wrap_pyfunction, Bound, PyAny, PyResult, Python,
+};
+
+/// Consume a Python `logging.LogRecord` and emit a Rust `Log` instead.
+#[pyfunction]
+fn host_log<'a>(record: Bound<'a, 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_ref()
+        .map(|x| x.as_str())
+        .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.
+pub fn setup_logging(py: Python, target: &str) -> PyResult<()> {
+    let logging = py.import_bound("logging")?;
+
+    logging.setattr("host_log", wrap_pyfunction!(host_log, &logging)?)?;
+
+    py.run_bound(
+        format!(
+            r#"
+class HostHandler(Handler):
+    def __init__(self, level=0):
+        super().__init__(level=level)
+
+    def emit(self, record):
+        host_log(record,"{}")
+
+oldBasicConfig = basicConfig
+def basicConfig(*pargs, **kwargs):
+    if "handlers" not in kwargs:
+        kwargs["handlers"] = [HostHandler()]
+    return oldBasicConfig(*pargs, **kwargs)
+"#,
+            target
+        )
+        .as_str(),
+        Some(&logging.dict()),
+        None,
+    )?;
+
+    let all = logging.index()?;
+    all.append("HostHandler")?;
+
+    Ok(())
+}
diff --git a/crates/yt_dlp/src/main.rs b/crates/yt_dlp/src/main.rs
new file mode 100644
index 0000000..c40ddc3
--- /dev/null
+++ b/crates/yt_dlp/src/main.rs
@@ -0,0 +1,96 @@
+// 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::{env::args, fs};
+
+use yt_dlp::wrapper::info_json::InfoJson;
+
+#[cfg(test)]
+mod test {
+    use url::Url;
+    use yt_dlp::wrapper::yt_dlp_options::{ExtractFlat, YtDlpOptions};
+
+    const YT_OPTS: YtDlpOptions = YtDlpOptions {
+        playliststart: 1,
+        playlistend: 10,
+        noplaylist: false,
+        extract_flat: ExtractFlat::InPlaylist,
+    };
+
+    #[test]
+    fn test_extract_info_video() {
+        let info = yt_dlp::extract_info(
+            YT_OPTS,
+            &Url::parse("https://www.youtube.com/watch?v=dbjPnXaacAU").expect("Is valid."),
+            false,
+            false,
+            false,
+        )
+        .map_err(|err| format!("Encountered error: '{}'", err))
+        .unwrap();
+
+        println!("{:#?}", info);
+    }
+
+    #[test]
+    fn test_extract_info_url() {
+        let err = yt_dlp::extract_info(
+            YT_OPTS,
+            &Url::parse("https://google.com").expect("Is valid."),
+            false,
+            false,
+            false,
+        )
+        .map_err(|err| format!("Encountered error: '{}'", err))
+        .unwrap();
+
+        println!("{:#?}", err);
+    }
+
+    #[test]
+    fn test_extract_info_playlist() {
+        let err = yt_dlp::extract_info(
+            YT_OPTS,
+            &Url::parse("https://www.youtube.com/@TheGarriFrischer/videos").expect("Is valid."),
+            false,
+            false,
+            true,
+        )
+        .map_err(|err| format!("Encountered error: '{}'", err))
+        .unwrap();
+
+        println!("{:#?}", err);
+    }
+    #[test]
+    fn test_extract_info_playlist_full() {
+        let err = yt_dlp::extract_info(
+            YT_OPTS,
+            &Url::parse("https://www.youtube.com/@NixOS-Foundation/videos").expect("Is valid."),
+            false,
+            false,
+            true,
+        )
+        .map_err(|err| format!("Encountered error: '{}'", err))
+        .unwrap();
+
+        println!("{:#?}", err);
+    }
+}
+
+fn main() {
+    let input_file: &str = &args().take(2).collect::<Vec<String>>()[1];
+
+    let input = fs::read_to_string(input_file).unwrap();
+
+    let output: InfoJson =
+        serde_json::from_str(&input).expect("Python should be able to produce correct json");
+
+    println!("{:#?}", output);
+}
diff --git a/crates/yt_dlp/src/wrapper/info_json.rs b/crates/yt_dlp/src/wrapper/info_json.rs
new file mode 100644
index 0000000..9c0d464
--- /dev/null
+++ b/crates/yt_dlp/src/wrapper/info_json.rs
@@ -0,0 +1,550 @@
+// 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::{collections::HashMap, path::PathBuf};
+
+use pyo3::{types::PyDict, Bound, PyResult, Python};
+use serde::{Deserialize, Deserializer, Serialize};
+use serde_json::Value;
+use url::Url;
+
+use crate::json_loads_str;
+
+type Todo = 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 {
+    pub __last_playlist_index: Option<u32>,
+    pub __post_extractor: Option<String>,
+    pub __x_forwarded_for_ip: Option<String>,
+    pub _filename: Option<PathBuf>,
+    pub _format_sort_fields: Option<Vec<String>>,
+    pub _has_drm: Option<Todo>,
+    pub _type: Option<InfoType>,
+    pub _version: Option<Version>,
+    pub abr: Option<f64>,
+    pub acodec: Option<String>,
+    pub age_limit: Option<u32>,
+    pub aspect_ratio: Option<f64>,
+    pub asr: Option<u32>,
+    pub audio_channels: Option<u32>,
+    pub audio_ext: Option<String>,
+    pub automatic_captions: Option<HashMap<String, Vec<Caption>>>,
+    pub availability: Option<String>,
+    pub average_rating: Option<String>,
+    pub categories: Option<Vec<String>>,
+    pub channel: Option<String>,
+    pub channel_follower_count: Option<u32>,
+    pub channel_id: Option<String>,
+    pub channel_is_verified: Option<bool>,
+    pub channel_url: Option<String>,
+    pub chapters: Option<Vec<Chapter>>,
+    pub comment_count: Option<u32>,
+    pub comments: Option<Vec<Comment>>,
+    pub concurrent_view_count: Option<u32>,
+    pub description: Option<String>,
+    pub display_id: Option<String>,
+    pub downloader_options: Option<DownloaderOptions>,
+    pub duration: Option<f64>,
+    pub duration_string: Option<String>,
+    pub dynamic_range: Option<String>,
+    pub entries: Option<Vec<InfoJson>>,
+    pub episode: Option<String>,
+    pub episode_number: Option<u32>,
+    pub epoch: Option<u32>,
+    pub ext: Option<String>,
+    pub extractor: Option<Extractor>,
+    pub extractor_key: Option<ExtractorKey>,
+    pub filename: Option<PathBuf>,
+    pub filesize: Option<u64>,
+    pub filesize_approx: Option<u64>,
+    pub format: Option<String>,
+    pub format_id: Option<String>,
+    pub format_index: Option<u32>,
+    pub format_note: Option<String>,
+    pub formats: Option<Vec<Format>>,
+    pub fps: Option<f64>,
+    pub fulltitle: Option<String>,
+    pub has_drm: Option<bool>,
+    pub heatmap: Option<Vec<HeatMapEntry>>,
+    pub height: Option<u32>,
+    pub http_headers: Option<HttpHeader>,
+    pub id: Option<String>,
+    pub ie_key: Option<ExtractorKey>,
+    pub is_live: Option<bool>,
+    pub language: Option<String>,
+    pub language_preference: Option<i32>,
+    pub license: Option<Todo>,
+    pub like_count: Option<u32>,
+    pub live_status: Option<String>,
+    pub location: Option<Todo>,
+    pub manifest_url: Option<Url>,
+    pub modified_date: Option<String>,
+    pub n_entries: Option<u32>,
+    pub original_url: Option<String>,
+    pub playable_in_embed: Option<bool>,
+    pub playlist: Option<Todo>,
+    pub playlist_autonumber: Option<u32>,
+    pub playlist_channel: Option<Todo>,
+    pub playlist_channel_id: Option<Todo>,
+    pub playlist_count: Option<u32>,
+    pub playlist_id: Option<Todo>,
+    pub playlist_index: Option<u64>,
+    pub playlist_title: Option<Todo>,
+    pub playlist_uploader: Option<Todo>,
+    pub playlist_uploader_id: Option<Todo>,
+    pub preference: Option<Todo>,
+    pub protocol: Option<String>,
+    pub quality: Option<f64>,
+    pub release_date: Option<String>,
+    pub release_timestamp: Option<u64>,
+    pub release_year: Option<u32>,
+    pub requested_downloads: Option<Vec<RequestedDownloads>>,
+    pub requested_entries: Option<Vec<u32>>,
+    pub requested_formats: Option<Vec<Format>>,
+    pub requested_subtitles: Option<HashMap<String, Subtitle>>,
+    pub resolution: Option<String>,
+    pub season: Option<String>,
+    pub season_number: Option<u32>,
+    pub series: Option<String>,
+    pub source_preference: Option<i32>,
+    pub sponsorblock_chapters: Option<Vec<SponsorblockChapter>>,
+    pub stretched_ratio: Option<Todo>,
+    pub subtitles: Option<HashMap<String, Vec<Caption>>>,
+    pub tags: Option<Vec<String>>,
+    pub tbr: Option<f64>,
+    pub thumbnail: Option<Url>,
+    pub thumbnails: Option<Vec<ThumbNail>>,
+    pub timestamp: Option<u64>,
+    pub title: Option<String>,
+    pub upload_date: Option<String>,
+    pub uploader: Option<String>,
+    pub uploader_id: Option<String>,
+    pub uploader_url: Option<String>,
+    pub url: Option<Url>,
+    pub vbr: Option<f64>,
+    pub vcodec: Option<String>,
+    pub video_ext: Option<String>,
+    pub view_count: Option<u32>,
+    pub was_live: Option<bool>,
+    pub webpage_url: Option<Url>,
+    pub webpage_url_basename: Option<String>,
+    pub webpage_url_domain: Option<String>,
+    pub width: Option<u32>,
+}
+
+#[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: f64,
+    pub asr: u32,
+    pub audio_channels: u32,
+    pub chapters: Option<Vec<SponsorblockChapter>>,
+    pub duration: Option<f64>,
+    pub dynamic_range: String,
+    pub ext: String,
+    pub filename: PathBuf,
+    pub filepath: PathBuf,
+    pub filesize_approx: u64,
+    pub format: String,
+    pub format_id: String,
+    pub format_note: String,
+    pub fps: f64,
+    pub height: u32,
+    pub infojson_filename: PathBuf,
+    pub language: Option<String>,
+    pub protocol: String,
+    pub requested_formats: Vec<Format>,
+    pub resolution: String,
+    pub tbr: f64,
+    pub vbr: f64,
+    pub vcodec: String,
+    pub width: u32,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[serde(deny_unknown_fields)]
+pub struct Subtitle {
+    pub ext: SubtitleExt,
+    pub filepath: PathBuf,
+    pub name: String,
+    pub url: Url,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[serde(deny_unknown_fields)]
+pub enum SubtitleExt {
+    #[serde(alias = "vtt")]
+    Vtt,
+
+    #[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, Eq, PartialOrd, Ord)]
+#[serde(deny_unknown_fields)]
+pub struct Caption {
+    pub ext: SubtitleExt,
+    pub name: Option<String>,
+    pub protocol: Option<String>,
+    pub url: String,
+    pub filepath: Option<PathBuf>,
+    pub video_id: Option<String>,
+    pub manifest_url: Option<Url>,
+    pub filesize: Option<u64>,
+}
+
+#[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)]
+#[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)]
+#[serde(deny_unknown_fields)]
+pub enum SponsorblockChapterCategory {
+    #[serde(alias = "filler")]
+    Filler,
+
+    #[serde(alias = "interaction")]
+    Interaction,
+
+    #[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)]
+pub struct HeatMapEntry {
+    pub start_time: f64,
+    pub end_time: f64,
+    pub value: f64,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[serde(deny_unknown_fields)]
+pub enum Extractor {
+    #[serde(alias = "generic")]
+    Generic,
+
+    #[serde(alias = "SVTSeries")]
+    SVTSeries,
+    #[serde(alias = "SVTPlay")]
+    SVTPlay,
+
+    #[serde(alias = "youtube")]
+    YouTube,
+    #[serde(alias = "youtube:tab")]
+    YouTubeTab,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[serde(deny_unknown_fields)]
+pub enum ExtractorKey {
+    #[serde(alias = "Generic")]
+    Generic,
+
+    #[serde(alias = "SVTSeries")]
+    SVTSeries,
+    #[serde(alias = "SVTPlay")]
+    SVTPlay,
+
+    #[serde(alias = "Youtube")]
+    YouTube,
+    #[serde(alias = "YoutubeTab")]
+    YouTubeTab,
+}
+
+#[derive(Debug, Deserialize, Serialize, PartialEq, PartialOrd, Ord, Eq)]
+#[serde(deny_unknown_fields)]
+pub enum InfoType {
+    #[serde(alias = "playlist")]
+    Playlist,
+
+    #[serde(alias = "url")]
+    Url,
+
+    #[serde(alias = "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 {
+    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)]
+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: 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 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)]
+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 url: Option<Url>,
+    pub duration: Option<f64>,
+    pub path: Option<PathBuf>,
+}
+
+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/mod.rs b/crates/yt_dlp/src/wrapper/mod.rs
new file mode 100644
index 0000000..3fe3247
--- /dev/null
+++ b/crates/yt_dlp/src/wrapper/mod.rs
@@ -0,0 +1,12 @@
+// 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>.
+
+pub mod info_json;
+// pub mod yt_dlp_options;
diff --git a/crates/yt_dlp/src/wrapper/yt_dlp_options.rs b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
new file mode 100644
index 0000000..c2a86df
--- /dev/null
+++ b/crates/yt_dlp/src/wrapper/yt_dlp_options.rs
@@ -0,0 +1,62 @@
+// 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::{types::PyDict, Bound, PyResult, Python};
+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
new file mode 100755
index 0000000..eb9c3c1
--- /dev/null
+++ b/crates/yt_dlp/update.sh
@@ -0,0 +1,14 @@
+#!/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
+#
+# 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" = "upgrade" ] && cargo upgrade
+cargo update