summary refs log tree commit diff
diff options
context:
space:
mode:
authorMalte Voos <git@mal.tc>2025-12-05 15:35:38 +0100
committerMalte Voos <git@mal.tc>2025-12-05 15:43:58 +0100
commitc347b6133365dcf1b7da4e77890b20d04d6cfba4 (patch)
treec83aac6f7d1e6edc57e607f01e5d3eeee8da4a0e
parent652b1c2a0ce7db4885ebc51f7f09133a43401442 (diff)
downloadlleap-c347b6133365dcf1b7da4e77890b20d04d6cfba4.tar.gz
lleap-c347b6133365dcf1b7da4e77890b20d04d6cfba4.zip
implement machine translation; various fixes and refactorings HEAD main
-rw-r--r--Cargo.lock1467
-rw-r--r--Cargo.toml2
-rw-r--r--data/style.css (renamed from resources/style.css)4
-rw-r--r--flake.nix1
-rw-r--r--src/app.rs335
-rw-r--r--src/cue_view.rs81
-rw-r--r--src/main.rs9
-rw-r--r--src/open_dialog.rs58
-rw-r--r--src/preferences_dialog.rs (renamed from src/preferences.rs)29
-rw-r--r--src/settings.rs5
-rw-r--r--src/subtitle_selection_dialog.rs148
-rw-r--r--src/subtitle_view.rs57
-rw-r--r--src/subtitles/extraction/embedded.rs (renamed from src/subtitle_extraction/embedded.rs)18
-rw-r--r--src/subtitles/extraction/mod.rs (renamed from src/subtitle_extraction/mod.rs)12
-rw-r--r--src/subtitles/extraction/whisper.rs (renamed from src/subtitle_extraction/whisper.rs)16
-rw-r--r--src/subtitles/mod.rs86
-rw-r--r--src/subtitles/state.rs63
-rw-r--r--src/track_selector.rs26
-rw-r--r--src/tracks.rs38
-rw-r--r--src/transcript.rs8
-rw-r--r--src/translation/deepl.rs106
-rw-r--r--src/translation/mod.rs11
-rw-r--r--src/util/tracker.rs18
-rw-r--r--subs.srt644
24 files changed, 2172 insertions, 1070 deletions
diff --git a/Cargo.lock b/Cargo.lock
index f3b1d1c..6a00a96 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -62,7 +62,7 @@ version = "1.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -73,7 +73,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
 dependencies = [
  "anstyle",
  "once_cell_polyfill",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -83,6 +83,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
 
 [[package]]
+name = "arrayvec"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
+
+[[package]]
 name = "async-channel"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -95,6 +101,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "atomic-waker"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
+
+[[package]]
 name = "atomic_refcell"
 version = "0.1.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -107,6 +119,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
 
 [[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
 name = "bindgen"
 version = "0.72.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -121,7 +139,7 @@ dependencies = [
  "regex",
  "rustc-hash",
  "shlex",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -143,6 +161,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
 
 [[package]]
+name = "bytes"
+version = "1.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
+
+[[package]]
 name = "cairo-rs"
 version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -231,9 +255,9 @@ dependencies = [
  "bitflags",
  "block",
  "cocoa-foundation",
- "core-foundation",
+ "core-foundation 0.10.1",
  "core-graphics",
- "foreign-types",
+ "foreign-types 0.5.0",
  "libc",
  "objc",
 ]
@@ -246,7 +270,7 @@ checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d"
 dependencies = [
  "bitflags",
  "block",
- "core-foundation",
+ "core-foundation 0.10.1",
  "core-graphics-types",
  "objc",
 ]
@@ -268,6 +292,16 @@ dependencies = [
 
 [[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"
 version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
@@ -289,9 +323,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
 dependencies = [
  "bitflags",
- "core-foundation",
+ "core-foundation 0.10.1",
  "core-graphics-types",
- "foreign-types",
+ "foreign-types 0.5.0",
  "libc",
 ]
 
@@ -302,7 +336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
 dependencies = [
  "bitflags",
- "core-foundation",
+ "core-foundation 0.10.1",
  "libc",
 ]
 
@@ -322,12 +356,89 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
+name = "deepl"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e315350d74306624c6519aea28860100be984dfbec7939a36d6b190fc0b77fb1"
+dependencies = [
+ "paste",
+ "reqwest",
+ "serde",
+ "serde_json",
+ "thiserror",
+ "tokio",
+ "tokio-stream",
+ "typed-builder",
+]
+
+[[package]]
+name = "deluxe"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ed332aaf752b459088acf3dd4eca323e3ef4b83c70a84ca48fb0ec5305f1488"
+dependencies = [
+ "deluxe-core",
+ "deluxe-macros",
+ "once_cell",
+ "proc-macro2",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "deluxe-core"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eddada51c8576df9d6a8450c351ff63042b092c9458b8ac7d20f89cbd0ffd313"
+dependencies = [
+ "arrayvec",
+ "proc-macro2",
+ "quote",
+ "strsim",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "deluxe-macros"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87546d9c837f0b7557e47b8bd6eae52c3c223141b76aa233c345c9ab41d9117"
+dependencies = [
+ "deluxe-core",
+ "heck 0.4.1",
+ "if_chain",
+ "proc-macro-crate 1.3.1",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
 name = "either"
 version = "1.15.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
 
 [[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 = "endi"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -363,6 +474,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
 
 [[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "event-listener"
 version = "5.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -384,6 +505,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "fastrand"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be"
+
+[[package]]
 name = "ffmpeg-next"
 version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -447,13 +574,28 @@ dependencies = [
 ]
 
 [[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foreign-types"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1"
+dependencies = [
+ "foreign-types-shared 0.1.1",
+]
+
+[[package]]
 name = "foreign-types"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d737d9aa519fb7b749cbc3b962edcf310a8dd1f4b67c91c4f83975dbdd17d965"
 dependencies = [
  "foreign-types-macros",
- "foreign-types-shared",
+ "foreign-types-shared 0.3.1",
 ]
 
 [[package]]
@@ -464,16 +606,31 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
 name = "foreign-types-shared"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b"
+
+[[package]]
+name = "foreign-types-shared"
 version = "0.3.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "aa9a19cbb55df58761df49b23516a86d432839add4af60fc256da840f66ed35b"
 
 [[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
 name = "fragile"
 version = "2.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -535,7 +692,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -664,6 +821,18 @@ dependencies = [
 ]
 
 [[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi",
+ "wasip2",
+]
+
+[[package]]
 name = "gio"
 version = "0.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -690,7 +859,7 @@ dependencies = [
  "gobject-sys",
  "libc",
  "system-deps",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -720,11 +889,11 @@ version = "0.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "880e524e0085f3546cfb38532b2c202c0d64741d9977a6e4aa24704bfc9f19fb"
 dependencies = [
- "heck",
- "proc-macro-crate",
+ "heck 0.5.0",
+ "proc-macro-crate 3.4.0",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -778,6 +947,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "gsettings-macro"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f3b38a7ca9498c2a7ed01ec9cff1f07397d8bc8f4a345b46a58a80170947263"
+dependencies = [
+ "deluxe",
+ "heck 0.5.0",
+ "proc-macro-error",
+ "proc-macro2",
+ "quick-xml 0.37.5",
+ "quote",
+ "serde",
+ "syn 2.0.110",
+]
+
+[[package]]
 name = "gsk4"
 version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -822,7 +1007,7 @@ dependencies = [
  "gstreamer-gl",
  "gstreamer-video",
  "gtk4",
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -832,7 +1017,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a68a894ef2d738054b950e1dbef5d9012b63fd968d4d32dbccd31bd8d8d4b219"
 dependencies = [
  "chrono",
- "toml_edit",
+ "toml_edit 0.23.7",
 ]
 
 [[package]]
@@ -1013,10 +1198,10 @@ version = "0.10.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "821160b4f17e7e4ed748818c23682d0a46bed04c287dbaac54dd4869d2c5e06a"
 dependencies = [
- "proc-macro-crate",
+ "proc-macro-crate 3.4.0",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -1045,7 +1230,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b9d94c8a63f94bbc35cf63e105791c5992bd60d4516d41fe5bf3db8d10b30b43"
 dependencies = [
  "flate2",
- "quick-xml",
+ "quick-xml 0.38.4",
  "serde",
  "serde_json",
  "walkdir",
@@ -1054,6 +1239,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "h2"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3c0b69cfcb4e1b9f1bf2f53f95f766e4661169728ec61cd3fe5a0166f2d1386"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "fnv",
+ "futures-core",
+ "futures-sink",
+ "http",
+ "indexmap",
+ "slab",
+ "tokio",
+ "tokio-util",
+ "tracing",
+]
+
+[[package]]
 name = "hashbrown"
 version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1061,6 +1265,12 @@ checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
 
 [[package]]
 name = "heck"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
+
+[[package]]
+name = "heck"
 version = "0.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
@@ -1072,6 +1282,125 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
 
 [[package]]
+name = "http"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+dependencies = [
+ "bytes",
+ "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.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11"
+dependencies = [
+ "atomic-waker",
+ "bytes",
+ "futures-channel",
+ "futures-core",
+ "h2",
+ "http",
+ "http-body",
+ "httparse",
+ "itoa",
+ "pin-project-lite",
+ "pin-utils",
+ "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.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52e9a2a24dc5c6821e71a7030e1e14b7b632acac55c40e9d2e082c621261bb56"
+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.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1096,6 +1425,114 @@ dependencies = [
 ]
 
 [[package]]
+name = "icu_collections"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4c6b649701667bbe825c3b7e6388cb521c23d88644678e83c0c4d0a621a34b43"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "edba7861004dd3714265b4db54a3c390e880ab658fec5f7db895fae2046b5bb6"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5f6c8828b67bf8908d82127b2054ea1b4427ff0230ee9141c54251934ab1b599"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7aedcccd01fc5fe81e6b489c15b247b8b0690feb23304303a9e560f37efc560a"
+
+[[package]]
+name = "icu_properties"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e93fcd3157766c0c8da2f8cff6ce651a31f0810eaa1c51ec363ef790bbb5fb99"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02845b3647bb045f1100ecd6480ff52f34c35f82d9880e029d329c21d1054899"
+
+[[package]]
+name = "icu_provider"
+version = "2.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85962cf0ce02e1e0a629cc34e7ca3e373ce20dda4c4d7294bbd0bf1fdb59e614"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "if_chain"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd62e6b5e86ea8eeeb8db1de02880a6abc01a397b2ebb64b5d74ac255318f5cb"
+
+[[package]]
 name = "indexmap"
 version = "2.12.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1106,6 +1543,22 @@ dependencies = [
 ]
 
 [[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.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4f867b9d1d896b67beb18518eda36fdb77a32ea590de864f1325b294a6d14397"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
 name = "is_terminal_polyfill"
 version = "1.70.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1164,7 +1617,7 @@ checksum = "980af8b43c3ad5d8d349ace167ec8170839f753a42d233ba19e08afe1850fa69"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -1234,14 +1687,28 @@ dependencies = [
 ]
 
 [[package]]
+name = "linux-raw-sys"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039"
+
+[[package]]
+name = "litemap"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6373607a59f0be73a39b6fe456b8192fcc3585f602af20751600e974dd455e77"
+
+[[package]]
 name = "lleap"
 version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-channel",
  "cocoa",
+ "deepl",
  "env_logger",
  "ffmpeg-next",
+ "gsettings-macro",
  "gst-plugin-gtk4",
  "gstreamer",
  "gstreamer-play",
@@ -1302,6 +1769,22 @@ dependencies = [
 ]
 
 [[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
 name = "minimal-lexical"
 version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1318,6 +1801,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "mio"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69d83b0086dc8ecf3ce9ae2874b2d1290252e2a30720bea58a5c6639b0092873"
+dependencies = [
+ "libc",
+ "wasi",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "muldiv"
 version = "1.0.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1329,7 +1823,24 @@ version = "0.7.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a51313c5820b0b02bd422f4b44776fbf47961755c74ce64afc73bfad10226c3"
 dependencies = [
- "getrandom",
+ "getrandom 0.2.16",
+]
+
+[[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]]
@@ -1402,6 +1913,50 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
 
 [[package]]
+name = "openssl"
+version = "0.10.75"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
+dependencies = [
+ "bitflags",
+ "cfg-if",
+ "foreign-types 0.3.2",
+ "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 2.0.110",
+]
+
+[[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.111"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82cab2d520aa75e3c58898289429321eb788c3106963d0dc886ec7a5f4adc321"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+ "vcpkg",
+]
+
+[[package]]
 name = "option-operations"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1441,12 +1996,24 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
 
 [[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
 name = "pastey"
 version = "0.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec"
 
 [[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
 name = "phf"
 version = "0.11.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1498,12 +2065,55 @@ dependencies = [
 ]
 
 [[package]]
+name = "potential_utf"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b73949432f5e2a09657003c25bca5e19a0e9c84f8058ca374f49e0ebe605af77"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "proc-macro-crate"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919"
+dependencies = [
+ "once_cell",
+ "toml_edit 0.19.15",
+]
+
+[[package]]
 name = "proc-macro-crate"
 version = "3.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "219cb19e96be00ab2e37d6e299658a0cfa83e52429179969b0f0121b4ac46983"
 dependencies = [
- "toml_edit",
+ "toml_edit 0.23.7",
+]
+
+[[package]]
+name = "proc-macro-error"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
+dependencies = [
+ "proc-macro-error-attr",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+ "version_check",
+]
+
+[[package]]
+name = "proc-macro-error-attr"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "version_check",
 ]
 
 [[package]]
@@ -1517,6 +2127,16 @@ dependencies = [
 
 [[package]]
 name = "quick-xml"
+version = "0.37.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "331e97a1af0bf59823e6eadffe373d7b27f485be8748f71471c662c1f269b7fb"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "quick-xml"
 version = "0.38.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b66c2058c55a409d601666cffe35f04333cf1013010882cec174a7467cd4e21c"
@@ -1535,6 +2155,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
 name = "regex"
 version = "1.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1627,7 +2253,65 @@ checksum = "175fce497fc6f11dde7ea56daa30ff7ad29a534bbc209d59d766659c880ba5f1"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "reqwest"
+version = "0.12.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d0946410b9f7b082a427e4ef5c8ff541a88b357bc6c637c40db3a68ac70a36f"
+dependencies = [
+ "base64",
+ "bytes",
+ "encoding_rs",
+ "futures-core",
+ "futures-util",
+ "h2",
+ "http",
+ "http-body",
+ "http-body-util",
+ "hyper",
+ "hyper-rustls",
+ "hyper-tls",
+ "hyper-util",
+ "js-sys",
+ "log",
+ "mime",
+ "mime_guess",
+ "native-tls",
+ "percent-encoding",
+ "pin-project-lite",
+ "rustls-pki-types",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sync_wrapper",
+ "tokio",
+ "tokio-native-tls",
+ "tokio-util",
+ "tower",
+ "tower-http",
+ "tower-service",
+ "url",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "wasm-streams",
+ "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]]
@@ -1646,6 +2330,52 @@ dependencies = [
 ]
 
 [[package]]
+name = "rustix"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "533f54bc6a7d4f647e46ad909549eda97bf5afc1585190ef692b4286b198bd8f"
+dependencies = [
+ "once_cell",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94182ad936a0c91c324cd46c6511b9510ed16af436d7b5bab34beab0afd55f7a"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
 name = "rustversion"
 version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1667,12 +2397,44 @@ dependencies = [
 ]
 
 [[package]]
+name = "schannel"
+version = "0.1.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "scopeguard"
 version = "1.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
 
 [[package]]
+name = "security-framework"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.4",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.15.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
 name = "semver"
 version = "1.0.27"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1705,7 +2467,7 @@ checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -1731,6 +2493,18 @@ dependencies = [
 ]
 
 [[package]]
+name = "serde_urlencoded"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
+dependencies = [
+ "form_urlencoded",
+ "itoa",
+ "ryu",
+ "serde",
+]
+
+[[package]]
 name = "shlex"
 version = "1.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1761,6 +2535,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
 
 [[package]]
+name = "socket2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
+dependencies = [
+ "libc",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
 name = "spin"
 version = "0.9.8"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1770,12 +2554,40 @@ dependencies = [
 ]
 
 [[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
 name = "static_assertions"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
 
 [[package]]
+name = "strsim"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "1.0.109"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
+dependencies = [
+ "proc-macro2",
+ "unicode-ident",
+]
+
+[[package]]
 name = "syn"
 version = "2.0.110"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1787,13 +2599,54 @@ 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.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "system-configuration"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
+dependencies = [
+ "bitflags",
+ "core-foundation 0.9.4",
+ "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 = "system-deps"
 version = "7.0.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "48c8f33736f986f16d69b6cb8b03f55ddcad5c41acc4ccc39dd88e84aa805e7f"
 dependencies = [
  "cfg-expr",
- "heck",
+ "heck 0.5.0",
  "pkg-config",
  "toml",
  "version-compare",
@@ -1806,6 +2659,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df7f62577c25e07834649fc3b39fafdc597c0a3527dc1c60129201ccfcbaa50c"
 
 [[package]]
+name = "tempfile"
+version = "3.23.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d31c77bdf42a745371d260a26ca7163f1e0924b64afa0b688e61b5a9fa02f16"
+dependencies = [
+ "fastrand",
+ "getrandom 0.3.4",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
 name = "thiserror"
 version = "2.0.17"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1822,7 +2688,17 @@ checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42d3e9c45c09de15d06dd8acf5f4e0e399e85927b7f00711024eb7ae10fa4869"
+dependencies = [
+ "displaydoc",
+ "zerovec",
 ]
 
 [[package]]
@@ -1831,7 +2707,68 @@ version = "1.48.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
 dependencies = [
+ "bytes",
+ "libc",
+ "mio",
  "pin-project-lite",
+ "socket2",
+ "tokio-macros",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "tokio-macros"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[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.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61"
+dependencies = [
+ "rustls",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-stream"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047"
+dependencies = [
+ "futures-core",
+ "pin-project-lite",
+ "tokio",
+]
+
+[[package]]
+name = "tokio-util"
+version = "0.7.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594"
+dependencies = [
+ "bytes",
+ "futures-core",
+ "futures-sink",
+ "pin-project-lite",
+ "tokio",
 ]
 
 [[package]]
@@ -1843,14 +2780,20 @@ dependencies = [
  "indexmap",
  "serde_core",
  "serde_spanned",
- "toml_datetime",
+ "toml_datetime 0.7.3",
  "toml_parser",
  "toml_writer",
- "winnow",
+ "winnow 0.7.13",
 ]
 
 [[package]]
 name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+
+[[package]]
+name = "toml_datetime"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f2cdb639ebbc97961c51720f858597f7f24c4fc295327923af55b74c3c724533"
@@ -1860,14 +2803,25 @@ dependencies = [
 
 [[package]]
 name = "toml_edit"
+version = "0.19.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
+dependencies = [
+ "indexmap",
+ "toml_datetime 0.6.11",
+ "winnow 0.5.40",
+]
+
+[[package]]
+name = "toml_edit"
 version = "0.23.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6485ef6d0d9b5d0ec17244ff7eb05310113c3f316f2d14200d4de56b3cb98f8d"
 dependencies = [
  "indexmap",
- "toml_datetime",
+ "toml_datetime 0.7.3",
  "toml_parser",
- "winnow",
+ "winnow 0.7.13",
 ]
 
 [[package]]
@@ -1876,7 +2830,7 @@ version = "1.0.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c0cbe268d35bdb4bb5a56a2de88d0ad0eb70af5384a99d648cd4b3d04039800e"
 dependencies = [
- "winnow",
+ "winnow 0.7.13",
 ]
 
 [[package]]
@@ -1886,6 +2840,51 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2"
 
 [[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.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
+dependencies = [
+ "bitflags",
+ "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"
@@ -1904,7 +2903,7 @@ checksum = "81383ab64e72a7a8b8e13130c49e3dab29def6d0c7d76a03087b3cf71c5c6903"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -1933,10 +2932,42 @@ checksum = "dc19eb2373ccf3d1999967c26c3d44534ff71ae5d8b9dacf78f4b13132229e48"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "try-lock"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
+
+[[package]]
+name = "typed-builder"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cd9d30e3a08026c78f246b173243cf07b3696d274debd26680773b6773c2afc7"
+dependencies = [
+ "typed-builder-macro",
+]
+
+[[package]]
+name = "typed-builder-macro"
+version = "0.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c36781cc0e46a83726d9879608e4cf6c2505237e263a8eb8c24502989cfdb28"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
 ]
 
 [[package]]
+name = "unicase"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539"
+
+[[package]]
 name = "unicode-ident"
 version = "1.0.22"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1949,6 +2980,30 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493"
 
 [[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "url"
+version = "2.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "08bc136a29a3d1758e07a9cca267be308aeebf5cfd5a10f3f67ab2097683ef5b"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+]
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
 name = "utf8parse"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1967,6 +3022,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
 
 [[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
 name = "walkdir"
 version = "2.5.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1977,12 +3038,30 @@ 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.1+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
 
 [[package]]
+name = "wasip2"
+version = "1.0.1+wasi-0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0562428422c63773dad2c345a1882263bbf4d65cf3f42e90921f787ef5ad58e7"
+dependencies = [
+ "wit-bindgen",
+]
+
+[[package]]
 name = "wasm-bindgen"
 version = "0.2.105"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1996,6 +3075,19 @@ dependencies = [
 ]
 
 [[package]]
+name = "wasm-bindgen-futures"
+version = "0.4.55"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "551f88106c6d5e7ccc7cd9a16f312dd3b5d36ea8b4954304657d5dfba115d4a0"
+dependencies = [
+ "cfg-if",
+ "js-sys",
+ "once_cell",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
 name = "wasm-bindgen-macro"
 version = "0.2.105"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2014,7 +3106,7 @@ dependencies = [
  "bumpalo",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
  "wasm-bindgen-shared",
 ]
 
@@ -2028,12 +3120,35 @@ dependencies = [
 ]
 
 [[package]]
+name = "wasm-streams"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15053d8d85c7eccdbefef60f06769760a563c7f0a9d6902a13d35c7800b0ad65"
+dependencies = [
+ "futures-util",
+ "js-sys",
+ "wasm-bindgen",
+ "wasm-bindgen-futures",
+ "web-sys",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.82"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3a1f95c0d03a47f4ae1f7a64643a6bb97465d9b740f0fa8f90ea33915c99a9a1"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
 name = "winapi-util"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
 dependencies = [
- "windows-sys",
+ "windows-sys 0.61.2",
 ]
 
 [[package]]
@@ -2057,7 +3172,7 @@ checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -2068,7 +3183,7 @@ checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -2078,6 +3193,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
 
 [[package]]
+name = "windows-registry"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720"
+dependencies = [
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
 name = "windows-result"
 version = "0.4.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2097,6 +3223,24 @@ dependencies = [
 
 [[package]]
 name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[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.5",
+]
+
+[[package]]
+name = "windows-sys"
 version = "0.61.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
@@ -2105,6 +3249,144 @@ dependencies = [
 ]
 
 [[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "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",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.53.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
+dependencies = [
+ "windows-link",
+ "windows_aarch64_gnullvm 0.53.1",
+ "windows_aarch64_msvc 0.53.1",
+ "windows_i686_gnu 0.53.1",
+ "windows_i686_gnullvm 0.53.1",
+ "windows_i686_msvc 0.53.1",
+ "windows_x86_64_gnu 0.53.1",
+ "windows_x86_64_gnullvm 0.53.1",
+ "windows_x86_64_msvc 0.53.1",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
+
+[[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.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.53.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+
+[[package]]
+name = "winnow"
+version = "0.5.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
 name = "winnow"
 version = "0.7.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2114,6 +3396,41 @@ dependencies = [
 ]
 
 [[package]]
+name = "wit-bindgen"
+version = "0.46.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f17a85883d4e6d00e8a97c586de764dabcc06133f7f1d55dce5cdc070ad7fe59"
+
+[[package]]
+name = "writeable"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9edde0db4769d2dc68579893f2306b26c6ecfbe0ef499b013d731b7b9247e0b9"
+
+[[package]]
+name = "yoke"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72d6e5c6afb84d73944e5cedb052c4680d5657337201555f9f2a16b7406d4954"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+ "synstructure",
+]
+
+[[package]]
 name = "zerocopy"
 version = "0.8.28"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2130,7 +3447,67 @@ checksum = "c640b22cd9817fae95be82f0d2f90b11f7605f6c319d16705c459b27ac2cbc26"
 dependencies = [
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2a59c17a5562d507e4b54960e8569ebee33bee890c70aa3fe7b97e85a9fd7851"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c28719294829477f525be0186d13efa9a3c602f7ec202ca9e353d310fb9a002"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
 ]
 
 [[package]]
@@ -2141,7 +3518,7 @@ checksum = "2be61892e4f2b1772727be11630a62664a1826b62efa43a6fe7449521cb8744c"
 dependencies = [
  "endi",
  "serde",
- "winnow",
+ "winnow 0.7.13",
  "zvariant_derive",
  "zvariant_utils",
 ]
@@ -2152,10 +3529,10 @@ version = "5.8.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "da58575a1b2b20766513b1ec59d8e2e68db2745379f961f86650655e862d2006"
 dependencies = [
- "proc-macro-crate",
+ "proc-macro-crate 3.4.0",
  "proc-macro2",
  "quote",
- "syn",
+ "syn 2.0.110",
  "zvariant_utils",
 ]
 
@@ -2168,6 +3545,6 @@ dependencies = [
  "proc-macro2",
  "quote",
  "serde",
- "syn",
- "winnow",
+ "syn 2.0.110",
+ "winnow 0.7.13",
 ]
diff --git a/Cargo.toml b/Cargo.toml
index 13ff3a6..5f7af0f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -23,6 +23,8 @@ isolang = { git = "https://github.com/humenda/isolang-rs" }
 serde = { version = "1.0.228", features = ["derive"] }
 serde_json = "1.0.145"
 relm4-icons = "0.10"
+deepl = "0.7.3"
+gsettings-macro = "0.2.2"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 cocoa = "0.26"
diff --git a/resources/style.css b/data/style.css
index 44106e1..03dc022 100644
--- a/resources/style.css
+++ b/data/style.css
@@ -5,6 +5,10 @@
     border-radius: 12px;
 }
 
+.cue-view:disabled {
+  opacity: 0;
+}
+
 .cue-view link {
   color: @theme_fg_color;
   text-decoration: none;
diff --git a/flake.nix b/flake.nix
index af7d9f5..5469f03 100644
--- a/flake.nix
+++ b/flake.nix
@@ -52,6 +52,7 @@
           ];
 
           buildInputs = with pkgs; [
+            openssl
             gtk4
             libadwaita
             ffmpeg_8-full.dev
diff --git a/src/app.rs b/src/app.rs
index 951392e..bdb2ef9 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -5,33 +5,36 @@ use crate::{
     icon_names,
     open_dialog::{OpenDialog, OpenDialogMsg, OpenDialogOutput},
     player::{Player, PlayerMsg, PlayerOutput},
-    preferences::{Preferences, PreferencesMsg},
-    subtitle_extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput},
+    preferences_dialog::{PreferencesDialog, PreferencesDialogMsg},
     subtitle_selection_dialog::{
         SubtitleSelectionDialog, SubtitleSelectionDialogMsg, SubtitleSelectionDialogOutput,
+        SubtitleSettings,
     },
     subtitle_view::{SubtitleView, SubtitleViewMsg, SubtitleViewOutput},
-    tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue},
+    subtitles::{
+        MetadataCollection, SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack,
+        extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput},
+        state::SubtitleState,
+    },
     transcript::{Transcript, TranscriptMsg, TranscriptOutput},
+    translation::{DeeplTranslator, deepl::DeeplTranslatorMsg},
     util::Tracker,
 };
 
 pub struct App {
+    root: adw::ApplicationWindow,
     transcript: Controller<Transcript>,
     player: Controller<Player>,
     subtitle_view: Controller<SubtitleView>,
     extractor: WorkerController<SubtitleExtractor>,
+    deepl_translator: AsyncController<DeeplTranslator>,
 
-    preferences: Controller<Preferences>,
+    preferences: Controller<PreferencesDialog>,
     open_url_dialog: Controller<OpenDialog>,
-    subtitle_selection_dialog: Controller<SubtitleSelectionDialog>,
+    subtitle_selection_dialog: Option<Controller<SubtitleSelectionDialog>>,
 
-    primary_stream_ix: Option<StreamIndex>,
-    primary_cue: Tracker<Option<String>>,
-    primary_last_cue_ix: Tracker<Option<usize>>,
-    secondary_cue: Tracker<Option<String>>,
-    secondary_stream_ix: Option<StreamIndex>,
-    secondary_last_cue_ix: Tracker<Option<usize>>,
+    primary_subtitle_state: SubtitleState,
+    secondary_subtitle_state: SubtitleState,
 
     // for auto-pausing
     autopaused: bool,
@@ -40,10 +43,9 @@ pub struct App {
 
 #[derive(Debug)]
 pub enum AppMsg {
-    NewCue(StreamIndex, SubtitleCue),
+    AddCue(StreamIndex, SubtitleCue),
     SubtitleExtractionComplete,
-    PrimarySubtitleTrackSelected(Option<StreamIndex>),
-    SecondarySubtitleTrackSelected(Option<StreamIndex>),
+    ApplySubtitleSettings(SubtitleSettings),
     PositionUpdate(gst::ClockTime),
     SetHoveringSubtitleCue(bool),
     ShowUrlOpenDialog,
@@ -51,6 +53,7 @@ pub enum AppMsg {
     ShowSubtitleSelectionDialog,
     Play {
         url: String,
+        metadata: MetadataCollection,
         whisper_stream_index: Option<StreamIndex>,
     },
 }
@@ -123,52 +126,46 @@ impl SimpleComponent for App {
             sender.input_sender(),
             |output| match output {
                 SubtitleExtractorOutput::NewCue(stream_index, cue) => {
-                    AppMsg::NewCue(stream_index, cue)
+                    AppMsg::AddCue(stream_index, cue)
                 }
                 SubtitleExtractorOutput::ExtractionComplete => AppMsg::SubtitleExtractionComplete,
             },
         );
 
-        let preferences = Preferences::builder().launch(root.clone().into()).detach();
+        let deepl_translator = DeeplTranslator::builder().launch(()).detach();
+
+        let preferences = PreferencesDialog::builder()
+            .launch(root.clone().into())
+            .detach();
         let open_url_dialog = OpenDialog::builder().launch(root.clone().into()).forward(
             sender.input_sender(),
             |output| match output {
                 OpenDialogOutput::Play {
                     url,
+                    metadata,
                     whisper_stream_index,
                 } => AppMsg::Play {
                     url,
+                    metadata,
                     whisper_stream_index,
                 },
             },
         );
-        let subtitle_selection_dialog = SubtitleSelectionDialog::builder()
-            .launch(root.clone().into())
-            .forward(sender.input_sender(), |output| match output {
-                SubtitleSelectionDialogOutput::PrimaryTrackSelected(ix) => {
-                    AppMsg::PrimarySubtitleTrackSelected(ix)
-                }
-                SubtitleSelectionDialogOutput::SecondaryTrackSelected(ix) => {
-                    AppMsg::SecondarySubtitleTrackSelected(ix)
-                }
-            });
 
         let model = Self {
+            root: root.clone(),
             player,
             transcript,
             subtitle_view,
             extractor,
+            deepl_translator,
 
             preferences,
             open_url_dialog,
-            subtitle_selection_dialog,
+            subtitle_selection_dialog: None,
 
-            primary_stream_ix: None,
-            primary_cue: Tracker::new(None),
-            primary_last_cue_ix: Tracker::new(None),
-            secondary_stream_ix: None,
-            secondary_cue: Tracker::new(None),
-            secondary_last_cue_ix: Tracker::new(None),
+            primary_subtitle_state: SubtitleState::default(),
+            secondary_subtitle_state: SubtitleState::default(),
 
             autopaused: false,
             hovering_primary_cue: false,
@@ -179,94 +176,45 @@ impl SimpleComponent for App {
         ComponentParts { model, widgets }
     }
 
-    fn update(&mut self, message: Self::Input, _sender: ComponentSender<Self>) {
+    fn update(&mut self, message: Self::Input, sender: ComponentSender<Self>) {
         match message {
-            AppMsg::NewCue(stream_index, cue) => {
+            AppMsg::AddCue(stream_ix, cue) => {
+                SUBTITLE_TRACKS
+                    .write()
+                    .get_mut(&stream_ix)
+                    .unwrap()
+                    .push_cue(cue.clone());
+
                 self.transcript
                     .sender()
-                    .send(TranscriptMsg::NewCue(stream_index, cue))
+                    .send(TranscriptMsg::NewCue(stream_ix, cue))
                     .unwrap();
             }
             AppMsg::SubtitleExtractionComplete => {
                 log::info!("Subtitle extraction complete");
             }
-            AppMsg::PrimarySubtitleTrackSelected(stream_index) => {
-                self.primary_stream_ix = stream_index;
+            AppMsg::ApplySubtitleSettings(settings) => {
+                self.primary_subtitle_state
+                    .set_stream_ix(settings.primary_track_ix);
+                self.secondary_subtitle_state
+                    .set_stream_ix(settings.secondary_track_ix);
 
                 self.transcript
                     .sender()
-                    .send(TranscriptMsg::SelectTrack(stream_index))
+                    .send(TranscriptMsg::SelectTrack(settings.primary_track_ix))
+                    .unwrap();
+                self.deepl_translator
+                    .sender()
+                    .send(DeeplTranslatorMsg::SelectTrack(settings.primary_track_ix))
                     .unwrap();
-            }
-            AppMsg::SecondarySubtitleTrackSelected(stream_index) => {
-                self.secondary_stream_ix = stream_index;
-            }
-            AppMsg::PositionUpdate(pos) => {
-                if let Some(stream_ix) = self.primary_stream_ix {
-                    // sometimes we get a few position update messages after
-                    // auto-pausing; this prevents us from immediately un-autopausing
-                    // again
-                    if self.autopaused {
-                        return;
-                    }
-
-                    let cue_was_some = self.primary_cue.get().is_some();
-
-                    Self::update_cue(
-                        stream_ix,
-                        pos,
-                        &mut self.primary_cue,
-                        &mut self.primary_last_cue_ix,
-                    );
-
-                    if self.primary_cue.is_dirty() {
-                        // last cue just ended -> auto-pause
-                        if cue_was_some && self.hovering_primary_cue {
-                            self.player.sender().send(PlayerMsg::Pause).unwrap();
-                            self.autopaused = true;
-                            return;
-                        }
-
-                        self.subtitle_view
-                            .sender()
-                            .send(SubtitleViewMsg::SetPrimaryCue(
-                                self.primary_cue.get().clone(),
-                            ))
-                            .unwrap();
-
-                        self.primary_cue.reset();
-                    }
-
-                    if self.primary_last_cue_ix.is_dirty() {
-                        if let Some(ix) = self.primary_last_cue_ix.get() {
-                            self.transcript
-                                .sender()
-                                .send(TranscriptMsg::ScrollToCue(*ix))
-                                .unwrap();
-                        }
 
-                        self.primary_last_cue_ix.reset();
-                    }
-                }
-                if let Some(stream_ix) = self.secondary_stream_ix {
-                    Self::update_cue(
-                        stream_ix,
-                        pos,
-                        &mut self.secondary_cue,
-                        &mut self.secondary_last_cue_ix,
-                    );
-
-                    if !self.autopaused && self.secondary_cue.is_dirty() {
-                        self.subtitle_view
-                            .sender()
-                            .send(SubtitleViewMsg::SetSecondaryCue(
-                                self.secondary_cue.get().clone(),
-                            ))
-                            .unwrap();
-
-                        self.secondary_cue.reset();
-                    }
-                }
+                self.subtitle_view
+                    .sender()
+                    .send(SubtitleViewMsg::ApplySubtitleSettings(settings))
+                    .unwrap();
+            }
+            AppMsg::PositionUpdate(position) => {
+                self.update_subtitle_states(position);
             }
             AppMsg::SetHoveringSubtitleCue(hovering) => {
                 self.hovering_primary_cue = hovering;
@@ -284,17 +232,20 @@ impl SimpleComponent for App {
             AppMsg::ShowPreferences => {
                 self.preferences
                     .sender()
-                    .send(PreferencesMsg::Show)
+                    .send(PreferencesDialogMsg::Show)
                     .unwrap();
             }
             AppMsg::ShowSubtitleSelectionDialog => {
-                self.subtitle_selection_dialog
-                    .sender()
-                    .send(SubtitleSelectionDialogMsg::Show)
-                    .unwrap();
+                if let Some(ref dialog) = self.subtitle_selection_dialog {
+                    dialog
+                        .sender()
+                        .send(SubtitleSelectionDialogMsg::Show)
+                        .unwrap();
+                }
             }
             AppMsg::Play {
                 url,
+                metadata,
                 whisper_stream_index,
             } => {
                 self.player
@@ -308,70 +259,128 @@ impl SimpleComponent for App {
                         whisper_stream_index,
                     })
                     .unwrap();
+
+                let subtitle_selection_dialog = SubtitleSelectionDialog::builder()
+                    .launch((self.root.clone().into(), metadata))
+                    .forward(sender.input_sender(), |output| match output {
+                        SubtitleSelectionDialogOutput::ApplySubtitleSettings(settings) => {
+                            AppMsg::ApplySubtitleSettings(settings)
+                        }
+                    });
+                self.subtitle_selection_dialog = Some(subtitle_selection_dialog);
             }
         }
     }
 }
 
 impl App {
-    fn update_cue(
-        stream_ix: StreamIndex,
-        position: gst::ClockTime,
-        cue: &mut Tracker<Option<String>>,
-        last_cue_ix: &mut Tracker<Option<usize>>,
-    ) {
-        let lock = SUBTITLE_TRACKS.read();
-        let track = lock.get(&stream_ix).unwrap();
+    fn update_subtitle_states(&mut self, position: gst::ClockTime) {
+        self.update_primary_subtitle_state(position);
+        self.update_secondary_subtitle_state(position);
+    }
 
-        // try to find current cue quickly (should usually succeed during playback)
-        if let Some(ix) = last_cue_ix.get() {
-            let last_cue = track.cues.get(*ix).unwrap();
-            if last_cue.start <= position && position <= last_cue.end {
-                // still at current cue
-                return;
-            } else if let Some(next_cue) = track.cues.get(ix + 1) {
-                if last_cue.end < position && position < next_cue.start {
-                    // strictly between cues
-                    cue.set(None);
-                    return;
-                }
-                if next_cue.start <= position && position <= next_cue.end {
-                    // already in next cue (this happens when one cue immediately
-                    // follows the previous one)
-                    cue.set(Some(next_cue.text.clone()));
-                    last_cue_ix.set(Some(ix + 1));
-                    return;
-                }
+    fn update_primary_subtitle_state(&mut self, position: gst::ClockTime) {
+        // sometimes we get a few position update messages after
+        // auto-pausing
+        if self.autopaused {
+            return;
+        }
+
+        update_subtitle_state(&mut self.primary_subtitle_state, position);
+
+        // last cue just ended -> auto-pause
+        if self.primary_subtitle_state.last_ended_cue_ix.is_dirty() && self.hovering_primary_cue {
+            self.player.sender().send(PlayerMsg::Pause).unwrap();
+            self.autopaused = true;
+            return;
+        }
+
+        if self.primary_subtitle_state.is_dirty() {
+            let cue = self.primary_subtitle_state.active_cue();
+
+            self.subtitle_view
+                .sender()
+                .send(SubtitleViewMsg::SetPrimaryCue(cue))
+                .unwrap();
+        }
+
+        if self.primary_subtitle_state.last_started_cue_ix.is_dirty() {
+            if let Some(ix) = *self.primary_subtitle_state.last_started_cue_ix {
+                self.transcript
+                    .sender()
+                    .send(TranscriptMsg::ScrollToCue(ix))
+                    .unwrap();
             }
         }
 
-        // if we are before the first subtitle, no need to look further
-        if track.cues.is_empty() || position < track.cues.first().unwrap().start {
-            cue.set(None);
-            last_cue_ix.set(None);
+        self.primary_subtitle_state.reset();
+    }
+
+    fn update_secondary_subtitle_state(&mut self, position: gst::ClockTime) {
+        // sometimes we get a few position update messages after
+        // auto-pausing
+        if self.autopaused {
             return;
         }
 
-        // otherwise, search the whole track (e.g. after seeking)
-        match track
-            .cues
-            .iter()
-            .enumerate()
-            .rev()
-            .find(|(_ix, cue)| cue.start <= position)
-        {
-            Some((ix, new_cue)) => {
-                last_cue_ix.set(Some(ix));
-                if position <= new_cue.end {
-                    cue.set(Some(new_cue.text.clone()));
-                } else {
-                    cue.set(None);
-                }
+        update_subtitle_state(&mut self.secondary_subtitle_state, position);
+
+        if self.secondary_subtitle_state.is_dirty() {
+            let cue = self.secondary_subtitle_state.active_cue();
+
+            self.subtitle_view
+                .sender()
+                .send(SubtitleViewMsg::SetSecondaryCue(cue))
+                .unwrap();
+        }
+
+        self.secondary_subtitle_state.reset();
+    }
+}
+
+fn update_subtitle_state(state: &mut SubtitleState, position: gst::ClockTime) {
+    if let Some(stream_ix) = state.stream_ix {
+        let lock = SUBTITLE_TRACKS.read();
+        let track = lock.get(&stream_ix).unwrap();
+
+        update_last_time_ix(&track.start_times, &mut state.last_started_cue_ix, position);
+        update_last_time_ix(&track.end_times, &mut state.last_ended_cue_ix, position);
+    }
+}
+
+fn update_last_time_ix(
+    times: &Vec<gst::ClockTime>,
+    last_time_ix: &mut Tracker<Option<usize>>,
+    current_time: gst::ClockTime,
+) {
+    // try to find index quickly (should succeed during normal playback)
+    if let Some(ix) = last_time_ix.get() {
+        let t0 = times.get(*ix).unwrap();
+        match (times.get(ix + 1), times.get(ix + 2)) {
+            (None, _) if current_time >= *t0 => {
+                return;
             }
-            None => {
-                cue.set(None);
-                last_cue_ix.set(None);
+            (Some(t1), _) if current_time >= *t0 && current_time < *t1 => {
+                return;
             }
-        };
+            (Some(t1), None) if current_time >= *t1 => {
+                last_time_ix.set(Some(ix + 1));
+                return;
+            }
+            (Some(t1), Some(t2)) if current_time >= *t1 && current_time < *t2 => {
+                last_time_ix.set(Some(ix + 1));
+                return;
+            }
+            _ => {}
+        }
+    }
+
+    // if we are before the first timestamp, no need to look further
+    if times.is_empty() || current_time < *times.first().unwrap() {
+        last_time_ix.set_if_ne(None);
+        return;
     }
+
+    // otherwise, search the whole array (e.g. after seeking)
+    last_time_ix.set(times.iter().rposition(|time| *time <= current_time));
 }
diff --git a/src/cue_view.rs b/src/cue_view.rs
index fbf2520..05c45c4 100644
--- a/src/cue_view.rs
+++ b/src/cue_view.rs
@@ -8,18 +8,25 @@ use relm4::prelude::*;
 use relm4::{ComponentParts, SimpleComponent};
 use unicode_segmentation::UnicodeSegmentation;
 
+use crate::subtitles::state::CueAddress;
+use crate::translation::TRANSLATIONS;
 use crate::util::Tracker;
 
-pub struct CueView {
-    text: Tracker<Option<String>>,
+pub struct ActiveCueViewState {
+    addr: CueAddress,
+    text: String,
     // byte ranges for the words in `text`
     word_ranges: Vec<Range<usize>>,
 }
 
+pub struct CueView {
+    state: Tracker<Option<ActiveCueViewState>>,
+}
+
 #[derive(Debug)]
 pub enum CueViewMsg {
     // messages from the app
-    SetText(Option<String>),
+    SetCue(Option<CueAddress>),
     // messages from UI
     MouseMotion,
 }
@@ -42,7 +49,7 @@ impl SimpleComponent for CueView {
         gtk::Label {
             add_controller: event_controller.clone(),
             set_use_markup: true,
-            set_visible: false,
+            set_sensitive: false,
             set_justify: gtk::Justification::Center,
             add_css_class: "cue-view",
         },
@@ -71,8 +78,7 @@ impl SimpleComponent for CueView {
         sender: relm4::ComponentSender<Self>,
     ) -> relm4::ComponentParts<Self> {
         let model = Self {
-            text: Tracker::new(None),
-            word_ranges: Vec::new(),
+            state: Tracker::new(None),
         };
 
         let widgets = view_output!();
@@ -81,19 +87,26 @@ impl SimpleComponent for CueView {
     }
 
     fn update(&mut self, message: Self::Input, _sender: relm4::ComponentSender<Self>) {
-        match message {
-            CueViewMsg::SetText(text) => {
-                self.text.set(text);
+        self.state.reset();
 
-                if let Some(text) = self.text.get() {
-                    self.word_ranges = UnicodeSegmentation::unicode_word_indices(text.as_str())
+        match message {
+            CueViewMsg::SetCue(addr) => {
+                if let Some(addr) = addr {
+                    let text = addr.resolve_text();
+                    let word_ranges = UnicodeSegmentation::unicode_word_indices(text.as_str())
                         .map(|(offset, slice)| Range {
                             start: offset,
                             end: offset + slice.len(),
                         })
                         .collect();
+
+                    self.state.set(Some(ActiveCueViewState {
+                        addr,
+                        text,
+                        word_ranges,
+                    }))
                 } else {
-                    self.word_ranges = Vec::new();
+                    self.state.set(None);
                 }
             }
             CueViewMsg::MouseMotion => {
@@ -103,11 +116,16 @@ impl SimpleComponent for CueView {
     }
 
     fn post_view() {
-        if self.text.is_dirty() {
-            if let Some(text) = self.text.get() {
+        if self.state.is_dirty() {
+            if let Some(ActiveCueViewState {
+                addr: _,
+                text,
+                word_ranges,
+            }) = self.state.get()
+            {
                 let mut markup = String::new();
 
-                let mut it = self.word_ranges.iter().enumerate().peekable();
+                let mut it = word_ranges.iter().enumerate().peekable();
                 if let Some((_, first_word_range)) = it.peek() {
                     markup.push_str(
                         glib::markup_escape_text(&text[..first_word_range.start]).as_str(),
@@ -127,24 +145,35 @@ impl SimpleComponent for CueView {
                     markup.push_str(glib::markup_escape_text(&text[next_gap_range]).as_str());
                 }
 
-                widgets.label.set_markup(markup.as_str());
-                widgets.label.set_visible(true);
+                widgets.label.set_markup(&markup);
+                widgets.label.set_sensitive(true);
             } else {
-                widgets.label.set_visible(false);
+                // insensitive = invisible by css
+                widgets.label.set_sensitive(false);
             }
         }
 
-        if let Some(word_ix_str) = widgets.label.current_uri() {
-            let range = self
-                .word_ranges
-                .get(usize::from_str(word_ix_str.as_str()).unwrap())
-                .unwrap();
-            widgets
-                .popover_label
-                .set_text(&self.text.get().as_ref().unwrap()[range.clone()]);
+        if let (
+            Some(ActiveCueViewState {
+                addr: CueAddress(stream_ix, cue_ix),
+                text: _,
+                word_ranges,
+            }),
+            Some(word_ix_str),
+        ) = (self.state.get(), widgets.label.current_uri())
+        {
+            let word_ix = usize::from_str(word_ix_str.as_str()).unwrap();
+
+            {
+                // TODO get translation
+                widgets.popover_label.set_text(word_ix_str.as_str());
+            }
+
+            let range = word_ranges.get(word_ix).unwrap();
             widgets
                 .popover
                 .set_pointing_to(Some(&Self::get_rect_of_byte_range(&widgets.label, &range)));
+
             widgets.popover.popup();
         } else {
             widgets.popover.popdown();
diff --git a/src/main.rs b/src/main.rs
index f010c6a..69ccb38 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -2,13 +2,14 @@ mod app;
 mod cue_view;
 mod open_dialog;
 mod player;
-mod preferences;
-mod subtitle_extraction;
+mod preferences_dialog;
+mod settings;
 mod subtitle_selection_dialog;
 mod subtitle_view;
+mod subtitles;
 mod track_selector;
-mod tracks;
 mod transcript;
+mod translation;
 mod util;
 
 use gtk::{CssProvider, STYLE_PROVIDER_PRIORITY_APPLICATION, gdk, glib};
@@ -38,7 +39,7 @@ fn main() {
 
     let css_provider = CssProvider::new();
     css_provider.load_from_bytes(&glib::Bytes::from_static(include_bytes!(
-        "../resources/style.css"
+        "../data/style.css"
     )));
     gtk::style_context_add_provider_for_display(
         &gdk::Display::default().unwrap(),
diff --git a/src/open_dialog.rs b/src/open_dialog.rs
index 2f17c59..3b822be 100644
--- a/src/open_dialog.rs
+++ b/src/open_dialog.rs
@@ -5,10 +5,10 @@ use gtk::gio;
 use gtk::glib::clone;
 use relm4::prelude::*;
 
+use crate::subtitles::{MetadataCollection, StreamIndex, TrackMetadata};
 use crate::track_selector::{
     TrackInfo, TrackSelector, TrackSelectorInit, TrackSelectorMsg, TrackSelectorOutput,
 };
-use crate::tracks::{StreamIndex, TrackMetadata};
 use crate::util::Tracker;
 
 pub struct OpenDialog {
@@ -23,6 +23,7 @@ pub struct OpenDialog {
     whisper_stream_index: Option<StreamIndex>,
 
     metadata_command_running: bool,
+    metadata: Option<MetadataCollection>,
 }
 
 #[derive(Debug)]
@@ -34,7 +35,7 @@ pub enum OpenDialogMsg {
     FileSelected(gio::File),
     UrlChanged(String),
     SetDoWhisperExtraction(bool),
-    WhisperTrackSelected(Option<StreamIndex>),
+    WhisperTrackSelected(StreamIndex),
     Play,
 }
 
@@ -42,6 +43,7 @@ pub enum OpenDialogMsg {
 pub enum OpenDialogOutput {
     Play {
         url: String,
+        metadata: MetadataCollection,
         whisper_stream_index: Option<StreamIndex>,
     },
 }
@@ -51,7 +53,7 @@ impl Component for OpenDialog {
     type Init = adw::ApplicationWindow;
     type Input = OpenDialogMsg;
     type Output = OpenDialogOutput;
-    type CommandOutput = Result<BTreeMap<StreamIndex, TrackMetadata>, ffmpeg::Error>;
+    type CommandOutput = Result<MetadataCollection, ffmpeg::Error>;
 
     view! {
         #[root]
@@ -186,6 +188,7 @@ impl Component for OpenDialog {
             whisper_stream_index: None,
 
             metadata_command_running: false,
+            metadata: None,
         };
 
         let widgets = view_output!();
@@ -227,23 +230,28 @@ impl Component for OpenDialog {
                 self.url.set(file.uri().into());
             }
             OpenDialogMsg::Play => {
-                sender
-                    .output(OpenDialogOutput::Play {
-                        url: self.url.get().clone(),
-                        whisper_stream_index: if self.do_whisper_extraction {
-                            self.whisper_stream_index
-                        } else {
-                            None
-                        },
-                    })
-                    .unwrap();
-                self.dialog.close();
+                if let Some(ref metadata) = self.metadata {
+                    sender
+                        .output(OpenDialogOutput::Play {
+                            url: self.url.get().clone(),
+                            metadata: metadata.clone(),
+                            whisper_stream_index: if self.do_whisper_extraction {
+                                self.whisper_stream_index
+                            } else {
+                                None
+                            },
+                        })
+                        .unwrap();
+                    self.dialog.close();
+                } else {
+                    log::error!("metadata is unavailable, can't play");
+                }
             }
             OpenDialogMsg::SetDoWhisperExtraction(val) => {
                 self.do_whisper_extraction = val;
             }
             OpenDialogMsg::WhisperTrackSelected(track_index) => {
-                self.whisper_stream_index = track_index;
+                self.whisper_stream_index = Some(track_index);
             }
         }
     }
@@ -259,10 +267,10 @@ impl Component for OpenDialog {
         self.metadata_command_running = false;
 
         match message {
-            Ok(audio_tracks) => {
+            Ok(metadata) => {
                 let list_model = gio::ListStore::new::<TrackInfo>();
 
-                for (&stream_index, track) in audio_tracks.iter() {
+                for (&stream_index, track) in metadata.audio.iter() {
                     let track_info = TrackInfo::new(
                         stream_index,
                         track.language.map(|lang| lang.to_name()),
@@ -276,6 +284,8 @@ impl Component for OpenDialog {
                     .send(TrackSelectorMsg::SetListModel(list_model))
                     .unwrap();
 
+                self.metadata = Some(metadata);
+
                 self.next();
             }
             Err(e) => {
@@ -302,7 +312,7 @@ impl OpenDialog {
         sender.spawn_oneshot_command(move || {
             let input = ffmpeg::format::input(&url)?;
 
-            let audio_tracks = input
+            let audio = input
                 .streams()
                 .filter_map(|stream| {
                     if stream.parameters().medium() == ffmpeg::media::Type::Audio {
@@ -312,8 +322,18 @@ impl OpenDialog {
                     }
                 })
                 .collect::<BTreeMap<_, _>>();
+            let subtitles = input
+                .streams()
+                .filter_map(|stream| {
+                    if stream.parameters().medium() == ffmpeg::media::Type::Subtitle {
+                        Some((stream.index(), TrackMetadata::from_ffmpeg_stream(&stream)))
+                    } else {
+                        None
+                    }
+                })
+                .collect::<BTreeMap<_, _>>();
 
-            Ok(audio_tracks)
+            Ok(MetadataCollection { audio, subtitles })
         });
 
         self.metadata_command_running = true;
diff --git a/src/preferences.rs b/src/preferences_dialog.rs
index c5f9bb1..5aacfe8 100644
--- a/src/preferences.rs
+++ b/src/preferences_dialog.rs
@@ -1,25 +1,23 @@
 use adw::prelude::*;
-use gtk::gio;
 use relm4::prelude::*;
 
-pub struct Preferences {
+use crate::settings::Settings;
+
+pub struct PreferencesDialog {
     parent_window: adw::ApplicationWindow,
     dialog: adw::PreferencesDialog,
 }
 
 #[derive(Debug)]
-pub enum PreferencesMsg {
+pub enum PreferencesDialogMsg {
     Show,
 }
 
-#[derive(Debug)]
-pub enum PreferencesOutput {}
-
 #[relm4::component(pub)]
-impl SimpleComponent for Preferences {
+impl SimpleComponent for PreferencesDialog {
     type Init = adw::ApplicationWindow;
-    type Input = PreferencesMsg;
-    type Output = PreferencesOutput;
+    type Input = PreferencesDialogMsg;
+    type Output = ();
 
     view! {
         #[root]
@@ -33,12 +31,9 @@ impl SimpleComponent for Preferences {
             adw::PreferencesGroup {
                 set_title: "Machine Translation",
 
+                #[name(deepl_api_key_row)]
                 adw::EntryRow {
                     set_title: "DeepL API key",
-                    set_text: settings.string("deepl-api-key").as_str(),
-                    connect_changed[settings] => move |entry| {
-                        settings.set_string("deepl-api-key", entry.text().as_str()).unwrap()
-                    }
                 },
             }
         }
@@ -49,7 +44,7 @@ impl SimpleComponent for Preferences {
         root: Self::Root,
         _sender: ComponentSender<Self>,
     ) -> ComponentParts<Self> {
-        let settings = gio::Settings::new("tc.mal.lleap");
+        let settings = Settings::default();
 
         let model = Self {
             parent_window,
@@ -58,12 +53,16 @@ impl SimpleComponent for Preferences {
 
         let widgets = view_output!();
 
+        settings
+            .bind_deepl_api_key(&widgets.deepl_api_key_row, "text")
+            .build();
+
         ComponentParts { model, widgets }
     }
 
     fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
         match msg {
-            PreferencesMsg::Show => {
+            PreferencesDialogMsg::Show => {
                 self.dialog.present(Some(&self.parent_window));
             }
         }
diff --git a/src/settings.rs b/src/settings.rs
new file mode 100644
index 0000000..eb1f6b9
--- /dev/null
+++ b/src/settings.rs
@@ -0,0 +1,5 @@
+use gsettings_macro::gen_settings;
+use gtk::{gio, glib};
+
+#[gen_settings(file = "./data/tc.mal.lleap.gschema.xml", id = "tc.mal.lleap")]
+pub struct Settings;
diff --git a/src/subtitle_selection_dialog.rs b/src/subtitle_selection_dialog.rs
index 6136d56..8e5d283 100644
--- a/src/subtitle_selection_dialog.rs
+++ b/src/subtitle_selection_dialog.rs
@@ -2,37 +2,47 @@ use adw::prelude::*;
 use gtk::gio;
 use relm4::prelude::*;
 
+use crate::subtitles::{MetadataCollection, StreamIndex};
 use crate::track_selector::{
     TrackInfo, TrackSelector, TrackSelectorInit, TrackSelectorMsg, TrackSelectorOutput,
 };
-use crate::tracks::{SUBTITLE_TRACKS, StreamIndex};
+
+#[derive(Clone, Copy, Default, Debug)]
+pub struct SubtitleSettings {
+    pub primary_track_ix: Option<StreamIndex>,
+    pub secondary_track_ix: Option<StreamIndex>,
+    pub show_secondary: bool,
+    pub show_machine_translation: bool,
+}
 
 pub struct SubtitleSelectionDialog {
     parent_window: adw::ApplicationWindow,
     dialog: adw::PreferencesDialog,
-    track_list_model: gio::ListStore,
     primary_selector: Controller<TrackSelector>,
     secondary_selector: Controller<TrackSelector>,
-    primary_track_ix: Option<StreamIndex>,
-    secondary_track_ix: Option<StreamIndex>,
+
+    settings: SubtitleSettings,
 }
 
 #[derive(Debug)]
 pub enum SubtitleSelectionDialogMsg {
     Show,
-    PrimaryTrackChanged(Option<StreamIndex>),
-    SecondaryTrackChanged(Option<StreamIndex>),
+    Close,
+    // ui messages
+    PrimaryTrackChanged(StreamIndex),
+    SecondaryTrackChanged(StreamIndex),
+    ShowSecondaryChanged(bool),
+    ShowMachineTranslationChanged(bool),
 }
 
 #[derive(Debug)]
 pub enum SubtitleSelectionDialogOutput {
-    PrimaryTrackSelected(Option<StreamIndex>),
-    SecondaryTrackSelected(Option<StreamIndex>),
+    ApplySubtitleSettings(SubtitleSettings),
 }
 
 #[relm4::component(pub)]
 impl SimpleComponent for SubtitleSelectionDialog {
-    type Init = adw::ApplicationWindow;
+    type Init = (adw::ApplicationWindow, MetadataCollection);
     type Input = SubtitleSelectionDialogMsg;
     type Output = SubtitleSelectionDialogOutput;
 
@@ -41,22 +51,50 @@ impl SimpleComponent for SubtitleSelectionDialog {
         adw::PreferencesDialog {
             set_title: "Subtitle Settings",
             add: &page,
+            connect_closed => SubtitleSelectionDialogMsg::Close,
         },
 
         #[name(page)]
         adw::PreferencesPage {
             adw::PreferencesGroup {
                 model.primary_selector.widget(),
-                model.secondary_selector.widget(),
+
+                adw::ExpanderRow {
+                    set_title: "Show secondary subtitles",
+                    set_subtitle: "Enable this if there exist subtitles a language you already know",
+                    set_show_enable_switch: true,
+                    #[watch]
+                    set_enable_expansion: model.settings.show_secondary,
+                    connect_enable_expansion_notify[sender] => move |expander_row| {
+                        sender.input(SubtitleSelectionDialogMsg::ShowSecondaryChanged(expander_row.enables_expansion()))
+                    },
+
+                    add_row: model.secondary_selector.widget(),
+                },
+
+                adw::ExpanderRow {
+                    set_title: "Show machine translations",
+                    set_subtitle: "This is useful in case there are no subtitles in your native language or you prefer a more direct translation of the primary subtitles",
+                    set_show_enable_switch: true,
+                    #[watch]
+                    set_enable_expansion: model.settings.show_machine_translation,
+                    connect_enable_expansion_notify[sender] => move |expander_row| {
+                        sender.input(SubtitleSelectionDialogMsg::ShowMachineTranslationChanged(expander_row.enables_expansion()))
+                    },
+
+                    // TODO add row for language selection
+                },
             }
         },
     }
 
     fn init(
-        parent_window: Self::Init,
+        init: Self::Init,
         root: Self::Root,
         sender: ComponentSender<Self>,
     ) -> ComponentParts<Self> {
+        let (parent_window, metadata) = init;
+
         let primary_selector = TrackSelector::builder()
             .launch(TrackSelectorInit {
                 title: "Primary subtitle track",
@@ -81,73 +119,59 @@ impl SimpleComponent for SubtitleSelectionDialog {
         let model = Self {
             parent_window,
             dialog: root.clone(),
-            track_list_model: gio::ListStore::new::<TrackInfo>(),
             primary_selector,
             secondary_selector,
-            primary_track_ix: None,
-            secondary_track_ix: None,
+            settings: Default::default(),
         };
 
         let widgets = view_output!();
 
+        let track_list_model = gio::ListStore::new::<TrackInfo>();
+        for (&stream_index, track_metadata) in metadata.subtitles.iter() {
+            let track_info = TrackInfo::new(
+                stream_index,
+                track_metadata.language.map(|lang| lang.to_name()),
+                track_metadata.title.clone(),
+            );
+            track_list_model.append(&track_info);
+        }
+
+        model
+            .primary_selector
+            .sender()
+            .send(TrackSelectorMsg::SetListModel(track_list_model.clone()))
+            .unwrap();
+        model
+            .secondary_selector
+            .sender()
+            .send(TrackSelectorMsg::SetListModel(track_list_model.clone()))
+            .unwrap();
+
         ComponentParts { model, widgets }
     }
 
     fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
         match msg {
             SubtitleSelectionDialogMsg::Show => {
-                self.update_track_list_model();
-
-                self.primary_selector
-                    .sender()
-                    .send(TrackSelectorMsg::SetListModel(
-                        self.track_list_model.clone(),
-                    ))
-                    .unwrap();
-                self.secondary_selector
-                    .sender()
-                    .send(TrackSelectorMsg::SetListModel(
-                        self.track_list_model.clone(),
-                    ))
-                    .unwrap();
-
                 self.dialog.present(Some(&self.parent_window));
             }
-            SubtitleSelectionDialogMsg::PrimaryTrackChanged(stream_index) => {
-                self.primary_track_ix = stream_index;
-                sender
-                    .output(SubtitleSelectionDialogOutput::PrimaryTrackSelected(
-                        stream_index,
-                    ))
-                    .unwrap();
+            SubtitleSelectionDialogMsg::Close => sender
+                .output(SubtitleSelectionDialogOutput::ApplySubtitleSettings(
+                    self.settings,
+                ))
+                .unwrap(),
+            SubtitleSelectionDialogMsg::PrimaryTrackChanged(stream_ix) => {
+                self.settings.primary_track_ix = Some(stream_ix);
             }
-            SubtitleSelectionDialogMsg::SecondaryTrackChanged(stream_index) => {
-                self.secondary_track_ix = stream_index;
-                sender
-                    .output(SubtitleSelectionDialogOutput::SecondaryTrackSelected(
-                        stream_index,
-                    ))
-                    .unwrap();
+            SubtitleSelectionDialogMsg::SecondaryTrackChanged(stream_ix) => {
+                self.settings.secondary_track_ix = Some(stream_ix);
+            }
+            SubtitleSelectionDialogMsg::ShowSecondaryChanged(val) => {
+                self.settings.show_secondary = val;
+            }
+            SubtitleSelectionDialogMsg::ShowMachineTranslationChanged(val) => {
+                self.settings.show_machine_translation = val;
             }
-        }
-    }
-}
-
-impl SubtitleSelectionDialog {
-    fn update_track_list_model(&mut self) {
-        let tracks = SUBTITLE_TRACKS.read();
-
-        // Clear previous entries
-        self.track_list_model.remove_all();
-
-        // Add all available tracks
-        for (&stream_index, track) in tracks.iter() {
-            let track_info = TrackInfo::new(
-                stream_index,
-                track.metadata.language.map(|lang| lang.to_name()),
-                track.metadata.title.clone(),
-            );
-            self.track_list_model.append(&track_info);
         }
     }
 }
diff --git a/src/subtitle_view.rs b/src/subtitle_view.rs
index fd98c60..4de73dd 100644
--- a/src/subtitle_view.rs
+++ b/src/subtitle_view.rs
@@ -1,17 +1,22 @@
 use crate::cue_view::{CueView, CueViewMsg, CueViewOutput};
-use crate::util::Tracker;
+use crate::subtitle_selection_dialog::SubtitleSettings;
+use crate::subtitles::state::CueAddress;
 use gtk::prelude::*;
 use relm4::prelude::*;
 
 pub struct SubtitleView {
     primary_cue: Controller<CueView>,
-    secondary_cue: Tracker<Option<String>>,
+    secondary_cue: Option<String>,
+    machine_translation: Option<String>,
+    show_secondary: bool,
+    show_machine_translation: bool,
 }
 
 #[derive(Debug)]
 pub enum SubtitleViewMsg {
-    SetPrimaryCue(Option<String>),
-    SetSecondaryCue(Option<String>),
+    SetPrimaryCue(Option<CueAddress>),
+    SetSecondaryCue(Option<CueAddress>),
+    ApplySubtitleSettings(SubtitleSettings),
 }
 
 #[derive(Debug)]
@@ -39,12 +44,30 @@ impl SimpleComponent for SubtitleView {
                 model.primary_cue.widget(),
 
                 gtk::Box {
+                    #[watch]
+                    set_visible: model.show_secondary,
                     set_vexpand: true,
                 },
 
                 gtk::Label {
-                    #[track = "model.secondary_cue.is_dirty()"]
-                    set_text: model.secondary_cue.get().as_ref().map(|val| val.as_str()).unwrap_or(""),
+                    #[watch]
+                    set_text: model.secondary_cue.as_ref().map(|val| val.as_str()).unwrap_or(""),
+                    #[watch]
+                    set_visible: model.show_secondary,
+                    set_justify: gtk::Justification::Center,
+                },
+
+                gtk::Box {
+                    #[watch]
+                    set_visible: model.show_machine_translation,
+                    set_vexpand: true,
+                },
+
+                gtk::Label {
+                    #[watch]
+                    set_text: model.machine_translation.as_ref().map(|val| val.as_str()).unwrap_or(""),
+                    #[watch]
+                    set_visible: model.show_machine_translation,
                     set_justify: gtk::Justification::Center,
                 },
 
@@ -67,7 +90,10 @@ impl SimpleComponent for SubtitleView {
                     CueViewOutput::MouseEnter => SubtitleViewOutput::SetHoveringCue(true),
                     CueViewOutput::MouseLeave => SubtitleViewOutput::SetHoveringCue(false),
                 }),
-            secondary_cue: Tracker::new(None),
+            secondary_cue: None,
+            machine_translation: None,
+            show_secondary: false,
+            show_machine_translation: false,
         };
 
         let widgets = view_output!();
@@ -76,18 +102,21 @@ impl SimpleComponent for SubtitleView {
     }
 
     fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
-        // Reset trackers
-        self.secondary_cue.reset();
-
         match msg {
-            SubtitleViewMsg::SetPrimaryCue(value) => {
+            SubtitleViewMsg::SetPrimaryCue(addr) => {
                 self.primary_cue
                     .sender()
-                    .send(CueViewMsg::SetText(value))
+                    .send(CueViewMsg::SetCue(addr))
                     .unwrap();
+                self.machine_translation = addr.and_then(|a| a.resolve_translation())
+            }
+            SubtitleViewMsg::SetSecondaryCue(addr) => {
+                let text = addr.map(|addr| addr.resolve_text());
+                self.secondary_cue = text;
             }
-            SubtitleViewMsg::SetSecondaryCue(value) => {
-                self.secondary_cue.set(value);
+            SubtitleViewMsg::ApplySubtitleSettings(settings) => {
+                self.show_secondary = settings.show_secondary;
+                self.show_machine_translation = settings.show_machine_translation;
             }
         }
     }
diff --git a/src/subtitle_extraction/embedded.rs b/src/subtitles/extraction/embedded.rs
index 0ba6178..920f52b 100644
--- a/src/subtitle_extraction/embedded.rs
+++ b/src/subtitles/extraction/embedded.rs
@@ -2,7 +2,7 @@ use std::sync::mpsc;
 
 use anyhow::Context;
 
-use crate::subtitle_extraction::*;
+use crate::{subtitles::SubtitleCue, subtitles::extraction::*};
 
 pub fn extract_embedded_subtitles(
     // stream index to use when storing extracted subtitles, this index already
@@ -23,12 +23,6 @@ pub fn extract_embedded_subtitles(
         match decoder.decode(&packet, &mut subtitle) {
             Ok(true) => {
                 if let Some(cue) = parse_subtitle(&subtitle, &packet, time_base) {
-                    SUBTITLE_TRACKS
-                        .write()
-                        .get_mut(&stream_ix)
-                        .unwrap()
-                        .cues
-                        .push(cue.clone());
                     sender
                         .output(SubtitleExtractorOutput::NewCue(stream_ix, cue))
                         .unwrap();
@@ -72,10 +66,14 @@ fn parse_subtitle(
         .collect::<Vec<String>>()
         .join("\n— ");
 
-    let start = pts_to_clock_time(packet.pts()?);
-    let end = pts_to_clock_time(packet.pts()? + packet.duration());
+    let start_time = pts_to_clock_time(packet.pts()?);
+    let end_time = pts_to_clock_time(packet.pts()? + packet.duration());
 
-    Some(SubtitleCue { start, end, text })
+    Some(SubtitleCue {
+        text,
+        start_time,
+        end_time,
+    })
 }
 
 fn extract_dialogue_text(dialogue_line: &str) -> Option<String> {
diff --git a/src/subtitle_extraction/mod.rs b/src/subtitles/extraction/mod.rs
index 9e7fff4..b012658 100644
--- a/src/subtitle_extraction/mod.rs
+++ b/src/subtitles/extraction/mod.rs
@@ -8,7 +8,7 @@ use std::{collections::BTreeMap, sync::mpsc, thread};
 use ffmpeg::Rational;
 use relm4::{ComponentSender, Worker};
 
-use crate::tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, TrackMetadata};
+use crate::subtitles::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack, TrackMetadata};
 
 pub struct SubtitleExtractor {}
 
@@ -87,10 +87,7 @@ impl SubtitleExtractor {
 
             if stream.parameters().medium() == ffmpeg::media::Type::Subtitle {
                 let metadata = TrackMetadata::from_ffmpeg_stream(&stream);
-                let track = SubtitleTrack {
-                    metadata,
-                    cues: Vec::new(),
-                };
+                let track = SubtitleTrack::new(metadata);
 
                 SUBTITLE_TRACKS.write().insert(stream_ix, track);
 
@@ -117,10 +114,7 @@ impl SubtitleExtractor {
                 None => "Auto-generated from audio (Whisper)".to_string(),
             });
 
-            let track = SubtitleTrack {
-                metadata,
-                cues: Vec::new(),
-            };
+            let track = SubtitleTrack::new(metadata);
 
             SUBTITLE_TRACKS.write().insert(stream_ix, track);
 
diff --git a/src/subtitle_extraction/whisper.rs b/src/subtitles/extraction/whisper.rs
index ffa2e47..bd6fba7 100644
--- a/src/subtitle_extraction/whisper.rs
+++ b/src/subtitles/extraction/whisper.rs
@@ -8,7 +8,10 @@ use anyhow::Context;
 use ffmpeg::{filter, frame};
 use serde::Deserialize;
 
-use crate::{subtitle_extraction::*, tracks::StreamIndex};
+use crate::{
+    subtitles::extraction::*,
+    subtitles::{StreamIndex, SubtitleCue},
+};
 
 #[derive(Debug, Deserialize)]
 struct WhisperCue {
@@ -117,18 +120,11 @@ fn handle_packet(
             let whisper_cue: WhisperCue = serde_json::from_str(&line_buf)?;
 
             let cue = SubtitleCue {
-                start: gst::ClockTime::from_mseconds(whisper_cue.start),
-                end: gst::ClockTime::from_mseconds(whisper_cue.end),
                 text: whisper_cue.text,
+                start_time: gst::ClockTime::from_mseconds(whisper_cue.start),
+                end_time: gst::ClockTime::from_mseconds(whisper_cue.end),
             };
 
-            // TODO deduplicate this vs. the code in embedded.rs
-            SUBTITLE_TRACKS
-                .write()
-                .get_mut(&stream_ix)
-                .unwrap()
-                .cues
-                .push(cue.clone());
             sender
                 .output(SubtitleExtractorOutput::NewCue(stream_ix, cue))
                 .unwrap();
diff --git a/src/subtitles/mod.rs b/src/subtitles/mod.rs
new file mode 100644
index 0000000..a545d52
--- /dev/null
+++ b/src/subtitles/mod.rs
@@ -0,0 +1,86 @@
+pub mod extraction;
+pub mod state;
+
+use std::collections::BTreeMap;
+
+use relm4::SharedState;
+
+pub type StreamIndex = usize;
+
+#[derive(Debug, Clone)]
+pub struct MetadataCollection {
+    pub audio: BTreeMap<StreamIndex, TrackMetadata>,
+    pub subtitles: BTreeMap<StreamIndex, TrackMetadata>,
+}
+
+#[derive(Debug, Clone)]
+pub struct TrackMetadata {
+    pub language: Option<isolang::Language>,
+    pub title: Option<String>,
+}
+
+#[derive(Debug, Clone)]
+pub struct SubtitleCue {
+    pub text: String,
+    pub start_time: gst::ClockTime,
+    pub end_time: gst::ClockTime,
+}
+
+#[derive(Debug, Clone)]
+pub struct SubtitleTrack {
+    pub metadata: TrackMetadata,
+    // SoA of cue text, start timestamp, end timestamp
+    pub texts: Vec<String>,
+    pub start_times: Vec<gst::ClockTime>,
+    pub end_times: Vec<gst::ClockTime>,
+}
+
+pub static SUBTITLE_TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new();
+
+impl TrackMetadata {
+    pub fn from_ffmpeg_stream(stream: &ffmpeg::Stream) -> Self {
+        let language_code = stream.metadata().get("language").map(|s| s.to_string());
+        let title = stream.metadata().get("title").map(|s| s.to_string());
+
+        Self {
+            language: language_code.and_then(|code| isolang::Language::from_639_2b(&code)),
+            title,
+        }
+    }
+}
+
+impl SubtitleTrack {
+    pub fn new(metadata: TrackMetadata) -> Self {
+        Self {
+            metadata,
+            texts: Vec::new(),
+            start_times: Vec::new(),
+            end_times: Vec::new(),
+        }
+    }
+
+    pub fn push_cue(&mut self, cue: SubtitleCue) {
+        let SubtitleCue {
+            text,
+            start_time,
+            end_time,
+        } = cue;
+
+        self.texts.push(text);
+        self.start_times.push(start_time);
+        self.end_times.push(end_time);
+    }
+
+    pub fn iter_cloned_cues(&self) -> impl Iterator<Item = SubtitleCue> {
+        self.texts
+            .iter()
+            .cloned()
+            .zip(self.start_times.iter().cloned())
+            .zip(self.end_times.iter().cloned())
+            .map(|((text, start_time), end_time)| SubtitleCue {
+                text,
+                start_time,
+                end_time,
+            })
+    }
+}
diff --git a/src/subtitles/state.rs b/src/subtitles/state.rs
new file mode 100644
index 0000000..6b1ebda
--- /dev/null
+++ b/src/subtitles/state.rs
@@ -0,0 +1,63 @@
+use crate::{
+    subtitles::{SUBTITLE_TRACKS, StreamIndex},
+    translation::TRANSLATIONS,
+    util::Tracker,
+};
+
+#[derive(Default)]
+pub struct SubtitleState {
+    pub stream_ix: Option<StreamIndex>,
+    pub last_started_cue_ix: Tracker<Option<usize>>,
+    pub last_ended_cue_ix: Tracker<Option<usize>>,
+}
+
+#[derive(Clone, Copy, Debug)]
+pub struct CueAddress(pub StreamIndex, pub usize);
+
+impl SubtitleState {
+    pub fn active_cue(&self) -> Option<CueAddress> {
+        if let Some(stream_ix) = self.stream_ix {
+            match (*self.last_started_cue_ix, *self.last_ended_cue_ix) {
+                (None, _) => None,
+                (Some(started_ix), None) => Some(CueAddress(stream_ix, started_ix)),
+                (Some(started_ix), Some(ended_ix)) => {
+                    if started_ix > ended_ix {
+                        Some(CueAddress(stream_ix, started_ix))
+                    } else {
+                        None
+                    }
+                }
+            }
+        } else {
+            None
+        }
+    }
+
+    pub fn is_dirty(&self) -> bool {
+        self.last_started_cue_ix.is_dirty() || self.last_ended_cue_ix.is_dirty()
+    }
+
+    pub fn reset(&mut self) {
+        self.last_started_cue_ix.reset();
+        self.last_ended_cue_ix.reset();
+    }
+
+    pub fn set_stream_ix(&mut self, stream_ix: Option<StreamIndex>) {
+        self.stream_ix = stream_ix;
+        self.last_started_cue_ix.set(None);
+        self.last_ended_cue_ix.set(None);
+    }
+}
+
+impl CueAddress {
+    pub fn resolve_text(&self) -> String {
+        SUBTITLE_TRACKS.read().get(&self.0).unwrap().texts[self.1].clone()
+    }
+
+    pub fn resolve_translation(&self) -> Option<String> {
+        TRANSLATIONS
+            .read()
+            .get(&self.0)
+            .and_then(|tln| tln.get(self.1).cloned())
+    }
+}
diff --git a/src/track_selector.rs b/src/track_selector.rs
index 5c56e4d..ce04d07 100644
--- a/src/track_selector.rs
+++ b/src/track_selector.rs
@@ -2,7 +2,7 @@ use adw::prelude::*;
 use gtk::{gio, glib};
 use relm4::prelude::*;
 
-use crate::tracks::StreamIndex;
+use crate::{subtitles::StreamIndex, util::Tracker};
 
 glib::wrapper! {
     pub struct TrackInfo(ObjectSubclass<imp::TrackInfo>);
@@ -65,11 +65,12 @@ pub struct TrackSelectorInit {
 #[derive(Debug)]
 pub enum TrackSelectorMsg {
     SetListModel(gio::ListStore),
+    Changed(StreamIndex),
 }
 
 #[derive(Debug)]
 pub enum TrackSelectorOutput {
-    Changed(Option<StreamIndex>),
+    Changed(StreamIndex),
 }
 
 #[relm4::component(pub)]
@@ -87,11 +88,15 @@ impl SimpleComponent for TrackSelector {
             set_factory: Some(&track_factory),
             #[watch]
             set_model: Some(&model.track_list_model),
-            #[watch]
-            set_selected: model.track_ix.map_or(gtk::INVALID_LIST_POSITION, |ix| get_list_ix_from_stream_ix(&model.track_list_model, ix)),
+            // #[watch]
+            // set_selected: model.track_ix.map_or(gtk::INVALID_LIST_POSITION, |ix| get_list_ix_from_stream_ix(&model.track_list_model, ix)),
             connect_selected_notify[sender] => move |combo| {
-                let stream_index = get_stream_ix_from_combo(combo);
-                sender.output(TrackSelectorOutput::Changed(stream_index)).unwrap();
+                if let Some(stream_ix) = get_stream_ix_from_combo(combo) {
+                    println!("selected {}", stream_ix);
+                    sender.input(TrackSelectorMsg::Changed(stream_ix));
+                } else {
+                    println!("selected none");
+                }
             },
         },
 
@@ -155,11 +160,18 @@ impl SimpleComponent for TrackSelector {
         ComponentParts { model, widgets }
     }
 
-    fn update(&mut self, msg: Self::Input, _sender: ComponentSender<Self>) {
+    fn update(&mut self, msg: Self::Input, sender: ComponentSender<Self>) {
         match msg {
             TrackSelectorMsg::SetListModel(list_model) => {
                 self.track_list_model = list_model;
             }
+            TrackSelectorMsg::Changed(track_ix) => {
+                println!("changed {:?}", track_ix);
+                self.track_ix = Some(track_ix);
+                sender
+                    .output(TrackSelectorOutput::Changed(track_ix))
+                    .unwrap();
+            }
         }
     }
 }
diff --git a/src/tracks.rs b/src/tracks.rs
deleted file mode 100644
index 4d69e12..0000000
--- a/src/tracks.rs
+++ /dev/null
@@ -1,38 +0,0 @@
-use std::collections::BTreeMap;
-
-use relm4::SharedState;
-
-pub type StreamIndex = usize;
-
-#[derive(Debug, Clone)]
-pub struct TrackMetadata {
-    pub language: Option<isolang::Language>,
-    pub title: Option<String>,
-}
-
-#[derive(Debug, Clone)]
-pub struct SubtitleTrack {
-    pub metadata: TrackMetadata,
-    pub cues: Vec<SubtitleCue>,
-}
-
-#[derive(Debug, Clone)]
-pub struct SubtitleCue {
-    pub start: gst::ClockTime,
-    pub end: gst::ClockTime,
-    pub text: String,
-}
-
-pub static SUBTITLE_TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new();
-
-impl TrackMetadata {
-    pub fn from_ffmpeg_stream(stream: &ffmpeg::Stream) -> Self {
-        let language_code = stream.metadata().get("language").map(|s| s.to_string());
-        let title = stream.metadata().get("title").map(|s| s.to_string());
-
-        Self {
-            language: language_code.and_then(|code| isolang::Language::from_639_2b(&code)),
-            title,
-        }
-    }
-}
diff --git a/src/transcript.rs b/src/transcript.rs
index a8ae554..602e340 100644
--- a/src/transcript.rs
+++ b/src/transcript.rs
@@ -1,7 +1,7 @@
 use gtk::{ListBox, pango::WrapMode, prelude::*};
 use relm4::prelude::*;
 
-use crate::tracks::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue};
+use crate::subtitles::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue};
 
 #[derive(Debug)]
 pub enum SubtitleCueOutput {
@@ -20,7 +20,7 @@ impl FactoryComponent for SubtitleCue {
         gtk::Button {
             inline_css: "padding: 5px; border-radius: 0;",
             connect_clicked: {
-                let start = self.start;
+                let start = self.start_time;
                 move |_| {
                     sender.output(SubtitleCueOutput::SeekTo(start)).unwrap()
                 }
@@ -124,8 +124,8 @@ impl SimpleComponent for Transcript {
                 if let Some(stream_ix) = stream_index {
                     let tracks = SUBTITLE_TRACKS.read();
                     if let Some(track) = tracks.get(&stream_ix) {
-                        for cue in &track.cues {
-                            self.active_cues.guard().push_back(cue.clone());
+                        for cue in track.iter_cloned_cues() {
+                            self.active_cues.guard().push_back(cue);
                         }
                     }
                 }
diff --git a/src/translation/deepl.rs b/src/translation/deepl.rs
new file mode 100644
index 0000000..f2e84d7
--- /dev/null
+++ b/src/translation/deepl.rs
@@ -0,0 +1,106 @@
+use std::{collections::BTreeMap, time::Duration};
+
+use deepl::DeepLApi;
+use relm4::prelude::*;
+
+use crate::{
+    settings::Settings,
+    subtitles::{SUBTITLE_TRACKS, StreamIndex},
+    translation::TRANSLATIONS,
+};
+
+pub struct DeeplTranslator {
+    stream_ix: Option<StreamIndex>,
+    next_cues_to_translate: BTreeMap<StreamIndex, usize>,
+}
+
+#[derive(Debug)]
+pub enum DeeplTranslatorMsg {
+    SelectTrack(Option<StreamIndex>),
+    // this is only used to drive the async translation
+    DoTranslate,
+}
+
+impl AsyncComponent for DeeplTranslator {
+    type Init = ();
+    type Input = DeeplTranslatorMsg;
+    type Output = ();
+    type CommandOutput = ();
+    type Root = ();
+    type Widgets = ();
+
+    async fn init(
+        _init: Self::Init,
+        _root: Self::Root,
+        sender: relm4::AsyncComponentSender<Self>,
+    ) -> AsyncComponentParts<Self> {
+        let model = Self {
+            stream_ix: None,
+            next_cues_to_translate: BTreeMap::new(),
+        };
+
+        sender.input(DeeplTranslatorMsg::DoTranslate);
+
+        AsyncComponentParts { model, widgets: () }
+    }
+
+    async fn update(
+        &mut self,
+        message: Self::Input,
+        sender: AsyncComponentSender<Self>,
+        _root: &Self::Root,
+    ) {
+        match message {
+            DeeplTranslatorMsg::SelectTrack(stream_ix) => {
+                self.stream_ix = stream_ix;
+            }
+            DeeplTranslatorMsg::DoTranslate => self.do_translate(sender).await,
+        }
+    }
+
+    fn init_root() -> Self::Root {
+        ()
+    }
+}
+
+impl DeeplTranslator {
+    async fn do_translate(&mut self, sender: AsyncComponentSender<Self>) {
+        if let Some(stream_ix) = self.stream_ix {
+            let deepl = DeepLApi::with(&Settings::default().deepl_api_key()).new();
+
+            let next_cue_to_translate = self.next_cues_to_translate.entry(stream_ix).or_insert(0);
+
+            if let Some(cue) = {
+                SUBTITLE_TRACKS
+                    .read()
+                    .get(&stream_ix)
+                    .unwrap()
+                    .texts
+                    .get(*next_cue_to_translate)
+                    .cloned()
+            } {
+                match deepl
+                    .translate_text(cue, deepl::Lang::EN)
+                    .model_type(deepl::ModelType::PreferQualityOptimized)
+                    .await
+                {
+                    Ok(mut resp) => {
+                        TRANSLATIONS
+                            .write()
+                            .entry(stream_ix)
+                            .or_insert(Vec::new())
+                            .push(resp.translations.pop().unwrap().text);
+
+                        *next_cue_to_translate = *next_cue_to_translate + 1;
+                    }
+                    Err(e) => {
+                        log::error!("error fetching translation: {}", e)
+                    }
+                };
+            }
+        }
+
+        relm4::tokio::time::sleep(Duration::from_secs(1)).await;
+        sender.input(DeeplTranslatorMsg::DoTranslate);
+    }
+}
diff --git a/src/translation/mod.rs b/src/translation/mod.rs
new file mode 100644
index 0000000..4a1b358
--- /dev/null
+++ b/src/translation/mod.rs
@@ -0,0 +1,11 @@
+use std::collections::BTreeMap;
+
+use relm4::SharedState;
+
+use crate::subtitles::StreamIndex;
+
+pub mod deepl;
+
+pub use deepl::DeeplTranslator;
+
+pub static TRANSLATIONS: SharedState<BTreeMap<StreamIndex, Vec<String>>> = SharedState::new();
diff --git a/src/util/tracker.rs b/src/util/tracker.rs
index 69a1c5f..060acae 100644
--- a/src/util/tracker.rs
+++ b/src/util/tracker.rs
@@ -1,3 +1,5 @@
+use std::ops::Deref;
+
 pub struct Tracker<T> {
     inner: T,
     dirty: bool,
@@ -40,8 +42,24 @@ impl<T> Tracker<T> {
     }
 }
 
+impl<T> Deref for Tracker<T> {
+    type Target = T;
+
+    fn deref(&self) -> &Self::Target {
+        self.get()
+    }
+}
+
 impl<T: Default> Default for Tracker<T> {
     fn default() -> Self {
         Self::new(T::default())
     }
 }
+
+impl<T: Eq> Tracker<T> {
+    pub fn set_if_ne(&mut self, value: T) {
+        if self.inner != value {
+            self.set(value);
+        }
+    }
+}
diff --git a/subs.srt b/subs.srt
deleted file mode 100644
index 5442ab9..0000000
--- a/subs.srt
+++ /dev/null
@@ -1,644 +0,0 @@
-0
-00:00:00.000 --> 00:00:00.040
-...
-
-0
-00:00:00.040 --> 00:00:13.000
-Alors, comment ça s'est passé ?
-
-0
-00:00:13.000 --> 00:00:17.180
-Formidable.
-
-0
-00:00:17.180 --> 00:00:23.000
-Vous voulez pas en parler ?
-
-0
-00:00:23.000 --> 00:00:25.040
-Si, je veux en parler.
-
-1
-00:00:29.994 --> 00:00:37.894
-Ça s'est passé quand ?
-
-1
-00:00:37.894 --> 00:00:38.414
-Ce matin.
-
-1
-00:00:38.414 --> 00:00:56.054
-Elle a pris ça comment ?
-
-1
-00:00:56.054 --> 00:00:58.814
-À votre avis ?
-
-1
-00:00:58.814 --> 00:00:59.974
-Et vous ?
-
-2
-00:00:59.988 --> 00:01:02.428
-ou à une vraie partie de plaisir.
-
-2
-00:01:02.428 --> 00:01:08.748
-Ça va ?
-
-2
-00:01:08.748 --> 00:01:09.428
-Je pars.
-
-2
-00:01:09.428 --> 00:01:12.788
-Tu pars où ?
-
-2
-00:01:12.788 --> 00:01:13.388
-En Jordanie.
-
-2
-00:01:13.388 --> 00:01:17.128
-Pour quoi faire ?
-
-2
-00:01:17.128 --> 00:01:19.488
-J'ai accepté un poste au lycée français d'Aman.
-
-2
-00:01:19.488 --> 00:01:23.368
-Mais longtemps ?
-
-2
-00:01:23.368 --> 00:01:24.688
-Ça remplace moi, je sais pas combien de temps.
-
-3
-00:01:29.983 --> 00:01:42.983
-Paul, tu es en train de me dire que tu pars ou tu es en train de me dire autre chose ?
-
-3
-00:01:42.983 --> 00:01:47.223
-Les deux.
-
-3
-00:01:47.223 --> 00:01:51.423
-Vous avez adopté quelle stratégie finalement ?
-
-3
-00:01:51.423 --> 00:01:51.983
-La plus nulle.
-
-3
-00:01:51.983 --> 00:01:54.023
-C'est une nuit à pas de stratégie.
-
-3
-00:01:54.023 --> 00:01:58.323
-Celle qui est naze, triste, juste on s'arrête quoi.
-
-4
-00:01:59.978 --> 00:02:03.938
-Vous avez dit quoi ?
-
-4
-00:02:03.938 --> 00:02:05.138
-Les trucs les plus nuls de la Terre.
-
-4
-00:02:05.138 --> 00:02:08.358
-Que je ne parvenais plus à comprendre ce que je faisais à Damas.
-
-4
-00:02:08.358 --> 00:02:10.978
-Que je vivais mal à mon divorce.
-
-4
-00:02:10.978 --> 00:02:14.398
-Que je ne supportais pas de vieillir, d'être prof.
-
-4
-00:02:14.398 --> 00:02:16.678
-Que ma fille me manquait.
-
-4
-00:02:16.678 --> 00:02:20.438
-Bref, qu'il fallait que je parte et que je devais partir seul.
-
-4
-00:02:20.438 --> 00:02:27.358
-Et qu'est-ce qu'elle a dit, elle ?
-
-4
-00:02:27.358 --> 00:02:29.358
-Qu'est-ce que vous voulez qu'elle dise ?
-
-5
-00:02:29.972 --> 00:02:31.772
-Elle m'a insulté.
-
-5
-00:02:31.772 --> 00:02:36.572
-Ça ne l'est pas.
-
-5
-00:02:36.572 --> 00:02:36.952
-Elle a rêvé.
-
-5
-00:02:36.952 --> 00:02:39.032
-Elle a un peu pleuré.
-
-5
-00:02:39.032 --> 00:02:47.412
-Je savais que tu n'allais pas rester éternellement en Syrie, surtout en ce moment.
-
-5
-00:02:47.412 --> 00:02:50.472
-Enfin, la scène habituelle, quoi. Je la largue un peu brutalement quand même.
-
-5
-00:02:50.472 --> 00:02:52.132
-Ça ne me dérangeait pas.
-
-5
-00:02:52.132 --> 00:02:56.232
-Je crois même que ça m'arrangeait à cause de Marwan.
-
-5
-00:02:56.232 --> 00:02:57.732
-Elle m'a crié dessus.
-
-6
-00:02:59.967 --> 00:03:02.407
-C'est triste.
-
-6
-00:03:02.407 --> 00:03:09.567
-Moi aussi, je suis triste.
-
-6
-00:03:09.567 --> 00:03:13.187
-Je t'aime bien.
-
-6
-00:03:13.187 --> 00:03:17.307
-C'est très bien.
-
-6
-00:03:17.307 --> 00:03:22.027
-Et puis c'est bien que tu me dises ça comme ça.
-
-6
-00:03:22.027 --> 00:03:25.047
-J'aurais pas aimé le savoir trop à l'avance.
-
-7
-00:03:29.962 --> 00:03:32.002
-Elle a voulu me jeter son sac au visage, mais elle s'est retenue.
-
-7
-00:03:32.002 --> 00:03:33.862
-Elle est partie.
-
-7
-00:03:33.862 --> 00:03:38.002
-En claquant la porte ?
-
-7
-00:03:38.002 --> 00:03:39.222
-En claquant la porte.
-
-7
-00:03:39.222 --> 00:03:47.862
-Bon, il va être temps que je quitte le pays, moi.
-
-7
-00:03:47.862 --> 00:03:51.242
-Il n'y a pas trop le choix, hein ?
-
-7
-00:03:51.242 --> 00:03:52.282
-Non.
-
-7
-00:03:52.282 --> 00:03:56.262
-Bon, je vais couper.
-
-7
-00:03:56.262 --> 00:03:57.222
-Ok, à bientôt.
-
-7
-00:03:57.222 --> 00:03:58.062
-À bientôt.
-
-8
-00:03:59.956 --> 00:04:09.956
-♪♪
-
-8
-00:04:09.956 --> 00:04:19.956
-♪♪
-
-8
-00:04:19.956 --> 00:04:29.956
-♪♪
-
-9
-00:04:29.951 --> 00:04:43.131
-Vous allez avec eux, moi je vous suivrai derrière.
-
-9
-00:04:43.131 --> 00:04:47.991
-Bonjour, on m'appelle Pépé.
-
-9
-00:04:47.991 --> 00:04:50.011
-Et moi c'est Mémé.
-
-9
-00:04:50.011 --> 00:04:50.691
-Enchanté.
-
-9
-00:04:50.691 --> 00:04:52.211
-Derrière c'est la mule.
-
-10
-00:04:59.946 --> 00:05:22.606
-C'est quoi le programme ?
-
-10
-00:05:22.606 --> 00:05:24.766
-On vous conduit à votre nouveau chez vous, on va passer par le SASS.
-
-10
-00:05:24.766 --> 00:05:26.666
-Celui de l'étoile ?
-
-10
-00:05:26.666 --> 00:05:27.966
-Non, celui d'opéra.
-
-12
-00:06:29.295 --> 00:06:29.935
-Bon à partir de là,
-
-13
-00:06:29.930 --> 00:06:31.930
-A partir de maintenant, vous passez toujours par un sas.
-
-13
-00:06:31.930 --> 00:06:32.930
-D'accord ?
-
-13
-00:06:32.930 --> 00:06:34.930
-Quelle que soit votre destination, vous passez par un sas.
-
-13
-00:06:34.930 --> 00:06:39.930
-Et si vous allez boulevard Mortier ou si vous en revenez, vous passez par un sas et vous changez de moyen de locomotion.
-
-13
-00:06:39.930 --> 00:06:40.930
-D'accord ?
-
-13
-00:06:40.930 --> 00:06:43.930
-Si vous venez à pied, vous prenez une voiture et vice versa.
-
-13
-00:06:43.930 --> 00:06:46.930
-D'accord.
-
-14
-00:06:59.924 --> 00:07:04.924
-I'm going to make a fire.
-
-14
-00:07:04.924 --> 00:07:09.924
-I'll make a fire with a fire extinguisher.
-
-14
-00:07:09.924 --> 00:07:14.924
-I'll make a fire with a fire extinguisher.
-
-14
-00:07:14.924 --> 00:07:19.924
-I'll make a fire with a fire extinguisher.
-
-14
-00:07:19.924 --> 00:07:24.924
-I'll make a fire with a fire extinguisher.
-
-14
-00:07:24.924 --> 00:07:29.884
-Takk for watching!
-
-15
-00:07:29.919 --> 00:07:43.919
-موسيقى
-
-15
-00:07:43.919 --> 00:07:57.919
-موسيقى
-
-17
-00:08:29.908 --> 00:08:40.308
-Il conduisait à deux à l'heure, il paraît.
-
-17
-00:08:40.308 --> 00:08:42.768
-Il était complètement ivre.
-
-17
-00:08:42.768 --> 00:08:45.748
-Il aurait dit aux policiers qu'il allait lentement parce qu'il avait peur d'avoir un incident.
-
-17
-00:08:45.748 --> 00:08:47.288
-Ils l'ont amené au poste.
-
-17
-00:08:47.288 --> 00:08:50.888
-Là, c'est là qu'a eu lieu l'interpellation, à Belouisdad.
-
-17
-00:08:50.888 --> 00:08:53.248
-Au centre d'Alger, tout près du Sofitel.
-
-17
-00:08:53.248 --> 00:08:58.368
-Ça, c'est la reconstitution du trajet à partir des points où le téléphone a borné.
-
-17
-00:08:59.468 --> 00:08:59.868
-Merci.
-
-18
-00:08:59.903 --> 00:09:00.763
-J'ai suivi cet itinéraire.
-
-18
-00:09:00.763 --> 00:09:07.283
-Là, c'est le commissariat de police de Belouisdad.
-
-18
-00:09:07.283 --> 00:09:08.903
-Et depuis ?
-
-18
-00:09:08.903 --> 00:09:10.543
-Depuis, il n'a pas bougé.
-
-18
-00:09:10.543 --> 00:09:12.623
-On vérifie tous les quarts d'heure.
-
-18
-00:09:12.623 --> 00:09:14.663
-Cyclone est toujours à l'intérieur.
-
-18
-00:09:14.663 --> 00:09:16.723
-On a mis une équipe devant l'entrée.
-
-18
-00:09:16.723 --> 00:09:18.903
-Vous êtes sûr que Cyclone était bourré ?
-
-18
-00:09:18.903 --> 00:09:20.123
-Ouais.
-
-18
-00:09:20.123 --> 00:09:22.163
-Mais il y avait pas mal de témoins.
-
-18
-00:09:22.163 --> 00:09:25.903
-Il était musulman pratiquant ?
-
-18
-00:09:25.903 --> 00:09:26.863
-Ouais.
-
-19
-00:09:29.898 --> 00:09:35.058
-Je propose qu'on envoie quelqu'un au commissariat pour vérifier dans quel état il est.
-
-19
-00:09:35.058 --> 00:09:37.178
-Très bien. Et dès qu'il sort, vous me promenez.
-
-19
-00:09:37.178 --> 00:09:45.938
-Dites-moi, vous l'aviez entraîné à subir un interrogatoire sous alcool, je suppose ?
-
-19
-00:09:45.938 --> 00:09:46.338
-Bien sûr.
-
-19
-00:09:46.338 --> 00:09:50.038
-Je peux voir les enregistrements ? J'aimerais bien savoir comment il s'en était sorti.
-
-19
-00:09:50.038 --> 00:09:53.678
-Il s'en était bien sorti ?
-
-19
-00:09:53.678 --> 00:09:55.578
-Ouais. Je vous les trouve.
-
-20
-00:09:59.892 --> 00:10:02.892
-Vous avez besoin de quelque chose, monsieur Manon ?
-
-20
-00:10:02.892 --> 00:10:04.892
-Non, non, ça va, ça va.
-
-21
-00:10:29.887 --> 00:10:37.367
-Il vient d'arriver.
-
-21
-00:10:37.367 --> 00:10:39.447
-C'est une rôle, ça.
-
-21
-00:10:39.447 --> 00:10:43.607
-J'ai déjà essayé chez le DG, ça doit te coûter un peu du cul.
-
-21
-00:10:43.607 --> 00:10:45.207
-Tu peux le dire, ouais.
-
-21
-00:10:45.207 --> 00:10:47.267
-Ça, c'est fini le dos coincé.
-
-21
-00:10:47.267 --> 00:10:49.647
-Tu veux que je t'aille à le déballer ?
-
-21
-00:10:49.647 --> 00:10:51.727
-Non, merci. Plus tard.
-
-22
-00:10:59.882 --> 00:11:10.782
-Si y a quelque chose qui m'a échappé une fois, c'est ma chatte en pêche de foie.
-
-22
-00:11:10.782 --> 00:11:20.402
-Oui, monsieur Jacques.
-
-22
-00:11:20.402 --> 00:11:22.022
-Il a été muté.
-
-22
-00:11:22.022 --> 00:11:24.882
-Il a fait une offre qu'il pouvait pas refuser.
-
-22
-00:11:24.882 --> 00:11:26.882
-Promotion ou quoi ?
-
-22
-00:11:26.882 --> 00:11:28.762
-Moi, j'avais compris.
-
-23
-00:11:29.876 --> 00:11:30.976
-Il m'en avait informé.
-
-23
-00:11:30.976 --> 00:11:35.176
-Donc c'est moi qui serai votre nouveau référent au Droneur.
-
-23
-00:11:35.176 --> 00:11:36.876
-Je m'appelle Raymond Cisteron.
-
-23
-00:11:36.876 --> 00:11:40.576
-Cisteron ?
-
-23
-00:11:40.576 --> 00:11:40.936
-Bon.
-
-23
-00:11:40.936 --> 00:11:42.996
-Et vous êtes un...
-
-23
-00:11:42.996 --> 00:11:44.476
-Oui, j'ai étudié les dossiers dans le détail.
-
-23
-00:11:44.476 --> 00:11:48.896
-Si vous êtes d'accord, on va commencer par celui du colonel Bazir.
-
-23
-00:11:48.896 --> 00:11:50.536
-D'accord.
-
-24
-00:11:59.871 --> 00:12:17.171
-Qu'est-ce que c'est ?
-
-24
-00:12:17.171 --> 00:12:19.131
-Château Giravates 2005.
-
-24
-00:12:19.131 --> 00:12:20.991
-Un Pauillac du très bon.
-
-24
-00:12:20.991 --> 00:12:24.251
-Vous voulez que je le boive ?
-
-24
-00:12:24.251 --> 00:12:25.711
-Vous voulez boire autre chose ?
-
-24
-00:12:25.711 --> 00:12:26.871
-La bière, la vodka ?
-
-25
-00:12:29.866 --> 00:12:34.026
-Je suis un peu embêté, mais vous savez que je n'ai pas le droit de boire de l'alcool, c'est contraire à ma religion.
-
-25
-00:12:34.026 --> 00:12:36.486
-Vous allez faire une entorse.
-
-25
-00:12:36.486 --> 00:12:41.286
-On doit tester votre capacité à réciter votre légende sous alcool.
-
-25
-00:12:41.286 --> 00:12:45.326
-Désolé, ce n'est pas possible.
-
-25
-00:12:45.326 --> 00:12:49.286
-Rachid, vous allez boire, ce n'est pas la peine de discuter, c'est le protocole.
-
-25
-00:12:49.286 --> 00:12:54.466
-En même temps, si vous m'envoyez à Alger, c'est aussi parce que je suis musulman.
-
-25
-00:12:55.286 --> 00:12:59.826
-Il est impossible de vous laisser partir si on ne sait pas comment vous tenez votre légende quand vous perdez le compte.
-
-26
-00:12:59.860 --> 00:13:00.260
-Je contrôle.
-
-26
-00:13:00.260 --> 00:13:06.900
-Si je n'avais jamais bu une goutte d'alcool,
-
-26
-00:13:06.900 --> 00:13:07.960
-je n'en boirais jamais de mon plein gré.
-
-26
-00:13:07.960 --> 00:13:11.160
-Si je me trouve dans une situation où on m'oblige à boire,
-
-26
-00:13:11.160 --> 00:13:12.780
-c'est que je suis déjà dans une très mauvaise posture.
-
-26
-00:13:12.780 --> 00:13:14.200
-Vous êtes d'accord ?
-
-26
-00:13:14.200 --> 00:13:15.480
-C'est que ma légende, elle pèse plus grand-chose.
-
-26
-00:13:15.480 --> 00:13:19.660
-Si on en est à me faire boire en Algérie,
-
-26
-00:13:19.660 --> 00:13:21.700
-c'est que c'est déjà trop tard.
-
-27
-00:13:29.855 --> 00:13:31.495
-Vous êtes vraiment buté.
-