about summary refs log tree commit diff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock408
-rw-r--r--Cargo.toml4
-rw-r--r--src/app.rs91
-rw-r--r--src/open_dialog.rs4
-rw-r--r--src/player.rs5
-rw-r--r--src/subtitles/extraction/embedded.rs3
-rw-r--r--src/subtitles/extraction/mod.rs33
-rw-r--r--src/subtitles/extraction/whisper.rs3
-rw-r--r--src/subtitles/mod.rs7
-rw-r--r--src/translation/deepl.rs65
-rw-r--r--src/util/cache.rs24
-rw-r--r--src/util/mod.rs2
12 files changed, 580 insertions, 69 deletions
diff --git a/Cargo.lock b/Cargo.lock
index 6a00a96..5353f66 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -9,6 +9,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
 
 [[package]]
+name = "ahash"
+version = "0.8.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a15f179cd60c4584b8a8c596927aadc462e27f2ca70c04e0071964a73ba7a75"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "version_check",
+ "zerocopy",
+]
+
+[[package]]
 name = "aho-corasick"
 version = "1.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -18,6 +30,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
 name = "android_system_properties"
 version = "0.1.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -101,6 +119,17 @@ dependencies = [
 ]
 
 [[package]]
+name = "async-trait"
+version = "0.1.89"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9035ad2d096bed7955a320ee7e2230574d28fd3c3a0f186cbea1ff3c7eed5dbb"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
 name = "atomic-waker"
 version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -130,7 +159,7 @@ version = "0.72.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "993776b509cfb49c750f11b8f07a46fa23e0a1386ffc01fb1e7d343efc387895"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "cexpr",
  "clang-sys",
  "itertools 0.13.0",
@@ -144,6 +173,12 @@ dependencies = [
 
 [[package]]
 name = "bitflags"
+version = "1.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
+
+[[package]]
+name = "bitflags"
 version = "2.10.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
@@ -161,18 +196,64 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43"
 
 [[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
 name = "bytes"
 version = "1.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
 
 [[package]]
+name = "cached"
+version = "0.56.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "801927ee168e17809ab8901d9f01f700cd7d8d6a6527997fee44e4b0327a253c"
+dependencies = [
+ "ahash",
+ "async-trait",
+ "cached_proc_macro",
+ "cached_proc_macro_types",
+ "directories",
+ "futures",
+ "hashbrown 0.15.5",
+ "once_cell",
+ "rmp-serde",
+ "serde",
+ "sled",
+ "thiserror",
+ "tokio",
+ "web-time",
+]
+
+[[package]]
+name = "cached_proc_macro"
+version = "0.25.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9225bdcf4e4a9a4c08bf16607908eb2fbf746828d5e0b5e019726dbf6571f201"
+dependencies = [
+ "darling",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "cached_proc_macro_types"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ade8366b8bd5ba243f0a58f036cc0ca8a2f069cff1a2351ef1cac6b083e16fc0"
+
+[[package]]
 name = "cairo-rs"
 version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dfe4354df4da648870e363387679081f8f9fc538ec8b55901e3740c6a0ef81b1"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "cairo-sys-rs",
  "glib",
  "libc",
@@ -252,7 +333,7 @@ version = "0.26.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ad36507aeb7e16159dfe68db81ccc27571c3ccd4b76fb2fb72fc59e7a4b1b64c"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "block",
  "cocoa-foundation",
  "core-foundation 0.10.1",
@@ -268,7 +349,7 @@ version = "0.2.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "81411967c50ee9a1fc11365f8c585f863a22a9697c89239c452292c40ba79b0d"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "block",
  "core-foundation 0.10.1",
  "core-graphics-types",
@@ -322,7 +403,7 @@ version = "0.24.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fa95a34622365fa5bbf40b20b75dba8dfa8c94c734aea8ac9a5ca38af14316f1"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "core-foundation 0.10.1",
  "core-graphics-types",
  "foreign-types 0.5.0",
@@ -335,7 +416,7 @@ version = "0.2.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3d44a101f213f6c4cdc1853d4b78aef6db6bdfa3468798cc1d9912f4735013eb"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "core-foundation 0.10.1",
  "libc",
 ]
@@ -350,12 +431,56 @@ dependencies = [
 ]
 
 [[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
 name = "crossbeam-utils"
 version = "0.8.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
 
 [[package]]
+name = "darling"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc7f46116c46ff9ab3eb1597a45688b6715c6e628b5c133e288e709a29bcb4ee"
+dependencies = [
+ "darling_core",
+ "darling_macro",
+]
+
+[[package]]
+name = "darling_core"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0d00b9596d185e565c2207a0b01f8bd1a135483d02d9b7b0a54b11da8d53412e"
+dependencies = [
+ "fnv",
+ "ident_case",
+ "proc-macro2",
+ "quote",
+ "strsim 0.11.1",
+ "syn 2.0.110",
+]
+
+[[package]]
+name = "darling_macro"
+version = "0.20.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc34b93ccb385b40dc71c6fceac4b2ad23662c7eeb248cf10d529b7e055b6ead"
+dependencies = [
+ "darling_core",
+ "quote",
+ "syn 2.0.110",
+]
+
+[[package]]
 name = "deepl"
 version = "0.7.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -393,7 +518,7 @@ dependencies = [
  "arrayvec",
  "proc-macro2",
  "quote",
- "strsim",
+ "strsim 0.10.0",
  "syn 2.0.110",
 ]
 
@@ -413,6 +538,27 @@ dependencies = [
 ]
 
 [[package]]
+name = "directories"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "16f5094c54661b38d03bd7e50df373292118db60b585c08a411c6d840017fe7d"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.60.2",
+]
+
+[[package]]
 name = "displaydoc"
 version = "0.2.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -516,7 +662,7 @@ version = "8.0.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d658424d233cbd993a972dd73a66ca733acd12a494c68995c9ac32ae1fe65b40"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "ffmpeg-sys-next",
  "libc",
 ]
@@ -580,6 +726,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
 
 [[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
 name = "foreign-types"
 version = "0.3.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -637,6 +789,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "28dd6caf6059519a65843af8fe2a3ae298b14b80179855aeb4adc2c1934ee619"
 
 [[package]]
+name = "fs2"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213"
+dependencies = [
+ "libc",
+ "winapi",
+]
+
+[[package]]
 name = "futures"
 version = "0.3.31"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -726,6 +888,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "fxhash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c"
+dependencies = [
+ "byteorder",
+]
+
+[[package]]
 name = "gdk-pixbuf"
 version = "0.21.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -868,7 +1039,7 @@ version = "0.21.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "5b9dbecb1c33e483a98be4acfea2ab369e1c28f517c6eadb674537409c25c4b2"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "futures-channel",
  "futures-core",
  "futures-executor",
@@ -1041,6 +1212,8 @@ dependencies = [
  "option-operations",
  "pastey",
  "pin-project-lite",
+ "serde",
+ "serde_bytes",
  "smallvec",
  "thiserror",
 ]
@@ -1259,6 +1432,17 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash",
+]
+
+[[package]]
+name = "hashbrown"
 version = "0.16.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
@@ -1506,6 +1690,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "ident_case"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
+
+[[package]]
 name = "idna"
 version = "1.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1539,7 +1729,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2"
 dependencies = [
  "equivalent",
- "hashbrown",
+ "hashbrown 0.16.1",
+]
+
+[[package]]
+name = "instant"
+version = "0.1.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0242819d153cba4b4b05a5a8f2a7e9bbf97b6055b2a002b395c96b5ff3c0222"
+dependencies = [
+ "cfg-if",
 ]
 
 [[package]]
@@ -1687,6 +1886,16 @@ dependencies = [
 ]
 
 [[package]]
+name = "libredox"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3"
+dependencies = [
+ "bitflags 2.10.0",
+ "libc",
+]
+
+[[package]]
 name = "linux-raw-sys"
 version = "0.11.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1704,8 +1913,10 @@ version = "0.1.0"
 dependencies = [
  "anyhow",
  "async-channel",
+ "cached",
  "cocoa",
  "deepl",
+ "directories",
  "env_logger",
  "ffmpeg-next",
  "gsettings-macro",
@@ -1870,6 +2081,7 @@ checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824"
 dependencies = [
  "num-integer",
  "num-traits",
+ "serde",
 ]
 
 [[package]]
@@ -1918,7 +2130,7 @@ version = "0.10.75"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "cfg-if",
  "foreign-types 0.3.2",
  "libc",
@@ -1957,6 +2169,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
 name = "option-operations"
 version = "0.6.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1996,6 +2214,54 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
 
 [[package]]
+name = "parking_lot"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
+dependencies = [
+ "instant",
+ "lock_api",
+ "parking_lot_core 0.8.6",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.12",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc"
+dependencies = [
+ "cfg-if",
+ "instant",
+ "libc",
+ "redox_syscall 0.2.16",
+ "smallvec",
+ "winapi",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall 0.5.18",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
 name = "paste"
 version = "1.0.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2161,6 +2427,35 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
 
 [[package]]
+name = "redox_syscall"
+version = "0.2.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a"
+dependencies = [
+ "bitflags 1.3.2",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags 2.10.0",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.16",
+ "libredox",
+ "thiserror",
+]
+
+[[package]]
 name = "regex"
 version = "1.12.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2315,6 +2610,25 @@ dependencies = [
 ]
 
 [[package]]
+name = "rmp"
+version = "0.8.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ba8be72d372b2c9b35542551678538b562e7cf86c3315773cae48dfbfe7790c"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "rmp-serde"
+version = "1.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f81bee8c8ef9b577d1681a70ebbc962c232461e397b22c208c43c04b67a155"
+dependencies = [
+ "rmp",
+ "serde",
+]
+
+[[package]]
 name = "rustc-hash"
 version = "2.1.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2335,7 +2649,7 @@ version = "1.1.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cd15f8a2c5551a84d56efdc1cd049089e409ac19a3072d5037a17fd70719ff3e"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "errno",
  "libc",
  "linux-raw-sys",
@@ -2417,7 +2731,7 @@ version = "2.11.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "core-foundation 0.9.4",
  "core-foundation-sys",
  "libc",
@@ -2451,6 +2765,15 @@ dependencies = [
 ]
 
 [[package]]
+name = "serde_bytes"
+version = "0.11.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96"
+dependencies = [
+ "serde",
+]
+
+[[package]]
 name = "serde_core"
 version = "1.0.228"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2529,6 +2852,22 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589"
 
 [[package]]
+name = "sled"
+version = "0.34.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f96b4737c2ce5987354855aed3797279def4ebf734436c6aa4552cf8e169935"
+dependencies = [
+ "crc32fast",
+ "crossbeam-epoch",
+ "crossbeam-utils",
+ "fs2",
+ "fxhash",
+ "libc",
+ "log",
+ "parking_lot 0.11.2",
+]
+
+[[package]]
 name = "smallvec"
 version = "1.15.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2572,6 +2911,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
 
 [[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
 name = "subtle"
 version = "2.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -2624,7 +2969,7 @@ version = "0.6.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "core-foundation 0.9.4",
  "system-configuration-sys",
 ]
@@ -2710,6 +3055,7 @@ dependencies = [
  "bytes",
  "libc",
  "mio",
+ "parking_lot 0.12.5",
  "pin-project-lite",
  "socket2",
  "tokio-macros",
@@ -2860,7 +3206,7 @@ version = "0.6.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9cf146f99d442e8e68e585f5d798ccd3cad9a7835b917e09728880a862706456"
 dependencies = [
- "bitflags",
+ "bitflags 2.10.0",
  "bytes",
  "futures-util",
  "http",
@@ -3143,6 +3489,32 @@ dependencies = [
 ]
 
 [[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
 name = "winapi-util"
 version = "0.1.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -3152,6 +3524,12 @@ dependencies = [
 ]
 
 [[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
 name = "windows-core"
 version = "0.62.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 5f7af0f..4cf357a 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -4,7 +4,7 @@ version = "0.1.0"
 edition = "2024"
 
 [dependencies]
-gst = { version = "0.24.3", package = "gstreamer", features = ["v1_26"] }
+gst = { version = "0.24.3", package = "gstreamer", features = ["v1_26", "serde"] }
 gst-video = { version = "0.24.3", package = "gstreamer-video", features = ["v1_26"] }
 gst-play = { version = "0.24.2", package = "gstreamer-play", features = ["v1_26"] }
 gst-plugin-gtk4 = { version = "0.14", features = ["gtk_v4_20"] }
@@ -25,6 +25,8 @@ serde_json = "1.0.145"
 relm4-icons = "0.10"
 deepl = "0.7.3"
 gsettings-macro = "0.2.2"
+cached = { version = "0.56.0", features = ["disk_store", "async"] }
+directories = "6.0.0"
 
 [target.'cfg(target_os = "macos")'.dependencies]
 cocoa = "0.26"
diff --git a/src/app.rs b/src/app.rs
index 49efd49..35a501e 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -1,4 +1,5 @@
 use adw::prelude::*;
+use cached::{DiskCache, IOCached};
 use relm4::{WorkerController, prelude::*};
 
 use crate::{
@@ -13,13 +14,15 @@ use crate::{
     subtitle_view::{SubtitleView, SubtitleViewMsg, SubtitleViewOutput},
     subtitles::{
         MetadataCollection, SUBTITLE_TRACKS, StreamIndex, SubtitleCue, SubtitleTrack,
-        TrackMetadata,
-        extraction::{SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput},
+        SubtitleTrackCollection, TrackMetadata,
+        extraction::{
+            ExtractionArgs, SubtitleExtractor, SubtitleExtractorMsg, SubtitleExtractorOutput,
+        },
         state::SubtitleState,
     },
     transcript::{Transcript, TranscriptMsg, TranscriptOutput},
-    translation::{DeeplTranslator, deepl::DeeplTranslatorMsg},
-    util::Tracker,
+    translation::{DeeplTranslator, TRANSLATIONS, deepl::DeeplTranslatorMsg},
+    util::{self, Tracker},
 };
 
 pub struct App {
@@ -34,6 +37,9 @@ pub struct App {
     open_url_dialog: Controller<OpenDialog>,
     subtitle_selection_dialog: Option<Controller<SubtitleSelectionDialog>>,
 
+    subtitle_extraction_args: Option<ExtractionArgs>,
+    subtitle_cache: DiskCache<ExtractionArgs, SubtitleTrackCollection>,
+
     primary_subtitle_state: SubtitleState,
     secondary_subtitle_state: SubtitleState,
 
@@ -153,6 +159,8 @@ impl SimpleComponent for App {
             },
         );
 
+        let subtitle_cache = util::make_cache("subtitles");
+
         let model = Self {
             root: root.clone(),
             player,
@@ -165,6 +173,9 @@ impl SimpleComponent for App {
             open_url_dialog,
             subtitle_selection_dialog: None,
 
+            subtitle_extraction_args: None,
+            subtitle_cache,
+
             primary_subtitle_state: SubtitleState::default(),
             secondary_subtitle_state: SubtitleState::default(),
 
@@ -193,6 +204,14 @@ impl SimpleComponent for App {
             }
             AppMsg::SubtitleExtractionComplete => {
                 log::info!("Subtitle extraction complete");
+                if let Some(ref args) = self.subtitle_extraction_args {
+                    if let Err(e) = self
+                        .subtitle_cache
+                        .cache_set(args.clone(), SUBTITLE_TRACKS.read().clone())
+                    {
+                        log::error!("error caching extracted subtitles: {}", e);
+                    }
+                }
             }
             AppMsg::ApplySubtitleSettings(settings) => {
                 self.primary_subtitle_state
@@ -249,6 +268,8 @@ impl SimpleComponent for App {
                 mut metadata,
                 whisper_stream_index,
             } => {
+                self.reset();
+
                 if let Some(ix) = whisper_stream_index {
                     let audio_metadata = metadata.audio.get(&ix).unwrap();
                     let subs_metadata = TrackMetadata {
@@ -263,17 +284,33 @@ impl SimpleComponent for App {
                     metadata.subtitles.insert(ix, subs_metadata);
                 }
 
-                self.player
-                    .sender()
-                    .send(PlayerMsg::SetUrl(url.clone()))
-                    .unwrap();
-                self.extractor
-                    .sender()
-                    .send(SubtitleExtractorMsg::ExtractFromUrl {
-                        url,
-                        whisper_stream_index,
-                    })
-                    .unwrap();
+                let extraction_args = ExtractionArgs {
+                    url: url.clone(),
+                    whisper_stream_index,
+                };
+
+                match self.subtitle_cache.cache_get(&extraction_args) {
+                    Ok(Some(track_collection)) => {
+                        log::debug!("subtitle cache hit");
+                        *(SUBTITLE_TRACKS.write()) = track_collection;
+                    }
+                    Ok(None) => {
+                        log::debug!("subtitle cache miss");
+                        self.extractor
+                            .sender()
+                            .send(SubtitleExtractorMsg::Extract(extraction_args.clone()))
+                            .unwrap();
+                    }
+                    Err(e) => {
+                        log::error!("error querying subtitle cache: {}", e);
+                        self.extractor
+                            .sender()
+                            .send(SubtitleExtractorMsg::Extract(extraction_args.clone()))
+                            .unwrap();
+                    }
+                }
+
+                self.subtitle_extraction_args = Some(extraction_args);
 
                 let subtitle_selection_dialog = SubtitleSelectionDialog::builder()
                     .launch((self.root.clone().into(), metadata))
@@ -283,12 +320,28 @@ impl SimpleComponent for App {
                         }
                     });
                 self.subtitle_selection_dialog = Some(subtitle_selection_dialog);
+
+                self.player.sender().send(PlayerMsg::SetUrl(url)).unwrap();
             }
         }
     }
 }
 
 impl App {
+    fn reset(&mut self) {
+        SUBTITLE_TRACKS.write().clear();
+        TRANSLATIONS.write().clear();
+
+        self.subtitle_selection_dialog = None;
+        self.subtitle_extraction_args = None;
+        self.primary_subtitle_state = SubtitleState::default();
+        self.secondary_subtitle_state = SubtitleState::default();
+        self.autopaused = false;
+        self.hovering_primary_cue = false;
+
+        // TODO also clear transcript?
+    }
+
     fn update_subtitle_states(&mut self, position: gst::ClockTime) {
         self.update_primary_subtitle_state(position);
         self.update_secondary_subtitle_state(position);
@@ -356,10 +409,10 @@ impl App {
 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);
+        if let Some(track) = lock.get(&stream_ix) {
+            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);
+        }
     }
 }
 
diff --git a/src/open_dialog.rs b/src/open_dialog.rs
index 3b822be..b84ff3b 100644
--- a/src/open_dialog.rs
+++ b/src/open_dialog.rs
@@ -68,6 +68,7 @@ impl Component for OpenDialog {
                 set_child = &adw::NavigationView {
                     add = &adw::NavigationPage {
                         set_title: "Open File or Stream",
+                        set_tag: Some("file_selection"),
 
                         #[wrap(Some)]
                         set_child = &adw::ToolbarView {
@@ -304,6 +305,9 @@ impl OpenDialog {
         self.url.get_mut().clear();
         self.do_whisper_extraction = false;
         self.whisper_stream_index = None;
+        if let Some(ref nav) = self.navigation_view {
+            nav.pop_to_tag("file_selection");
+        }
     }
 
     fn fetch_metadata(&mut self, sender: ComponentSender<Self>) {
diff --git a/src/player.rs b/src/player.rs
index d533f48..0804fed 100644
--- a/src/player.rs
+++ b/src/player.rs
@@ -205,9 +205,12 @@ impl SimpleComponent for Player {
                             }
                         }
                         PlayMessage::Buffering(_) => {
+                            // println!("buffering")
                             // TODO
                         }
-                        _ => {}
+                        msg => {
+                            // println!("msg: {:?}", msg);
+                        }
                     }
 
                     glib::ControlFlow::Continue
diff --git a/src/subtitles/extraction/embedded.rs b/src/subtitles/extraction/embedded.rs
index 920f52b..39698cf 100644
--- a/src/subtitles/extraction/embedded.rs
+++ b/src/subtitles/extraction/embedded.rs
@@ -5,8 +5,7 @@ use anyhow::Context;
 use crate::{subtitles::SubtitleCue, subtitles::extraction::*};
 
 pub fn extract_embedded_subtitles(
-    // stream index to use when storing extracted subtitles, this index already
-    // has to be in TRACKS when this function is called!
+    // stream index to use when storing extracted subtitles
     stream_ix: StreamIndex,
     context: ffmpeg::codec::Context,
     time_base: ffmpeg::Rational,
diff --git a/src/subtitles/extraction/mod.rs b/src/subtitles/extraction/mod.rs
index 5070fdb..6495b62 100644
--- a/src/subtitles/extraction/mod.rs
+++ b/src/subtitles/extraction/mod.rs
@@ -3,22 +3,30 @@ mod embedded;
 /// Synthesis of subtitles from audio using whisper.cpp
 mod whisper;
 
-use std::{collections::BTreeMap, sync::mpsc, thread};
+use std::{collections::BTreeMap, fmt::Display, sync::mpsc, thread};
 
 use ffmpeg::Rational;
 use relm4::{ComponentSender, Worker};
 
-use crate::subtitles::{SUBTITLE_TRACKS, StreamIndex, SubtitleCue};
+use crate::subtitles::{StreamIndex, SubtitleCue};
 
 pub struct SubtitleExtractor {}
 
+#[derive(Debug, Clone)]
+pub struct ExtractionArgs {
+    pub url: String,
+    pub whisper_stream_index: Option<usize>,
+}
+
+impl Display for ExtractionArgs {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(f, "{} {:?}", self.url, self.whisper_stream_index)
+    }
+}
+
 #[derive(Debug)]
 pub enum SubtitleExtractorMsg {
-    ExtractFromUrl {
-        url: String,
-        // the index of the audio stream on which to run a whisper transcription
-        whisper_stream_index: Option<usize>,
-    },
+    Extract(ExtractionArgs),
 }
 
 #[derive(Debug)]
@@ -38,10 +46,10 @@ impl Worker for SubtitleExtractor {
 
     fn update(&mut self, msg: SubtitleExtractorMsg, sender: ComponentSender<Self>) {
         match msg {
-            SubtitleExtractorMsg::ExtractFromUrl {
+            SubtitleExtractorMsg::Extract(ExtractionArgs {
                 url,
                 whisper_stream_index: whisper_audio_stream_ix,
-            } => {
+            }) => {
                 self.handle_extract_from_url(url, whisper_audio_stream_ix, sender);
             }
         }
@@ -55,12 +63,8 @@ impl SubtitleExtractor {
         whisper_audio_stream_ix: Option<usize>,
         sender: ComponentSender<Self>,
     ) {
-        // Clear existing tracks
-        SUBTITLE_TRACKS.write().clear();
-
         match self.extract_subtitles(&url, whisper_audio_stream_ix, sender.clone()) {
             Ok(_) => {
-                log::info!("Subtitle extraction completed successfully");
                 sender
                     .output(SubtitleExtractorOutput::ExtractionComplete)
                     .unwrap();
@@ -125,7 +129,8 @@ impl SubtitleExtractor {
         }
 
         // wait for extraction to complete
-        for (_, (_, join_handle)) in subtitle_extractors {
+        for (packet_tx, join_handle) in subtitle_extractors.into_values() {
+            drop(packet_tx);
             join_handle
                 .join()
                 .unwrap()
diff --git a/src/subtitles/extraction/whisper.rs b/src/subtitles/extraction/whisper.rs
index bd6fba7..be4346a 100644
--- a/src/subtitles/extraction/whisper.rs
+++ b/src/subtitles/extraction/whisper.rs
@@ -21,8 +21,7 @@ struct WhisperCue {
 }
 
 pub fn generate_whisper_subtitles(
-    // stream index to use when storing generated subtitles, this index
-    // already has to be in TRACKS when this function is called!
+    // stream index to use when storing generated subtitles
     stream_ix: StreamIndex,
     context: ffmpeg::codec::Context,
     time_base: ffmpeg::Rational,
diff --git a/src/subtitles/mod.rs b/src/subtitles/mod.rs
index acb73dc..de747f1 100644
--- a/src/subtitles/mod.rs
+++ b/src/subtitles/mod.rs
@@ -4,6 +4,7 @@ pub mod state;
 use std::collections::BTreeMap;
 
 use relm4::SharedState;
+use serde::{Deserialize, Serialize};
 
 pub type StreamIndex = usize;
 
@@ -26,7 +27,7 @@ pub struct SubtitleCue {
     pub end_time: gst::ClockTime,
 }
 
-#[derive(Default, Debug, Clone)]
+#[derive(Default, Debug, Clone, Serialize, Deserialize)]
 pub struct SubtitleTrack {
     // SoA of cue text, start timestamp, end timestamp
     pub texts: Vec<String>,
@@ -34,7 +35,9 @@ pub struct SubtitleTrack {
     pub end_times: Vec<gst::ClockTime>,
 }
 
-pub static SUBTITLE_TRACKS: SharedState<BTreeMap<StreamIndex, SubtitleTrack>> = SharedState::new();
+pub type SubtitleTrackCollection = BTreeMap<StreamIndex, SubtitleTrack>;
+
+pub static SUBTITLE_TRACKS: SharedState<SubtitleTrackCollection> = SharedState::new();
 
 impl TrackMetadata {
     pub fn from_ffmpeg_stream(stream: &ffmpeg::Stream) -> Self {
diff --git a/src/translation/deepl.rs b/src/translation/deepl.rs
index f2e84d7..11510d5 100644
--- a/src/translation/deepl.rs
+++ b/src/translation/deepl.rs
@@ -1,6 +1,9 @@
-use std::{collections::BTreeMap, time::Duration};
+use std::{collections::BTreeMap, fmt::Display, time::Duration};
 
+use crate::util;
+use cached::proc_macro::io_cached;
 use deepl::DeepLApi;
+use deepl::ModelType;
 use relm4::prelude::*;
 
 use crate::{
@@ -66,30 +69,28 @@ impl AsyncComponent for DeeplTranslator {
 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) = {
+            while let Some(cue) = {
                 SUBTITLE_TRACKS
                     .read()
                     .get(&stream_ix)
-                    .unwrap()
-                    .texts
-                    .get(*next_cue_to_translate)
+                    .and_then(|stream| stream.texts.get(*next_cue_to_translate))
                     .cloned()
             } {
-                match deepl
-                    .translate_text(cue, deepl::Lang::EN)
-                    .model_type(deepl::ModelType::PreferQualityOptimized)
-                    .await
+                match translate_text(TranslateRequest {
+                    input: cue.clone(),
+                    source_lang: None,            // TODO
+                    target_lang: deepl::Lang::EN, // TODO
+                })
+                .await
                 {
-                    Ok(mut resp) => {
+                    Ok(translated) => {
                         TRANSLATIONS
                             .write()
                             .entry(stream_ix)
                             .or_insert(Vec::new())
-                            .push(resp.translations.pop().unwrap().text);
+                            .push(translated);
 
                         *next_cue_to_translate = *next_cue_to_translate + 1;
                     }
@@ -100,7 +101,45 @@ impl DeeplTranslator {
             }
         }
 
+        // check every second for new cues to be translated
         relm4::tokio::time::sleep(Duration::from_secs(1)).await;
         sender.input(DeeplTranslatorMsg::DoTranslate);
     }
 }
+
+#[derive(Clone)]
+struct TranslateRequest {
+    input: String,
+    source_lang: Option<deepl::Lang>,
+    target_lang: deepl::Lang,
+}
+
+impl Display for TranslateRequest {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        write!(
+            f,
+            "{:?}-{}:{}",
+            self.source_lang, self.target_lang, self.input
+        )
+    }
+}
+
+#[io_cached(
+    disk = true,
+    create = r#"{ util::make_cache("deepl_translations") }"#,
+    map_error = r#"|err| err"#
+)]
+async fn translate_text(req: TranslateRequest) -> anyhow::Result<String> {
+    let deepl = DeepLApi::with(&Settings::default().deepl_api_key()).new();
+    let mut requester = deepl.translate_text(req.input, req.target_lang);
+    requester.model_type(ModelType::PreferQualityOptimized);
+    if let Some(source_lang) = req.source_lang {
+        requester.source_lang(source_lang);
+    }
+    let translated = requester.await?.translations.pop().unwrap().text;
+
+    // try to respect deepl's rate-limit
+    relm4::tokio::time::sleep(Duration::from_millis(500)).await;
+
+    Ok(translated)
+}
diff --git a/src/util/cache.rs b/src/util/cache.rs
new file mode 100644
index 0000000..6e4672f
--- /dev/null
+++ b/src/util/cache.rs
@@ -0,0 +1,24 @@
+use std::env;
+
+use cached::DiskCache;
+use directories::BaseDirs;
+use serde::{Serialize, de::DeserializeOwned};
+
+pub fn make_cache<K, V>(name: &str) -> DiskCache<K, V>
+where
+    K: ToString,
+    V: Serialize + DeserializeOwned,
+{
+    let dir = match BaseDirs::new() {
+        Some(base_dirs) => base_dirs.cache_dir().join("lleap"),
+        None => env::current_dir()
+            .expect("unable to determine current directory")
+            .join("lleap_cache"),
+    };
+
+    DiskCache::new(name)
+        .set_disk_directory(dir)
+        .set_sync_to_disk_on_cache_change(true)
+        .build()
+        .expect("unable to open disk cache")
+}
diff --git a/src/util/mod.rs b/src/util/mod.rs
index 4d19eff..2098a35 100644
--- a/src/util/mod.rs
+++ b/src/util/mod.rs
@@ -1,3 +1,5 @@
+mod cache;
 mod tracker;
 
+pub use cache::make_cache;
 pub use tracker::Tracker;