Major update with lots of new features and fixes
This commit is contained in:
parent
17c149c21a
commit
74b3efd990
16 changed files with 1648 additions and 904 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -4,6 +4,11 @@
|
||||||
# Rust build artifacts
|
# Rust build artifacts
|
||||||
/target/
|
/target/
|
||||||
|
|
||||||
|
# makepkg build artifacts (finished .pkg.tar.zst is NOT ignored)
|
||||||
|
/pkg/
|
||||||
|
/src/yt-offline/
|
||||||
|
/yt-offline/
|
||||||
|
|
||||||
# Editor / OS cruft
|
# Editor / OS cruft
|
||||||
*.swp
|
*.swp
|
||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
|
||||||
31
.vscode/launch.json
vendored
Normal file
31
.vscode/launch.json
vendored
Normal file
|
|
@ -0,0 +1,31 @@
|
||||||
|
{
|
||||||
|
// Use IntelliSense to learn about possible attributes.
|
||||||
|
// Hover to view descriptions of existing attributes.
|
||||||
|
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||||
|
"version": "0.2.0",
|
||||||
|
"configurations": [
|
||||||
|
{
|
||||||
|
"name": "Debug executable 'yt-offline'",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"run",
|
||||||
|
"--bin=yt-offline"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"args": []
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "Debug unit tests in executable 'yt-offline'",
|
||||||
|
"type": "lldb",
|
||||||
|
"request": "launch",
|
||||||
|
"cargo": {
|
||||||
|
"args": [
|
||||||
|
"test",
|
||||||
|
"--bin=yt-offline"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
677
Cargo.lock
generated
677
Cargo.lock
generated
|
|
@ -208,7 +208,7 @@ version = "0.38.0+1.3.281"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
|
checksum = "0bb44936d800fea8f016d7f2311c6a4f97aebd5dc86f09906139ec848cf3a46f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -353,29 +353,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atk"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "241b621213072e993be4f6f3a9e4b45f65b7e6faad43001be957184b7bb1824b"
|
|
||||||
dependencies = [
|
|
||||||
"atk-sys",
|
|
||||||
"glib",
|
|
||||||
"libc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "atk-sys"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "c5e48b684b0ca77d2bbadeef17424c2ea3c897d44d566a1617e7e8f30614d086"
|
|
||||||
dependencies = [
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "atomic-waker"
|
name = "atomic-waker"
|
||||||
version = "1.1.2"
|
version = "1.1.2"
|
||||||
|
|
@ -532,9 +509,6 @@ name = "bitflags"
|
||||||
version = "2.11.1"
|
version = "2.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
|
||||||
dependencies = [
|
|
||||||
"serde_core",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "block"
|
name = "block"
|
||||||
|
|
@ -620,31 +594,6 @@ version = "1.11.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cairo-rs"
|
|
||||||
version = "0.18.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8ca26ef0159422fb77631dc9d17b102f253b876fe1586b03b803e63a309b4ee2"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"cairo-sys-rs",
|
|
||||||
"glib",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cairo-sys-rs"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "685c9fa8e590b8b3d678873528d83411db17242a73fccaed827770ea0fedda51"
|
|
||||||
dependencies = [
|
|
||||||
"glib-sys",
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "calloop"
|
name = "calloop"
|
||||||
version = "0.13.0"
|
version = "0.13.0"
|
||||||
|
|
@ -708,16 +657,6 @@ dependencies = [
|
||||||
"shlex",
|
"shlex",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cfg-expr"
|
|
||||||
version = "0.15.8"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d067ad48b8650848b989a59a86c6c36a995d02d2bf778d45c3c5d57bc2718f02"
|
|
||||||
dependencies = [
|
|
||||||
"smallvec",
|
|
||||||
"target-lexicon",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "cfg-if"
|
name = "cfg-if"
|
||||||
version = "1.0.4"
|
version = "1.0.4"
|
||||||
|
|
@ -754,36 +693,6 @@ dependencies = [
|
||||||
"error-code",
|
"error-code",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cocoa"
|
|
||||||
version = "0.25.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "f6140449f97a6e97f9511815c5632d84c8aacf8ac271ad77c559218161a1373c"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"block",
|
|
||||||
"cocoa-foundation",
|
|
||||||
"core-foundation 0.9.4",
|
|
||||||
"core-graphics",
|
|
||||||
"foreign-types",
|
|
||||||
"libc",
|
|
||||||
"objc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "cocoa-foundation"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8c6234cbb2e4c785b456c0644748b1ac416dd045799740356f8363dfe00c93f7"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"block",
|
|
||||||
"core-foundation 0.9.4",
|
|
||||||
"core-graphics-types",
|
|
||||||
"libc",
|
|
||||||
"objc",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "codespan-reporting"
|
name = "codespan-reporting"
|
||||||
version = "0.11.1"
|
version = "0.11.1"
|
||||||
|
|
@ -912,15 +821,6 @@ dependencies = [
|
||||||
"cfg-if",
|
"cfg-if",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "crossbeam-channel"
|
|
||||||
version = "0.5.15"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
|
|
||||||
dependencies = [
|
|
||||||
"crossbeam-utils",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "crossbeam-utils"
|
name = "crossbeam-utils"
|
||||||
version = "0.8.21"
|
version = "0.8.21"
|
||||||
|
|
@ -962,27 +862,6 @@ dependencies = [
|
||||||
"crypto-common",
|
"crypto-common",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-next"
|
|
||||||
version = "2.0.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"dirs-sys-next",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "dirs-sys-next"
|
|
||||||
version = "0.1.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "4ebda144c4fe02d1f7ea1a7d9641b6fc6b580adcfa024ae48797ecdeb6825b4d"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"redox_users",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "dispatch"
|
name = "dispatch"
|
||||||
version = "0.2.0"
|
version = "0.2.0"
|
||||||
|
|
@ -1016,7 +895,7 @@ version = "0.5.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
checksum = "ab8ecd87370524b461f8557c119c405552c396ed91fc0a8eec68679eab26f94a"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -1282,16 +1161,6 @@ dependencies = [
|
||||||
"simd-adler32",
|
"simd-adler32",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "field-offset"
|
|
||||||
version = "0.3.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
|
|
||||||
dependencies = [
|
|
||||||
"memoffset",
|
|
||||||
"rustc_version",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "find-msvc-tools"
|
name = "find-msvc-tools"
|
||||||
version = "0.1.9"
|
version = "0.1.9"
|
||||||
|
|
@ -1365,17 +1234,6 @@ version = "0.3.32"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "futures-executor"
|
|
||||||
version = "0.3.32"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
|
|
||||||
dependencies = [
|
|
||||||
"futures-core",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-io"
|
name = "futures-io"
|
||||||
version = "0.3.32"
|
version = "0.3.32"
|
||||||
|
|
@ -1434,64 +1292,6 @@ dependencies = [
|
||||||
"slab",
|
"slab",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gdk"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d9f245958c627ac99d8e529166f9823fb3b838d1d41fd2b297af3075093c2691"
|
|
||||||
dependencies = [
|
|
||||||
"cairo-rs",
|
|
||||||
"gdk-pixbuf",
|
|
||||||
"gdk-sys",
|
|
||||||
"gio",
|
|
||||||
"glib",
|
|
||||||
"libc",
|
|
||||||
"pango",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gdk-pixbuf"
|
|
||||||
version = "0.18.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "50e1f5f1b0bfb830d6ccc8066d18db35c487b1b2b1e8589b5dfe9f07e8defaec"
|
|
||||||
dependencies = [
|
|
||||||
"gdk-pixbuf-sys",
|
|
||||||
"gio",
|
|
||||||
"glib",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gdk-pixbuf-sys"
|
|
||||||
version = "0.18.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "3f9839ea644ed9c97a34d129ad56d38a25e6756f99f3a88e15cd39c20629caf7"
|
|
||||||
dependencies = [
|
|
||||||
"gio-sys",
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gdk-sys"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "5c2d13f38594ac1e66619e188c6d5a1adb98d11b2fcf7894fc416ad76aa2f3f7"
|
|
||||||
dependencies = [
|
|
||||||
"cairo-sys-rs",
|
|
||||||
"gdk-pixbuf-sys",
|
|
||||||
"gio-sys",
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"pango-sys",
|
|
||||||
"pkg-config",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "generic-array"
|
name = "generic-array"
|
||||||
version = "0.14.7"
|
version = "0.14.7"
|
||||||
|
|
@ -1548,38 +1348,6 @@ dependencies = [
|
||||||
"wasip3",
|
"wasip3",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gio"
|
|
||||||
version = "0.18.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "d4fc8f532f87b79cbc51a79748f16a6828fb784be93145a322fa14d06d354c73"
|
|
||||||
dependencies = [
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-io",
|
|
||||||
"futures-util",
|
|
||||||
"gio-sys",
|
|
||||||
"glib",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"pin-project-lite",
|
|
||||||
"smallvec",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gio-sys"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "37566df850baf5e4cb0dfb78af2e4b9898d817ed9263d1090a2df958c64737d2"
|
|
||||||
dependencies = [
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gl_generator"
|
name = "gl_generator"
|
||||||
version = "0.14.0"
|
version = "0.14.0"
|
||||||
|
|
@ -1591,53 +1359,6 @@ dependencies = [
|
||||||
"xml-rs",
|
"xml-rs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glib"
|
|
||||||
version = "0.18.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "233daaf6e83ae6a12a52055f568f9d7cf4671dabb78ff9560ab6da230ce00ee5"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"futures-channel",
|
|
||||||
"futures-core",
|
|
||||||
"futures-executor",
|
|
||||||
"futures-task",
|
|
||||||
"futures-util",
|
|
||||||
"gio-sys",
|
|
||||||
"glib-macros",
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"memchr",
|
|
||||||
"once_cell",
|
|
||||||
"smallvec",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glib-macros"
|
|
||||||
version = "0.18.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc"
|
|
||||||
dependencies = [
|
|
||||||
"heck 0.4.1",
|
|
||||||
"proc-macro-crate 2.0.2",
|
|
||||||
"proc-macro-error",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "glib-sys"
|
|
||||||
version = "0.18.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "063ce2eb6a8d0ea93d2bf8ba1957e78dbab6be1c2220dd3daca57d5a9d869898"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "glow"
|
name = "glow"
|
||||||
version = "0.13.1"
|
version = "0.13.1"
|
||||||
|
|
@ -1675,7 +1396,7 @@ dependencies = [
|
||||||
"glutin_egl_sys",
|
"glutin_egl_sys",
|
||||||
"glutin_glx_sys",
|
"glutin_glx_sys",
|
||||||
"glutin_wgl_sys",
|
"glutin_wgl_sys",
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
"objc2 0.6.4",
|
"objc2 0.6.4",
|
||||||
"objc2-app-kit 0.3.2",
|
"objc2-app-kit 0.3.2",
|
||||||
"objc2-core-foundation",
|
"objc2-core-foundation",
|
||||||
|
|
@ -1728,17 +1449,6 @@ dependencies = [
|
||||||
"gl_generator",
|
"gl_generator",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gobject-sys"
|
|
||||||
version = "0.18.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "0850127b514d1c4a4654ead6dedadb18198999985908e6ffe4436f53c785ce44"
|
|
||||||
dependencies = [
|
|
||||||
"glib-sys",
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "gpu-alloc"
|
name = "gpu-alloc"
|
||||||
version = "0.6.0"
|
version = "0.6.0"
|
||||||
|
|
@ -1791,58 +1501,6 @@ dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gtk"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "fd56fb197bfc42bd5d2751f4f017d44ff59fbb58140c6b49f9b3b2bdab08506a"
|
|
||||||
dependencies = [
|
|
||||||
"atk",
|
|
||||||
"cairo-rs",
|
|
||||||
"field-offset",
|
|
||||||
"futures-channel",
|
|
||||||
"gdk",
|
|
||||||
"gdk-pixbuf",
|
|
||||||
"gio",
|
|
||||||
"glib",
|
|
||||||
"gtk-sys",
|
|
||||||
"gtk3-macros",
|
|
||||||
"libc",
|
|
||||||
"pango",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gtk-sys"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "8f29a1c21c59553eb7dd40e918be54dccd60c52b049b75119d5d96ce6b624414"
|
|
||||||
dependencies = [
|
|
||||||
"atk-sys",
|
|
||||||
"cairo-sys-rs",
|
|
||||||
"gdk-pixbuf-sys",
|
|
||||||
"gdk-sys",
|
|
||||||
"gio-sys",
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"pango-sys",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "gtk3-macros"
|
|
||||||
version = "0.18.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d"
|
|
||||||
dependencies = [
|
|
||||||
"proc-macro-crate 1.3.1",
|
|
||||||
"proc-macro-error",
|
|
||||||
"proc-macro2",
|
|
||||||
"quote",
|
|
||||||
"syn 2.0.117",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "hashbrown"
|
name = "hashbrown"
|
||||||
version = "0.14.5"
|
version = "0.14.5"
|
||||||
|
|
@ -1885,18 +1543,12 @@ dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"com",
|
"com",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
"thiserror 1.0.69",
|
"thiserror 1.0.69",
|
||||||
"widestring",
|
"widestring",
|
||||||
"winapi",
|
"winapi",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "heck"
|
|
||||||
version = "0.4.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "heck"
|
name = "heck"
|
||||||
version = "0.5.0"
|
version = "0.5.0"
|
||||||
|
|
@ -1954,6 +1606,12 @@ dependencies = [
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "http-range-header"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "httparse"
|
name = "httparse"
|
||||||
version = "1.10.1"
|
version = "1.10.1"
|
||||||
|
|
@ -2121,7 +1779,7 @@ dependencies = [
|
||||||
"image-webp",
|
"image-webp",
|
||||||
"moxcms",
|
"moxcms",
|
||||||
"num-traits",
|
"num-traits",
|
||||||
"png 0.18.1",
|
"png",
|
||||||
"zune-core",
|
"zune-core",
|
||||||
"zune-jpeg",
|
"zune-jpeg",
|
||||||
]
|
]
|
||||||
|
|
@ -2243,17 +1901,6 @@ dependencies = [
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "keyboard-types"
|
|
||||||
version = "0.7.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b750dcadc39a09dbadd74e118f6dd6598df77fa01df0cfcdc52c28dece74528a"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 2.11.1",
|
|
||||||
"serde",
|
|
||||||
"unicode-segmentation",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "khronos-egl"
|
name = "khronos-egl"
|
||||||
version = "6.0.0"
|
version = "6.0.0"
|
||||||
|
|
@ -2261,7 +1908,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
|
checksum = "6aae1df220ece3c0ada96b8153459b67eebe9ae9212258bb0134ae60416fdf76"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
"pkg-config",
|
"pkg-config",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -2277,46 +1924,12 @@ version = "0.1.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libappindicator"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "03589b9607c868cc7ae54c0b2a22c8dc03dd41692d48f2d7df73615c6a95dc0a"
|
|
||||||
dependencies = [
|
|
||||||
"glib",
|
|
||||||
"gtk",
|
|
||||||
"gtk-sys",
|
|
||||||
"libappindicator-sys",
|
|
||||||
"log",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libappindicator-sys"
|
|
||||||
version = "0.9.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "6e9ec52138abedcc58dc17a7c6c0c00a2bdb4f3427c7f63fa97fd0d859155caf"
|
|
||||||
dependencies = [
|
|
||||||
"gtk-sys",
|
|
||||||
"libloading 0.7.4",
|
|
||||||
"once_cell",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libc"
|
name = "libc"
|
||||||
version = "0.2.186"
|
version = "0.2.186"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libloading"
|
|
||||||
version = "0.7.4"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b67380fd3b2fbe7527a606e18729d21c6f3951633d0500574c4dc22d2d638b9f"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-if",
|
|
||||||
"winapi",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libloading"
|
name = "libloading"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -2350,25 +1963,6 @@ dependencies = [
|
||||||
"vcpkg",
|
"vcpkg",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libxdo"
|
|
||||||
version = "0.6.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "00333b8756a3d28e78def82067a377de7fa61b24909000aeaa2b446a948d14db"
|
|
||||||
dependencies = [
|
|
||||||
"libxdo-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "libxdo-sys"
|
|
||||||
version = "0.11.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "db23b9e7e2b7831bbd8aac0bbeeeb7b68cbebc162b227e7052e8e55829a09212"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"x11",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "linux-raw-sys"
|
name = "linux-raw-sys"
|
||||||
version = "0.4.15"
|
version = "0.4.15"
|
||||||
|
|
@ -2480,6 +2074,16 @@ version = "0.3.17"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
|
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]]
|
[[package]]
|
||||||
name = "miniz_oxide"
|
name = "miniz_oxide"
|
||||||
version = "0.8.9"
|
version = "0.8.9"
|
||||||
|
|
@ -2511,25 +2115,6 @@ dependencies = [
|
||||||
"pxfm",
|
"pxfm",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "muda"
|
|
||||||
version = "0.13.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "86b959f97c97044e4c96e32e1db292a7d594449546a3c6b77ae613dc3a5b5145"
|
|
||||||
dependencies = [
|
|
||||||
"cocoa",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"dpi",
|
|
||||||
"gtk",
|
|
||||||
"keyboard-types",
|
|
||||||
"libxdo",
|
|
||||||
"objc",
|
|
||||||
"once_cell",
|
|
||||||
"png 0.17.16",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "naga"
|
name = "naga"
|
||||||
version = "22.1.0"
|
version = "22.1.0"
|
||||||
|
|
@ -2654,7 +2239,7 @@ version = "0.7.6"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
checksum = "680998035259dcfcafe653688bf2aa6d3e2dc05e98be6ab46afb089dc84f1df8"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.5.0",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -2977,31 +2562,6 @@ dependencies = [
|
||||||
"ttf-parser",
|
"ttf-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pango"
|
|
||||||
version = "0.18.3"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "7ca27ec1eb0457ab26f3036ea52229edbdb74dee1edd29063f5b9b010e7ebee4"
|
|
||||||
dependencies = [
|
|
||||||
"gio",
|
|
||||||
"glib",
|
|
||||||
"libc",
|
|
||||||
"once_cell",
|
|
||||||
"pango-sys",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "pango-sys"
|
|
||||||
version = "0.18.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "436737e391a843e5933d6d9aa102cb126d501e815b83601365a948a518555dc5"
|
|
||||||
dependencies = [
|
|
||||||
"glib-sys",
|
|
||||||
"gobject-sys",
|
|
||||||
"libc",
|
|
||||||
"system-deps",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "parking"
|
name = "parking"
|
||||||
version = "2.2.1"
|
version = "2.2.1"
|
||||||
|
|
@ -3092,19 +2652,6 @@ version = "0.2.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "png"
|
|
||||||
version = "0.17.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "82151a2fc869e011c153adc57cf2789ccb8d9906ce52c0b39a6b5697749d7526"
|
|
||||||
dependencies = [
|
|
||||||
"bitflags 1.3.2",
|
|
||||||
"crc32fast",
|
|
||||||
"fdeflate",
|
|
||||||
"flate2",
|
|
||||||
"miniz_oxide",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "png"
|
name = "png"
|
||||||
version = "0.18.1"
|
version = "0.18.1"
|
||||||
|
|
@ -3172,26 +2719,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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 = "2.0.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "b00f26d3400549137f92511a46ac1cd8ce37cb5598a96d382381458b992a5d24"
|
|
||||||
dependencies = [
|
|
||||||
"toml_datetime 0.6.3",
|
|
||||||
"toml_edit 0.20.2",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "proc-macro-crate"
|
name = "proc-macro-crate"
|
||||||
version = "3.5.0"
|
version = "3.5.0"
|
||||||
|
|
@ -3201,30 +2728,6 @@ dependencies = [
|
||||||
"toml_edit 0.25.11+spec-1.1.0",
|
"toml_edit 0.25.11+spec-1.1.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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]]
|
[[package]]
|
||||||
name = "proc-macro2"
|
name = "proc-macro2"
|
||||||
version = "1.0.106"
|
version = "1.0.106"
|
||||||
|
|
@ -3364,17 +2867,6 @@ dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "redox_users"
|
|
||||||
version = "0.4.6"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "ba009ff324d1fc1b900bd1fdb31564febe58a8ccc8a6fdbb93b543d33b13ca43"
|
|
||||||
dependencies = [
|
|
||||||
"getrandom 0.2.17",
|
|
||||||
"libredox",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "renderdoc-sys"
|
name = "renderdoc-sys"
|
||||||
version = "1.1.0"
|
version = "1.1.0"
|
||||||
|
|
@ -3798,25 +3290,6 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "system-deps"
|
|
||||||
version = "6.2.2"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "a3e535eb8dded36d55ec13eddacd30dec501792ff23a0b1682c38601b8cf2349"
|
|
||||||
dependencies = [
|
|
||||||
"cfg-expr",
|
|
||||||
"heck 0.5.0",
|
|
||||||
"pkg-config",
|
|
||||||
"toml",
|
|
||||||
"version-compare",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "target-lexicon"
|
|
||||||
version = "0.12.16"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tauri-winrt-notification"
|
name = "tauri-winrt-notification"
|
||||||
version = "0.7.2"
|
version = "0.7.2"
|
||||||
|
|
@ -3973,6 +3446,30 @@ dependencies = [
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-stream"
|
||||||
|
version = "0.1.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "32da49809aab5c3bc678af03902d4ccddea2a87d028d86392a4b1560c6906c70"
|
||||||
|
dependencies = [
|
||||||
|
"futures-core",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tokio-util"
|
||||||
|
version = "0.7.18"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "9ae9cec805b01e8fc3fd2fe289f89149a9b66dd16786abd8b19cfa7b48cb0098"
|
||||||
|
dependencies = [
|
||||||
|
"bytes",
|
||||||
|
"futures-core",
|
||||||
|
"futures-sink",
|
||||||
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml"
|
name = "toml"
|
||||||
version = "0.8.2"
|
version = "0.8.2"
|
||||||
|
|
@ -4003,17 +3500,6 @@ dependencies = [
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[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.3",
|
|
||||||
"winnow 0.5.40",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "toml_edit"
|
name = "toml_edit"
|
||||||
version = "0.20.2"
|
version = "0.20.2"
|
||||||
|
|
@ -4072,12 +3558,21 @@ checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bitflags 2.11.1",
|
"bitflags 2.11.1",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
"futures-util",
|
||||||
"http",
|
"http",
|
||||||
"http-body",
|
"http-body",
|
||||||
"http-body-util",
|
"http-body-util",
|
||||||
|
"http-range-header",
|
||||||
|
"httpdate",
|
||||||
|
"mime",
|
||||||
|
"mime_guess",
|
||||||
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
"tokio",
|
||||||
|
"tokio-util",
|
||||||
"tower-layer",
|
"tower-layer",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
|
"tracing",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -4124,25 +3619,6 @@ dependencies = [
|
||||||
"once_cell",
|
"once_cell",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "tray-icon"
|
|
||||||
version = "0.13.5"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "39240037d755a1832e752d64f99078c3b0b21c09a71c12405070c75ef4e7cd3c"
|
|
||||||
dependencies = [
|
|
||||||
"cocoa",
|
|
||||||
"core-graphics",
|
|
||||||
"crossbeam-channel",
|
|
||||||
"dirs-next",
|
|
||||||
"libappindicator",
|
|
||||||
"muda",
|
|
||||||
"objc",
|
|
||||||
"once_cell",
|
|
||||||
"png 0.17.16",
|
|
||||||
"thiserror 1.0.69",
|
|
||||||
"windows-sys 0.52.0",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ttf-parser"
|
name = "ttf-parser"
|
||||||
version = "0.25.1"
|
version = "0.25.1"
|
||||||
|
|
@ -4175,6 +3651,12 @@ dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.61.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "unicase"
|
||||||
|
version = "2.9.0"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "unicode-ident"
|
name = "unicode-ident"
|
||||||
version = "1.0.24"
|
version = "1.0.24"
|
||||||
|
|
@ -4234,12 +3716,6 @@ version = "0.2.15"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "version-compare"
|
|
||||||
version = "0.2.1"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "version_check"
|
name = "version_check"
|
||||||
version = "0.9.5"
|
version = "0.9.5"
|
||||||
|
|
@ -4610,7 +4086,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"khronos-egl",
|
"khronos-egl",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
"log",
|
"log",
|
||||||
"metal",
|
"metal",
|
||||||
"naga",
|
"naga",
|
||||||
|
|
@ -5144,7 +4620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"heck 0.5.0",
|
"heck",
|
||||||
"wit-parser",
|
"wit-parser",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
@ -5155,7 +4631,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"heck 0.5.0",
|
"heck",
|
||||||
"indexmap",
|
"indexmap",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -5222,16 +4698,6 @@ version = "0.6.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "x11"
|
|
||||||
version = "2.21.0"
|
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
|
||||||
checksum = "502da5464ccd04011667b11c435cb992822c2c0dbde1770c988480d312a0db2e"
|
|
||||||
dependencies = [
|
|
||||||
"libc",
|
|
||||||
"pkg-config",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "x11-dl"
|
name = "x11-dl"
|
||||||
version = "2.21.0"
|
version = "2.21.0"
|
||||||
|
|
@ -5252,7 +4718,7 @@ dependencies = [
|
||||||
"as-raw-xcb-connection",
|
"as-raw-xcb-connection",
|
||||||
"gethostname",
|
"gethostname",
|
||||||
"libc",
|
"libc",
|
||||||
"libloading 0.8.9",
|
"libloading",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"x11rb-protocol",
|
"x11rb-protocol",
|
||||||
|
|
@ -5340,9 +4806,10 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
"tokio-stream",
|
||||||
|
"tokio-util",
|
||||||
"toml",
|
"toml",
|
||||||
"tower-http",
|
"tower-http",
|
||||||
"tray-icon",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5448,7 +4915,7 @@ version = "4.4.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
|
checksum = "267db9407081e90bbfa46d841d3cbc60f59c0351838c4bc65199ecd79ab1983e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.5.0",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -5461,7 +4928,7 @@ version = "5.15.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
|
checksum = "51fa5406ad9175a8c825a931f8cf347116b531b3634fcb0b627c290f1f2516ff"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.5.0",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -5633,7 +5100,7 @@ version = "4.2.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
|
checksum = "73e2ba546bda683a90652bac4a279bc146adad1386f25379cf73200d2002c449"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.5.0",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -5646,7 +5113,7 @@ version = "5.11.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
|
checksum = "c7d5b780599bbde114e39d9a0799577fad1ced5105d38515745f7b3099d8ceda"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro-crate 3.5.0",
|
"proc-macro-crate",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,12 @@ toml = "0.8"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
rusqlite = { version = "0.31", features = ["bundled"] }
|
rusqlite = { version = "0.31", features = ["bundled"] }
|
||||||
tray-icon = "0.13"
|
|
||||||
notify-rust = { version = "4", default-features = false, features = ["z"] }
|
notify-rust = { version = "4", default-features = false, features = ["z"] }
|
||||||
axum = { version = "0.7", features = ["macros"] }
|
axum = { version = "0.7", features = ["macros"] }
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower-http = { version = "0.5", features = ["cors"] }
|
tokio-util = { version = "0.7", features = ["io"] }
|
||||||
|
tokio-stream = "0.1"
|
||||||
|
tower-http = { version = "0.5", features = ["cors", "fs"] }
|
||||||
|
|
||||||
[profile.release]
|
[profile.release]
|
||||||
opt-level = 2
|
opt-level = 2
|
||||||
|
|
|
||||||
212
SECURITY_AUDIT.md
Normal file
212
SECURITY_AUDIT.md
Normal file
|
|
@ -0,0 +1,212 @@
|
||||||
|
# Security Audit — yt-offline
|
||||||
|
|
||||||
|
**Date:** 2026-05-17
|
||||||
|
**Scope:** Rust codebase + dependencies
|
||||||
|
**Threat Model:** Self-hosted personal YouTube archiving tool; single user accessing via localhost + LAN
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
✅ **Overall Risk: LOW**
|
||||||
|
|
||||||
|
No critical vulnerabilities found. The codebase follows security best practices for command execution, database access, and file serving. One informational warning on a transitive dependency (low risk).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Findings
|
||||||
|
|
||||||
|
### ✅ Command Injection — SECURE
|
||||||
|
|
||||||
|
All external process invocations use `std::process::Command` or `tokio::process::Command` with `.arg()` calls, bypassing shell interpretation:
|
||||||
|
|
||||||
|
- **yt-dlp invocations** (`src/downloader.rs:160–188`): User URL passed as a clean argument, not shell-interpreted. Safe even with special characters.
|
||||||
|
- **ffmpeg invocations** (`src/web.rs:404–415`): Similarly safe, using separate `.arg()` calls.
|
||||||
|
- **Preview handler** (`src/web.rs:492–498`): yt-dlp preview command also uses `.arg()`, not vulnerable to command injection.
|
||||||
|
|
||||||
|
**No shell escaping needed; no `format!()` in command strings.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ SQL Injection — SECURE
|
||||||
|
|
||||||
|
All database queries in `src/database.rs` use **parameterized statements**:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// Correct — parameter substitution
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO watched (video_id) VALUES (?1)",
|
||||||
|
[video_id],
|
||||||
|
)?;
|
||||||
|
|
||||||
|
// Correct — rusqlite::params! macro
|
||||||
|
self.conn.execute(
|
||||||
|
"INSERT OR REPLACE INTO positions (video_id, position_secs) VALUES (?1, ?2)",
|
||||||
|
rusqlite::params![video_id, position_secs],
|
||||||
|
)?;
|
||||||
|
```
|
||||||
|
|
||||||
|
**No string concatenation in SQL strings.** Video IDs come from path parameters and are bound safely.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Path Traversal — SECURE
|
||||||
|
|
||||||
|
**File serving** (`src/web.rs:687`):
|
||||||
|
- Uses `tower_http::services::ServeDir::new(&channels_root)` — a well-audited crate that prevents `..` traversal.
|
||||||
|
|
||||||
|
**Info.json access** (`src/web.rs:230–236`):
|
||||||
|
- Videos are looked up **only** from the scanned library (`find_video_info_path`).
|
||||||
|
- Cannot directly request arbitrary files by path; must exist in the library.
|
||||||
|
|
||||||
|
**URL path encoding** (`src/web.rs:176–186`):
|
||||||
|
- Correct percent-encoding of URL segments.
|
||||||
|
- Path components are filtered to `Component::Normal` (rejects `..` and symlinks).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ✅ Unsafe Code — NONE FOUND
|
||||||
|
|
||||||
|
**Result of search:** `grep -r "unsafe"` across all `.rs` files returns **zero unsafe blocks**.
|
||||||
|
|
||||||
|
All memory safety is guaranteed by Rust's type system. No C FFI or raw pointers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### ⚠️ Dependency Warning (LOW RISK)
|
||||||
|
|
||||||
|
**Crate:** `paste` v1.0.15
|
||||||
|
**Status:** Unmaintained since 2024-10-07
|
||||||
|
**Severity:** Warning (not an error)
|
||||||
|
**Dependency Chain:**
|
||||||
|
```
|
||||||
|
paste 1.0.15
|
||||||
|
← egui-wgpu (GUI rendering)
|
||||||
|
← eframe (GUI framework)
|
||||||
|
← yt-offline (your app)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Impact:**
|
||||||
|
- `paste` is a procedural macro for code generation; it does not process untrusted input.
|
||||||
|
- The crate is used only at compile-time for the GUI stack.
|
||||||
|
- No security-critical functionality in `paste`.
|
||||||
|
- **Mitigation:** Monitor for a replacement in the egui/eframe ecosystem. No action needed now.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Input Validation
|
||||||
|
|
||||||
|
### Download URLs
|
||||||
|
- **Handler:** `post_download()` (`src/web.rs:339–350`)
|
||||||
|
- **Validation:** Trimmed and checked for empty; passed directly to yt-dlp via `.arg()`.
|
||||||
|
- **Risk:** Low — yt-dlp validates URLs server-side and fails gracefully on malformed input.
|
||||||
|
|
||||||
|
### Video IDs
|
||||||
|
- **Source:** Extracted from URL paths and library scans.
|
||||||
|
- **Usage:** Passed to SQL via parameterized queries and used to look up files in the library.
|
||||||
|
- **Risk:** Low — parameterized queries eliminate injection; library lookup prevents arbitrary file access.
|
||||||
|
|
||||||
|
### Position values
|
||||||
|
- **Handler:** `post_resume()` (`src/web.rs:464–479`)
|
||||||
|
- **Validation:** Numeric (f64) — deserialized by serde, bounds-checked (position > 3.0 filter).
|
||||||
|
- **Risk:** Low — stored in SQLite as REAL, no string manipulation.
|
||||||
|
|
||||||
|
### Preview query (yt-dlp URL)
|
||||||
|
- **Handler:** `get_preview()` (`src/web.rs:484–527`)
|
||||||
|
- **Validation:** Trimmed and checked for empty.
|
||||||
|
- **Risk:** Low — passed to yt-dlp via `.arg()`, no shell injection.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Data Exposure
|
||||||
|
|
||||||
|
### Watched status & resume positions
|
||||||
|
- **Storage:** SQLite database (`yt-offline.db`), local filesystem.
|
||||||
|
- **Access:** Only available to the user running the app (same UID).
|
||||||
|
- **No network exposure:** Positions are stored server-side; browser caches position locally.
|
||||||
|
- **Risk:** Low — local-only; no credentials or PII stored.
|
||||||
|
|
||||||
|
### Download logs
|
||||||
|
- **Stored in:** Job objects, kept in RAM and shown in the UI.
|
||||||
|
- **Leakage risk:** stderr from yt-dlp is visible in the web UI; sensitive auth errors could be logged.
|
||||||
|
- **Mitigation:** Consider filtering `--cookies` error messages in production. Currently acceptable for personal use.
|
||||||
|
|
||||||
|
### yt-dlp output (preview, metadata)
|
||||||
|
- **Handler:** Parses JSON from yt-dlp; returns filtered fields to the browser.
|
||||||
|
- **Risk:** Low — only public YouTube metadata (title, duration, view count).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration & Secrets
|
||||||
|
|
||||||
|
### `cookies.txt`
|
||||||
|
- **Location:** Current working directory (hardcoded in `downloader.rs`).
|
||||||
|
- **Risk:** File is world-readable by default. **Recommendation:** Ensure restrictive file permissions (`chmod 600 cookies.txt`).
|
||||||
|
|
||||||
|
### `config.toml`
|
||||||
|
- **Contains:** Directory paths, port number, browser name, `source_url`.
|
||||||
|
- **Risk:** Low — no secrets stored. `source_url` is read-only from browser.
|
||||||
|
- **Sensitive data:** Only the `backup.directory` path could leak file structure; this is expected behavior.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Network Security
|
||||||
|
|
||||||
|
### Web server scope
|
||||||
|
- **Binding:** Configured via `web.port` in `config.toml` (default 8080).
|
||||||
|
- **Current binding:** Likely `127.0.0.1:8080` (assumed; verify with netstat).
|
||||||
|
- **Risk:** If bound to `0.0.0.0`, anyone on the network can access. **Recommendation:** Bind to `127.0.0.1` or use a reverse proxy with authentication.
|
||||||
|
|
||||||
|
### No HTTPS
|
||||||
|
- **Status:** Web UI is served over HTTP.
|
||||||
|
- **Risk:** Medium if exposed to untrusted networks. **Recommendation:** Use a reverse proxy (nginx, Caddy) with TLS for remote access.
|
||||||
|
|
||||||
|
### No authentication
|
||||||
|
- **Status:** No login required; access is "security by obscurity" (port number).
|
||||||
|
- **Risk:** Medium if the port is discovered. **Recommendation:** Add optional HTTP Basic Auth or reverse proxy authentication for LAN access.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Recommendations (Priority Order)
|
||||||
|
|
||||||
|
### HIGH
|
||||||
|
1. **Verify web server binding** — Ensure `axum` binds to `127.0.0.1` only, not `0.0.0.0`.
|
||||||
|
- Check: `src/web.rs:618` where the listener is created.
|
||||||
|
- Mitigation: Add a config option for `bind_addr` or document localhost-only setup.
|
||||||
|
|
||||||
|
### MEDIUM
|
||||||
|
2. **File permissions on `cookies.txt`** — Remind users to `chmod 600 cookies.txt`.
|
||||||
|
- Add note to README.
|
||||||
|
|
||||||
|
3. **TLS + authentication for remote access** — If exposing to LAN/internet:
|
||||||
|
- Use a reverse proxy (nginx, Caddy) with mTLS or Basic Auth.
|
||||||
|
- Or add built-in HTTP Basic Auth via an optional config field.
|
||||||
|
|
||||||
|
4. **Filter sensitive logs** — Consider not echoing yt-dlp stderr if it contains auth errors.
|
||||||
|
- Current: Acceptable for personal use. Not a blocker.
|
||||||
|
|
||||||
|
### LOW
|
||||||
|
5. **Update `paste` crate dependency chain** — Monitor egui/eframe for a replacement.
|
||||||
|
- Not urgent; no security impact in your use case.
|
||||||
|
|
||||||
|
6. **Add Content-Security-Policy headers** — Prevent XSS if web UI is exposed to untrusted networks.
|
||||||
|
- Current: Low risk (browser only, no JavaScript vulnerabilities found).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Conclusion
|
||||||
|
|
||||||
|
The codebase demonstrates **strong security practices** for a personal self-hosted tool:
|
||||||
|
- ✅ No injection vulnerabilities (command, SQL).
|
||||||
|
- ✅ No unsafe code.
|
||||||
|
- ✅ Path traversal protection in place.
|
||||||
|
- ✅ Parameterized database queries.
|
||||||
|
- ✅ Safe process spawning.
|
||||||
|
|
||||||
|
**No blockers for personal use.** For public/LAN exposure, implement the HIGH-priority recommendations.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Auditor:** Claude Code
|
||||||
|
**Audit Method:** Manual code review + cargo audit
|
||||||
|
**Status:** Complete
|
||||||
16
config.toml
16
config.toml
|
|
@ -1,8 +1,18 @@
|
||||||
[backup]
|
[backup]
|
||||||
# Directory where channel backups are stored
|
|
||||||
directory = "/mnt/InannaBeloved/youtube-backup/channels"
|
directory = "/mnt/InannaBeloved/youtube-backup/channels"
|
||||||
|
|
||||||
[player]
|
[player]
|
||||||
# Video player command (default: mpv)
|
|
||||||
# Examples: "mpv", "vlc", "ffplay"
|
|
||||||
command = "mpv"
|
command = "mpv"
|
||||||
|
browser = "firefox"
|
||||||
|
|
||||||
|
[ui]
|
||||||
|
theme = "dark"
|
||||||
|
|
||||||
|
[scheduler]
|
||||||
|
enabled = false
|
||||||
|
interval_hours = 24
|
||||||
|
|
||||||
|
[web]
|
||||||
|
port = 8081
|
||||||
|
transcode = false
|
||||||
|
# source_url = "https://codeberg.org/anassaeneroi/yt-offline" # required for AGPL §13
|
||||||
|
|
|
||||||
158
src/app.rs
158
src/app.rs
|
|
@ -1,7 +1,13 @@
|
||||||
|
//! egui desktop application — the main window, sidebar, video grid, and settings UI.
|
||||||
|
//!
|
||||||
|
//! The [`App`] struct holds all UI state and implements [`eframe::App`].
|
||||||
|
//! Background work (downloads, scheduled rescans) runs in threads and
|
||||||
|
//! communicates back via `mpsc` channels stored on `App`.
|
||||||
|
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
use std::sync::mpsc::Receiver;
|
use std::sync::mpsc::{Receiver, Sender};
|
||||||
use std::time::Instant;
|
use std::time::Instant;
|
||||||
|
|
||||||
use eframe::egui;
|
use eframe::egui;
|
||||||
|
|
@ -65,7 +71,9 @@ pub struct App {
|
||||||
show_settings: bool,
|
show_settings: bool,
|
||||||
dl_url: String,
|
dl_url: String,
|
||||||
textures: HashMap<PathBuf, Option<egui::TextureHandle>>,
|
textures: HashMap<PathBuf, Option<egui::TextureHandle>>,
|
||||||
decode_budget: u32,
|
thumb_request_tx: Sender<PathBuf>,
|
||||||
|
thumb_result_rx: Receiver<(PathBuf, Option<egui::ColorImage>)>,
|
||||||
|
thumb_pending: HashSet<PathBuf>,
|
||||||
desc_cache: HashMap<PathBuf, String>,
|
desc_cache: HashMap<PathBuf, String>,
|
||||||
status: String,
|
status: String,
|
||||||
settings_dir: String,
|
settings_dir: String,
|
||||||
|
|
@ -82,6 +90,13 @@ pub struct App {
|
||||||
bulk_selected: HashSet<String>,
|
bulk_selected: HashSet<String>,
|
||||||
// Scheduler
|
// Scheduler
|
||||||
last_scheduled_check: Option<Instant>,
|
last_scheduled_check: Option<Instant>,
|
||||||
|
// Cards cache — recomputed only when inputs change
|
||||||
|
cards_cache: Vec<Card>,
|
||||||
|
cards_cache_key: Option<(String, SortMode, SidebarView, u64)>,
|
||||||
|
library_generation: u64,
|
||||||
|
// Web server
|
||||||
|
web_server_running: bool,
|
||||||
|
web_server_shutdown: Option<Sender<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl App {
|
impl App {
|
||||||
|
|
@ -117,6 +132,20 @@ impl App {
|
||||||
|
|
||||||
let browser = config.player.browser.clone();
|
let browser = config.player.browser.clone();
|
||||||
|
|
||||||
|
let (thumb_request_tx, thumb_request_rx) = std::sync::mpsc::channel::<PathBuf>();
|
||||||
|
let (thumb_result_tx, thumb_result_rx) =
|
||||||
|
std::sync::mpsc::channel::<(PathBuf, Option<egui::ColorImage>)>();
|
||||||
|
let ctx = cc.egui_ctx.clone();
|
||||||
|
std::thread::spawn(move || {
|
||||||
|
while let Ok(path) = thumb_request_rx.recv() {
|
||||||
|
let img = decode_thumbnail_image(&path);
|
||||||
|
if thumb_result_tx.send((path, img)).is_err() {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
ctx.request_repaint();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
config,
|
config,
|
||||||
config_path,
|
config_path,
|
||||||
|
|
@ -130,7 +159,9 @@ impl App {
|
||||||
show_settings: false,
|
show_settings: false,
|
||||||
dl_url: String::new(),
|
dl_url: String::new(),
|
||||||
textures: HashMap::new(),
|
textures: HashMap::new(),
|
||||||
decode_budget: 0,
|
thumb_request_tx,
|
||||||
|
thumb_result_rx,
|
||||||
|
thumb_pending: HashSet::new(),
|
||||||
desc_cache: HashMap::new(),
|
desc_cache: HashMap::new(),
|
||||||
status,
|
status,
|
||||||
settings_dir,
|
settings_dir,
|
||||||
|
|
@ -145,6 +176,11 @@ impl App {
|
||||||
bulk_mode: false,
|
bulk_mode: false,
|
||||||
bulk_selected: HashSet::new(),
|
bulk_selected: HashSet::new(),
|
||||||
last_scheduled_check: None,
|
last_scheduled_check: None,
|
||||||
|
cards_cache: Vec::new(),
|
||||||
|
cards_cache_key: None,
|
||||||
|
library_generation: 0,
|
||||||
|
web_server_running: false,
|
||||||
|
web_server_shutdown: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -161,7 +197,21 @@ impl App {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn cards(&self) -> Vec<Card> {
|
fn cards_take(&mut self) -> Vec<Card> {
|
||||||
|
let key = (
|
||||||
|
self.search.clone(),
|
||||||
|
self.sort_mode.clone(),
|
||||||
|
self.sidebar_view.clone(),
|
||||||
|
self.library_generation,
|
||||||
|
);
|
||||||
|
if self.cards_cache_key.as_ref() != Some(&key) {
|
||||||
|
self.cards_cache = self.compute_cards();
|
||||||
|
self.cards_cache_key = Some(key);
|
||||||
|
}
|
||||||
|
std::mem::take(&mut self.cards_cache)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_cards(&self) -> Vec<Card> {
|
||||||
let query = self.search.trim().to_lowercase();
|
let query = self.search.trim().to_lowercase();
|
||||||
|
|
||||||
let mut cards = Vec::new();
|
let mut cards = Vec::new();
|
||||||
|
|
@ -265,18 +315,15 @@ impl App {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
||||||
fn texture(&mut self, ctx: &egui::Context, path: &Path) -> Option<egui::TextureHandle> {
|
fn texture(&mut self, _ctx: &egui::Context, path: &Path) -> Option<egui::TextureHandle> {
|
||||||
if let Some(slot) = self.textures.get(path) {
|
if let Some(slot) = self.textures.get(path) {
|
||||||
return slot.clone();
|
return slot.clone();
|
||||||
}
|
}
|
||||||
if self.decode_budget == 0 {
|
let pb = path.to_path_buf();
|
||||||
ctx.request_repaint();
|
if self.thumb_pending.insert(pb.clone()) {
|
||||||
return None;
|
let _ = self.thumb_request_tx.send(pb);
|
||||||
}
|
}
|
||||||
self.decode_budget -= 1;
|
None
|
||||||
let handle = decode_thumbnail(ctx, path);
|
|
||||||
self.textures.insert(path.to_path_buf(), handle.clone());
|
|
||||||
handle
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn description(&mut self, video: &Video) -> String {
|
fn description(&mut self, video: &Video) -> String {
|
||||||
|
|
@ -365,6 +412,28 @@ impl App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn start_web_server(&mut self) {
|
||||||
|
if self.web_server_running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let shutdown = crate::web::run_with_shutdown(self.config.clone());
|
||||||
|
self.web_server_shutdown = Some(shutdown);
|
||||||
|
self.web_server_running = true;
|
||||||
|
let port = self.config.web.port;
|
||||||
|
self.status = format!("Web server started on port {port}. Access at http://localhost:{port}");
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stop_web_server(&mut self) {
|
||||||
|
if !self.web_server_running {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(shutdown) = self.web_server_shutdown.take() {
|
||||||
|
let _ = shutdown.send(());
|
||||||
|
}
|
||||||
|
self.web_server_running = false;
|
||||||
|
self.status = "Web server stopped.".to_string();
|
||||||
|
}
|
||||||
|
|
||||||
fn bulk_mark_watched(&mut self, watched: bool) {
|
fn bulk_mark_watched(&mut self, watched: bool) {
|
||||||
let ids: Vec<String> = self.bulk_selected.iter().cloned().collect();
|
let ids: Vec<String> = self.bulk_selected.iter().cloned().collect();
|
||||||
for id in &ids {
|
for id in &ids {
|
||||||
|
|
@ -429,10 +498,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn channel_total_size(ch: &library::Channel) -> u64 {
|
fn channel_total_size(ch: &library::Channel) -> u64 {
|
||||||
ch.videos.iter()
|
ch.total_size_cached
|
||||||
.chain(ch.playlists.iter().flat_map(|p| p.videos.iter()))
|
|
||||||
.filter_map(|v| v.file_size)
|
|
||||||
.sum()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn top_bar(&mut self, ctx: &egui::Context) {
|
fn top_bar(&mut self, ctx: &egui::Context) {
|
||||||
|
|
@ -521,14 +587,24 @@ impl App {
|
||||||
let mut pending_ch_download: Option<(String, String)> = None; // (url, channel_name)
|
let mut pending_ch_download: Option<(String, String)> = None; // (url, channel_name)
|
||||||
|
|
||||||
for i in 0..self.library.len() {
|
for i in 0..self.library.len() {
|
||||||
let (name, total, has_playlists, size_bytes, channel_url) = {
|
let (name, total, has_playlists, size_bytes, channel_url, url_inferred) = {
|
||||||
let ch = &self.library[i];
|
let ch = &self.library[i];
|
||||||
|
let meta_url = ch.meta.as_ref().and_then(|m| m.channel_url.clone());
|
||||||
|
let (url, inferred) = if let Some(u) = meta_url {
|
||||||
|
(Some(u), false)
|
||||||
|
} else {
|
||||||
|
let folder_url = ch.path.file_name()
|
||||||
|
.and_then(|n| n.to_str())
|
||||||
|
.map(|n| format!("https://www.youtube.com/@{n}"));
|
||||||
|
(folder_url, true)
|
||||||
|
};
|
||||||
(
|
(
|
||||||
ch.name.clone(),
|
ch.name.clone(),
|
||||||
ch.total_videos(),
|
ch.total_videos(),
|
||||||
!ch.playlists.is_empty(),
|
!ch.playlists.is_empty(),
|
||||||
Self::channel_total_size(ch),
|
Self::channel_total_size(ch),
|
||||||
ch.meta.as_ref().and_then(|m| m.channel_url.clone()),
|
url,
|
||||||
|
inferred,
|
||||||
)
|
)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -555,13 +631,14 @@ impl App {
|
||||||
let name_for_menu = name.clone();
|
let name_for_menu = name.clone();
|
||||||
resp.context_menu(|ui| {
|
resp.context_menu(|ui| {
|
||||||
if let Some(ref url) = url_for_menu {
|
if let Some(ref url) = url_for_menu {
|
||||||
if ui.button("⬇ Check for new videos").clicked() {
|
let mut btn = ui.button("⬇ Check for new videos");
|
||||||
|
if url_inferred {
|
||||||
|
btn = btn.on_hover_text(format!("URL inferred from folder name:\n{url}"));
|
||||||
|
}
|
||||||
|
if btn.clicked() {
|
||||||
pending_ch_download = Some((url.clone(), name_for_menu.clone()));
|
pending_ch_download = Some((url.clone(), name_for_menu.clone()));
|
||||||
ui.close_menu();
|
ui.close_menu();
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
ui.add_enabled(false, egui::Button::new("⬇ Check for new videos"))
|
|
||||||
.on_hover_text("Download this channel first to store its URL");
|
|
||||||
}
|
}
|
||||||
if ui.button("📁 Open folder").clicked() {
|
if ui.button("📁 Open folder").clicked() {
|
||||||
let path = self.library[i].path.clone();
|
let path = self.library[i].path.clone();
|
||||||
|
|
@ -786,6 +863,22 @@ impl App {
|
||||||
.range(1024..=65535),
|
.range(1024..=65535),
|
||||||
);
|
);
|
||||||
ui.end_row();
|
ui.end_row();
|
||||||
|
|
||||||
|
ui.label("Web server:");
|
||||||
|
ui.horizontal(|ui| {
|
||||||
|
if self.web_server_running {
|
||||||
|
if ui.button("🛑 Stop").clicked() {
|
||||||
|
self.stop_web_server();
|
||||||
|
}
|
||||||
|
ui.label(egui::RichText::new("Running").small().weak());
|
||||||
|
} else {
|
||||||
|
if ui.button("▶ Start").clicked() {
|
||||||
|
self.start_web_server();
|
||||||
|
}
|
||||||
|
ui.label(egui::RichText::new("Stopped").small().weak());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
ui.end_row();
|
||||||
});
|
});
|
||||||
|
|
||||||
ui.add_space(8.0);
|
ui.add_space(8.0);
|
||||||
|
|
@ -911,7 +1004,7 @@ impl App {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn video_list(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
fn video_list(&mut self, ctx: &egui::Context, ui: &mut egui::Ui) {
|
||||||
let cards = self.cards();
|
let cards = self.cards_take();
|
||||||
let show_channel = !matches!(self.sidebar_view, SidebarView::Channel(_) | SidebarView::Playlist(_, _));
|
let show_channel = !matches!(self.sidebar_view, SidebarView::Channel(_) | SidebarView::Playlist(_, _));
|
||||||
|
|
||||||
// Channel metadata banner
|
// Channel metadata banner
|
||||||
|
|
@ -1181,6 +1274,7 @@ impl App {
|
||||||
ui.separator();
|
ui.separator();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
self.cards_cache = cards;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1221,7 +1315,18 @@ impl eframe::App for App {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.decode_budget = 6;
|
// Drain any decoded thumbnails from the worker thread
|
||||||
|
while let Ok((path, img)) = self.thumb_result_rx.try_recv() {
|
||||||
|
self.thumb_pending.remove(&path);
|
||||||
|
let handle = img.map(|color_image| {
|
||||||
|
ctx.load_texture(
|
||||||
|
path.to_string_lossy(),
|
||||||
|
color_image,
|
||||||
|
egui::TextureOptions::LINEAR,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
self.textures.insert(path, handle);
|
||||||
|
}
|
||||||
|
|
||||||
self.top_bar(ctx);
|
self.top_bar(ctx);
|
||||||
self.channel_panel(ctx);
|
self.channel_panel(ctx);
|
||||||
|
|
@ -1286,13 +1391,12 @@ fn spawn_mpv_tracker(sock_path: String, video_id: String, tx: std::sync::mpsc::S
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn decode_thumbnail(ctx: &egui::Context, path: &Path) -> Option<egui::TextureHandle> {
|
fn decode_thumbnail_image(path: &Path) -> Option<egui::ColorImage> {
|
||||||
let image = image::open(path).ok()?;
|
let image = image::open(path).ok()?;
|
||||||
let image = image.thumbnail(384, 216);
|
let image = image.thumbnail(384, 216);
|
||||||
let rgba = image.to_rgba8();
|
let rgba = image.to_rgba8();
|
||||||
let (w, h) = (rgba.width() as usize, rgba.height() as usize);
|
let (w, h) = (rgba.width() as usize, rgba.height() as usize);
|
||||||
let color_image = egui::ColorImage::from_rgba_unmultiplied([w, h], rgba.as_raw());
|
Some(egui::ColorImage::from_rgba_unmultiplied([w, h], rgba.as_raw()))
|
||||||
Some(ctx.load_texture(path.to_string_lossy(), color_image, egui::TextureOptions::LINEAR))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn file_label(path: &Path) -> String {
|
fn file_label(path: &Path) -> String {
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,12 @@
|
||||||
|
//! Application configuration loaded from `config.toml`.
|
||||||
|
//!
|
||||||
|
//! Each top-level section maps to a TOML table. Missing sections get sane
|
||||||
|
//! defaults so existing config files continue to work after upgrades.
|
||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
|
/// Root configuration object, serialised from/to `config.toml`.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
pub backup: BackupSection,
|
pub backup: BackupSection,
|
||||||
|
|
@ -14,11 +20,13 @@ pub struct Config {
|
||||||
pub web: WebSection,
|
pub web: WebSection,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `[backup]` table — where to store downloaded videos.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct BackupSection {
|
pub struct BackupSection {
|
||||||
pub directory: PathBuf,
|
pub directory: PathBuf,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `[player]` table — external player and browser cookie source.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct PlayerSection {
|
pub struct PlayerSection {
|
||||||
#[serde(default = "default_player")]
|
#[serde(default = "default_player")]
|
||||||
|
|
@ -33,6 +41,7 @@ impl Default for PlayerSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `[ui]` table — egui desktop theme.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct UiSection {
|
pub struct UiSection {
|
||||||
#[serde(default = "default_theme")]
|
#[serde(default = "default_theme")]
|
||||||
|
|
@ -45,6 +54,7 @@ impl Default for UiSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `[scheduler]` table — periodic background download schedule.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct SchedulerSection {
|
pub struct SchedulerSection {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
|
@ -59,15 +69,33 @@ impl Default for SchedulerSection {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `[web]` table — built-in HTTP server settings.
|
||||||
|
///
|
||||||
|
/// `source_url` is **required for AGPL §13 compliance**: set it to a URL
|
||||||
|
/// where the running source code can be obtained (e.g. your Codeberg repo).
|
||||||
|
/// It is shown as a "Source" link in the web UI footer.
|
||||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||||
pub struct WebSection {
|
pub struct WebSection {
|
||||||
#[serde(default = "default_web_port")]
|
#[serde(default = "default_web_port")]
|
||||||
pub port: u16,
|
pub port: u16,
|
||||||
|
#[serde(default = "default_web_bind")]
|
||||||
|
/// Address to bind the HTTP server to. Defaults to `127.0.0.1` (localhost only).
|
||||||
|
/// Set to `0.0.0.0` to accept connections from any interface (not recommended).
|
||||||
|
pub bind: String,
|
||||||
|
#[serde(default)]
|
||||||
|
pub transcode: bool,
|
||||||
|
/// Public URL to the source repository, shown in the web UI per AGPL §13.
|
||||||
|
pub source_url: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for WebSection {
|
impl Default for WebSection {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self { port: default_web_port() }
|
Self {
|
||||||
|
port: default_web_port(),
|
||||||
|
bind: default_web_bind(),
|
||||||
|
transcode: false,
|
||||||
|
source_url: None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -76,19 +104,23 @@ fn default_browser() -> String { "firefox".to_string() }
|
||||||
fn default_theme() -> String { "dark".to_string() }
|
fn default_theme() -> String { "dark".to_string() }
|
||||||
fn default_interval_hours() -> u32 { 24 }
|
fn default_interval_hours() -> u32 { 24 }
|
||||||
fn default_web_port() -> u16 { 8080 }
|
fn default_web_port() -> u16 { 8080 }
|
||||||
|
fn default_web_bind() -> String { "127.0.0.1".to_string() }
|
||||||
|
|
||||||
impl Config {
|
impl Config {
|
||||||
|
/// Load and parse `config.toml` from `path`.
|
||||||
pub fn load(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
|
pub fn load(path: &Path) -> Result<Self, Box<dyn std::error::Error>> {
|
||||||
let contents = std::fs::read_to_string(path)?;
|
let contents = std::fs::read_to_string(path)?;
|
||||||
toml::from_str(&contents).map_err(|e| e.into())
|
toml::from_str(&contents).map_err(|e| e.into())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Serialise the config back to `path` in pretty TOML.
|
||||||
pub fn save(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
pub fn save(&self, path: &Path) -> Result<(), Box<dyn std::error::Error>> {
|
||||||
let contents = toml::to_string_pretty(self)?;
|
let contents = toml::to_string_pretty(self)?;
|
||||||
std::fs::write(path, contents)?;
|
std::fs::write(path, contents)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Construct a minimal default config pointing `backup.directory` at `dir`.
|
||||||
pub fn default_with_dir(dir: PathBuf) -> Self {
|
pub fn default_with_dir(dir: PathBuf) -> Self {
|
||||||
Self {
|
Self {
|
||||||
backup: BackupSection { directory: dir },
|
backup: BackupSection { directory: dir },
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,25 @@
|
||||||
|
//! Persistent storage for watched status and playback positions.
|
||||||
|
//!
|
||||||
|
//! Uses a bundled SQLite database (`yt-offline.db` by default).
|
||||||
|
//!
|
||||||
|
//! # Schema
|
||||||
|
//!
|
||||||
|
//! | Table | Columns | Purpose |
|
||||||
|
//! |---|---|---|
|
||||||
|
//! | `watched` | `video_id` (PK), `watched_at` | Records videos the user has marked watched |
|
||||||
|
//! | `positions` | `video_id` (PK), `position_secs`, `updated_at` | Stores resume positions |
|
||||||
|
|
||||||
use rusqlite::{Connection, Result};
|
use rusqlite::{Connection, Result};
|
||||||
use std::collections::{HashMap, HashSet};
|
use std::collections::{HashMap, HashSet};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
/// Thin wrapper around a SQLite connection with schema management.
|
||||||
pub struct Database {
|
pub struct Database {
|
||||||
conn: Connection,
|
conn: Connection,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Database {
|
impl Database {
|
||||||
|
/// Open or create the database at `path`, running schema migrations.
|
||||||
pub fn open(path: &Path) -> Result<Self> {
|
pub fn open(path: &Path) -> Result<Self> {
|
||||||
let conn = Connection::open(path)?;
|
let conn = Connection::open(path)?;
|
||||||
let db = Database { conn };
|
let db = Database { conn };
|
||||||
|
|
@ -14,6 +27,7 @@ impl Database {
|
||||||
Ok(db)
|
Ok(db)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Open an in-memory database — used in tests.
|
||||||
pub fn open_in_memory() -> Result<Self> {
|
pub fn open_in_memory() -> Result<Self> {
|
||||||
let conn = Connection::open_in_memory()?;
|
let conn = Connection::open_in_memory()?;
|
||||||
let db = Database { conn };
|
let db = Database { conn };
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,27 @@
|
||||||
//! Running `yt-dlp` in the background and surfacing its progress to the UI.
|
//! Running `yt-dlp` in the background and surfacing its progress to the UI.
|
||||||
|
//!
|
||||||
|
//! Each call to [`Downloader::start`] spawns a thread that runs `yt-dlp` and
|
||||||
|
//! pipes stdout/stderr back to the main thread through an `mpsc` channel.
|
||||||
|
//! The caller polls for updates via [`Downloader::poll`], which drains the
|
||||||
|
//! channel into each [`Job`]'s log buffer.
|
||||||
|
//!
|
||||||
|
//! # yt-dlp command flags used
|
||||||
|
//!
|
||||||
|
//! | Flag | Purpose |
|
||||||
|
//! |---|---|
|
||||||
|
//! | `--cookies cookies.txt` | Pass browser cookies for age-gated/member videos |
|
||||||
|
//! | `--write-subs --write-auto-subs` | Download subtitles alongside the video |
|
||||||
|
//! | `--write-thumbnail` | Download channel/video thumbnails |
|
||||||
|
//! | `--write-description` | Save video description as a sidecar `.description` file |
|
||||||
|
//! | `--write-info-json` | Save full metadata as a `.info.json` sidecar |
|
||||||
|
//! | `--remux-video mkv` | Re-container to MKV (no re-encode) |
|
||||||
|
//! | `--embed-metadata --embed-info-json --embed-chapters` | Embed rich metadata into the MKV |
|
||||||
|
//! | `--xattrs` | Store metadata in filesystem extended attributes |
|
||||||
|
//! | `--sponsorblock-mark all` | Mark (but don't remove) SponsorBlock segments |
|
||||||
|
//! | `--extractor-args youtube:player_client=web` | Use the web player API to avoid throttling |
|
||||||
|
//! | `--impersonate Chrome-146:Macos-26` | Impersonate a real browser for bot detection |
|
||||||
|
//! | `--break-on-existing` | Stop when the archive file records the video as already downloaded |
|
||||||
|
//! | `--download-archive archive.txt` | Record downloaded IDs to avoid re-downloading |
|
||||||
|
|
||||||
use std::io::{BufRead, BufReader};
|
use std::io::{BufRead, BufReader};
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
@ -6,13 +29,17 @@ use std::process::{Command, Stdio};
|
||||||
use std::sync::mpsc::{channel, Receiver};
|
use std::sync::mpsc::{channel, Receiver};
|
||||||
use std::thread;
|
use std::thread;
|
||||||
|
|
||||||
|
/// Describes the kind of YouTube URL being downloaded, which determines the
|
||||||
|
/// output path template passed to yt-dlp.
|
||||||
pub enum UrlKind {
|
pub enum UrlKind {
|
||||||
|
/// A channel URL (`/@handle`, `/channel/ID`, or `/c/name`).
|
||||||
Channel { handle: String },
|
Channel { handle: String },
|
||||||
Playlist,
|
Playlist,
|
||||||
Video,
|
Video,
|
||||||
Unknown,
|
Unknown,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Classify a YouTube URL into a [`UrlKind`] by inspecting its path.
|
||||||
pub fn detect_url_kind(url: &str) -> UrlKind {
|
pub fn detect_url_kind(url: &str) -> UrlKind {
|
||||||
if url.contains("playlist?list=") {
|
if url.contains("playlist?list=") {
|
||||||
return UrlKind::Playlist;
|
return UrlKind::Playlist;
|
||||||
|
|
@ -39,6 +66,7 @@ fn extract_after<'a>(url: &'a str, marker: &str) -> Option<&'a str> {
|
||||||
if end == 0 { None } else { Some(&rest[..end]) }
|
if end == 0 { None } else { Some(&rest[..end]) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Lifecycle state of a download job.
|
||||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum JobState {
|
pub enum JobState {
|
||||||
Running,
|
Running,
|
||||||
|
|
@ -46,17 +74,22 @@ pub enum JobState {
|
||||||
Failed,
|
Failed,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Internal message sent from the yt-dlp thread to the job.
|
||||||
enum Msg {
|
enum Msg {
|
||||||
Line(String),
|
Line(String),
|
||||||
Progress(f32),
|
Progress(f32),
|
||||||
Finished(bool),
|
Finished(bool),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A single yt-dlp invocation tracked by the downloader.
|
||||||
pub struct Job {
|
pub struct Job {
|
||||||
pub url: String,
|
pub url: String,
|
||||||
|
/// Short human-readable path shown in the UI (e.g. `channels/handle/`).
|
||||||
pub label: String,
|
pub label: String,
|
||||||
pub state: JobState,
|
pub state: JobState,
|
||||||
|
/// Download progress as a fraction in `[0.0, 1.0]`.
|
||||||
pub progress: f32,
|
pub progress: f32,
|
||||||
|
/// Rolling log buffer — capped at 800 lines to avoid unbounded growth.
|
||||||
pub log: Vec<String>,
|
pub log: Vec<String>,
|
||||||
rx: Receiver<Msg>,
|
rx: Receiver<Msg>,
|
||||||
}
|
}
|
||||||
|
|
@ -81,9 +114,12 @@ impl Job {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Manages all active and recently completed yt-dlp download jobs.
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
pub jobs: Vec<Job>,
|
pub jobs: Vec<Job>,
|
||||||
pub channels_root: PathBuf,
|
pub channels_root: PathBuf,
|
||||||
|
/// Browser name passed to `--cookies-from-browser` (unused) — cookie file
|
||||||
|
/// is currently always `cookies.txt`.
|
||||||
pub browser: String,
|
pub browser: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -92,10 +128,13 @@ impl Downloader {
|
||||||
Self { jobs: Vec::new(), channels_root, browser }
|
Self { jobs: Vec::new(), channels_root, browser }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn a yt-dlp process for `url` and track it as a new [`Job`].
|
||||||
|
///
|
||||||
|
/// The output path template is derived from `kind` so that channels,
|
||||||
|
/// playlists, and individual videos land in the right sub-directories.
|
||||||
pub fn start(&mut self, url: String, kind: &UrlKind) {
|
pub fn start(&mut self, url: String, kind: &UrlKind) {
|
||||||
let (tx, rx) = channel();
|
let (tx, rx) = channel();
|
||||||
let archive_path = self.channels_root.join("archive.txt");
|
let archive_path = self.channels_root.join("archive.txt");
|
||||||
let browser = self.browser.clone();
|
|
||||||
|
|
||||||
let (out_arg, label) = match kind {
|
let (out_arg, label) = match kind {
|
||||||
UrlKind::Channel { handle } => {
|
UrlKind::Channel { handle } => {
|
||||||
|
|
@ -121,25 +160,32 @@ impl Downloader {
|
||||||
let mut cmd = Command::new("yt-dlp");
|
let mut cmd = Command::new("yt-dlp");
|
||||||
cmd.arg("--newline")
|
cmd.arg("--newline")
|
||||||
.arg("--no-color")
|
.arg("--no-color")
|
||||||
|
.arg("--cookies")
|
||||||
|
.arg("cookies.txt")
|
||||||
.arg("--write-subs")
|
.arg("--write-subs")
|
||||||
|
.arg("--write-auto-subs")
|
||||||
.arg("--write-thumbnail")
|
.arg("--write-thumbnail")
|
||||||
.arg("--write-description")
|
.arg("--write-description")
|
||||||
.arg("--write-info-json")
|
.arg("--write-info-json")
|
||||||
.arg("-f")
|
.arg("--remux-video")
|
||||||
.arg("mkv")
|
.arg("mkv")
|
||||||
.arg("--embed-metadata")
|
.arg("--embed-metadata")
|
||||||
|
.arg("--embed-info-json")
|
||||||
|
.arg("--embed-chapters")
|
||||||
|
.arg("--xattrs")
|
||||||
|
.arg("--sponsorblock-mark")
|
||||||
|
.arg("all")
|
||||||
|
.arg("--extractor-args")
|
||||||
|
.arg("youtube:player_client=web")
|
||||||
|
.arg("--progress")
|
||||||
.arg("--break-on-existing")
|
.arg("--break-on-existing")
|
||||||
.arg("--download-archive")
|
.arg("--download-archive")
|
||||||
.arg(archive_path.display().to_string())
|
.arg(archive_path.display().to_string())
|
||||||
.arg("--ignore-errors")
|
.arg("--impersonate")
|
||||||
|
.arg("Chrome-146:Macos-26")
|
||||||
.arg("-o")
|
.arg("-o")
|
||||||
.arg(&out_arg);
|
.arg(&out_arg)
|
||||||
|
.arg(&url_for_thread)
|
||||||
if !browser.is_empty() && browser != "none" {
|
|
||||||
cmd.arg("--cookies-from-browser").arg(&browser);
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd.arg(&url_for_thread)
|
|
||||||
.stdin(Stdio::null())
|
.stdin(Stdio::null())
|
||||||
.stdout(Stdio::piped())
|
.stdout(Stdio::piped())
|
||||||
.stderr(Stdio::piped());
|
.stderr(Stdio::piped());
|
||||||
|
|
@ -178,6 +224,9 @@ impl Downloader {
|
||||||
self.jobs.push(Job { url, label, state: JobState::Running, progress: 0.0, log: Vec::new(), rx });
|
self.jobs.push(Job { url, label, state: JobState::Running, progress: 0.0, log: Vec::new(), rx });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Drain pending messages from all job threads into their log buffers.
|
||||||
|
///
|
||||||
|
/// Call this regularly from the UI event loop to pick up progress updates.
|
||||||
pub fn poll(&mut self) {
|
pub fn poll(&mut self) {
|
||||||
for job in &mut self.jobs {
|
for job in &mut self.jobs {
|
||||||
job.drain();
|
job.drain();
|
||||||
|
|
@ -187,8 +236,24 @@ impl Downloader {
|
||||||
pub fn any_running(&self) -> bool {
|
pub fn any_running(&self) -> bool {
|
||||||
self.jobs.iter().any(|j| j.state == JobState::Running)
|
self.jobs.iter().any(|j| j.state == JobState::Running)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove all jobs that have finished (done or failed), keeping only running ones.
|
||||||
|
pub fn clear_finished(&mut self) {
|
||||||
|
self.jobs.retain(|j| j.state == JobState::Running);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Remove a single finished job by index. Silently ignores the request
|
||||||
|
/// if the job is still running.
|
||||||
|
pub fn remove_job(&mut self, idx: usize) {
|
||||||
|
if let Some(j) = self.jobs.get(idx) {
|
||||||
|
if j.state != JobState::Running {
|
||||||
|
self.jobs.remove(idx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Parse a yt-dlp `[download] 42.7% …` line into a `[0.0, 1.0]` fraction.
|
||||||
fn parse_progress(line: &str) -> Option<f32> {
|
fn parse_progress(line: &str) -> Option<f32> {
|
||||||
let rest = line.trim_start().strip_prefix("[download]")?.trim_start();
|
let rest = line.trim_start().strip_prefix("[download]")?.trim_start();
|
||||||
let pct_end = rest.find('%')?;
|
let pct_end = rest.find('%')?;
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,23 @@
|
||||||
//! Scanning the `channels/` directory tree into channels, playlists, and videos.
|
//! Scanning the `channels/` directory tree into channels, playlists, and videos.
|
||||||
|
//!
|
||||||
|
//! # Directory layout expected
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! channels/
|
||||||
|
//! <channel-name>/
|
||||||
|
//! Title [VIDEO_ID].mkv
|
||||||
|
//! Title [VIDEO_ID].webp ← thumbnail
|
||||||
|
//! Title [VIDEO_ID].description
|
||||||
|
//! Title [VIDEO_ID].info.json
|
||||||
|
//! Title [VIDEO_ID].en.vtt ← subtitle (lang = "en")
|
||||||
|
//! <playlist-name>/
|
||||||
|
//! Title [VIDEO_ID].mkv
|
||||||
|
//! …
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! Files that don't match the `Title [ID].ext` naming convention are silently
|
||||||
|
//! ignored. Hidden directories (name starts with `.`) and directories that
|
||||||
|
//! contain no recognisable video files are skipped.
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
@ -6,8 +25,18 @@ use std::path::{Path, PathBuf};
|
||||||
const VIDEO_EXTS: &[&str] = &["mkv", "mp4", "webm", "m4v", "mov", "avi"];
|
const VIDEO_EXTS: &[&str] = &["mkv", "mp4", "webm", "m4v", "mov", "avi"];
|
||||||
const THUMB_EXTS: &[&str] = &["webp", "jpg", "jpeg", "png"];
|
const THUMB_EXTS: &[&str] = &["webp", "jpg", "jpeg", "png"];
|
||||||
|
|
||||||
|
/// A single WebVTT subtitle track discovered alongside a video file.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct Subtitle {
|
||||||
|
/// ISO 639-1/2 language code extracted from the `.lang.vtt` filename suffix.
|
||||||
|
pub lang: String,
|
||||||
|
pub path: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A fully enriched video entry, ready to serve to the UI.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Video {
|
pub struct Video {
|
||||||
|
/// yt-dlp video ID (the part inside `[…]` in the filename).
|
||||||
pub id: String,
|
pub id: String,
|
||||||
pub title: String,
|
pub title: String,
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
|
|
@ -15,11 +44,17 @@ pub struct Video {
|
||||||
pub video_path: Option<PathBuf>,
|
pub video_path: Option<PathBuf>,
|
||||||
pub thumb_path: Option<PathBuf>,
|
pub thumb_path: Option<PathBuf>,
|
||||||
pub description_path: Option<PathBuf>,
|
pub description_path: Option<PathBuf>,
|
||||||
|
/// Path to the `.info.json` sidecar — used to read duration, chapters, etc.
|
||||||
|
pub info_path: Option<PathBuf>,
|
||||||
|
pub subtitles: Vec<Subtitle>,
|
||||||
pub has_live_chat: bool,
|
pub has_live_chat: bool,
|
||||||
|
/// Duration read from `info.json`; `None` if the sidecar is missing.
|
||||||
pub duration_secs: Option<f64>,
|
pub duration_secs: Option<f64>,
|
||||||
|
/// Size of the video file on disk; `None` if the video file is missing.
|
||||||
pub file_size: Option<u64>,
|
pub file_size: Option<u64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A sub-directory inside a channel that contains videos (treated as a playlist).
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Playlist {
|
pub struct Playlist {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
|
|
@ -28,6 +63,7 @@ pub struct Playlist {
|
||||||
pub videos: Vec<Video>,
|
pub videos: Vec<Video>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Channel-level metadata pulled from the first available `info.json`.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct ChannelMeta {
|
pub struct ChannelMeta {
|
||||||
pub subscriber_count: Option<u64>,
|
pub subscriber_count: Option<u64>,
|
||||||
|
|
@ -35,21 +71,32 @@ pub struct ChannelMeta {
|
||||||
pub uploader: Option<String>,
|
pub uploader: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// A top-level channel directory with all its videos and playlists.
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Channel {
|
pub struct Channel {
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub path: PathBuf,
|
pub path: PathBuf,
|
||||||
|
/// Videos stored directly inside the channel directory (not in a sub-folder).
|
||||||
pub videos: Vec<Video>,
|
pub videos: Vec<Video>,
|
||||||
|
/// Sub-directories that contain at least one video.
|
||||||
pub playlists: Vec<Playlist>,
|
pub playlists: Vec<Playlist>,
|
||||||
pub meta: Option<ChannelMeta>,
|
pub meta: Option<ChannelMeta>,
|
||||||
|
/// Cached sum of `videos.len() + playlists[*].videos.len()`.
|
||||||
|
pub total_videos_cached: usize,
|
||||||
|
/// Cached sum of all video file sizes.
|
||||||
|
pub total_size_cached: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Channel {
|
impl Channel {
|
||||||
pub fn total_videos(&self) -> usize {
|
pub fn total_videos(&self) -> usize {
|
||||||
self.videos.len() + self.playlists.iter().map(|p| p.videos.len()).sum::<usize>()
|
self.total_videos_cached
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan `root` for channel directories and return them sorted alphabetically.
|
||||||
|
///
|
||||||
|
/// Skips hidden directories (names starting with `.`) and directories that
|
||||||
|
/// contain no recognisable video files.
|
||||||
pub fn scan_channels(root: &Path) -> Vec<Channel> {
|
pub fn scan_channels(root: &Path) -> Vec<Channel> {
|
||||||
let mut channels = Vec::new();
|
let mut channels = Vec::new();
|
||||||
let Ok(entries) = std::fs::read_dir(root) else { return channels };
|
let Ok(entries) = std::fs::read_dir(root) else { return channels };
|
||||||
|
|
@ -57,9 +104,26 @@ pub fn scan_channels(root: &Path) -> Vec<Channel> {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !path.is_dir() { continue; }
|
if !path.is_dir() { continue; }
|
||||||
let name = entry.file_name().to_string_lossy().into_owned();
|
let name = entry.file_name().to_string_lossy().into_owned();
|
||||||
|
if name.starts_with('.') { continue; }
|
||||||
let (videos, playlists) = scan_channel_dir(&path);
|
let (videos, playlists) = scan_channel_dir(&path);
|
||||||
|
if videos.is_empty() && playlists.is_empty() { continue; }
|
||||||
let meta = load_channel_meta(&videos);
|
let meta = load_channel_meta(&videos);
|
||||||
channels.push(Channel { name, path, videos, playlists, meta });
|
let total_videos_cached =
|
||||||
|
videos.len() + playlists.iter().map(|p| p.videos.len()).sum::<usize>();
|
||||||
|
let total_size_cached = videos
|
||||||
|
.iter()
|
||||||
|
.chain(playlists.iter().flat_map(|p| p.videos.iter()))
|
||||||
|
.filter_map(|v| v.file_size)
|
||||||
|
.sum();
|
||||||
|
channels.push(Channel {
|
||||||
|
name,
|
||||||
|
path,
|
||||||
|
videos,
|
||||||
|
playlists,
|
||||||
|
meta,
|
||||||
|
total_videos_cached,
|
||||||
|
total_size_cached,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
channels.sort_by_key(|c| c.name.to_lowercase());
|
channels.sort_by_key(|c| c.name.to_lowercase());
|
||||||
channels
|
channels
|
||||||
|
|
@ -134,15 +198,28 @@ struct RawVideo {
|
||||||
thumb_path: Option<PathBuf>,
|
thumb_path: Option<PathBuf>,
|
||||||
description_path: Option<PathBuf>,
|
description_path: Option<PathBuf>,
|
||||||
info_path: Option<PathBuf>,
|
info_path: Option<PathBuf>,
|
||||||
|
subtitles: Vec<Subtitle>,
|
||||||
has_live_chat: bool,
|
has_live_chat: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_raw_videos(entries: impl Iterator<Item = std::fs::DirEntry>) -> Vec<RawVideo> {
|
fn collect_raw_videos(entries: impl Iterator<Item = std::fs::DirEntry>) -> Vec<RawVideo> {
|
||||||
let mut by_stem: BTreeMap<String, RawVideo> = BTreeMap::new();
|
let mut by_stem: BTreeMap<String, RawVideo> = BTreeMap::new();
|
||||||
|
let mut pending_subs: Vec<(String, String, PathBuf)> = Vec::new();
|
||||||
for entry in entries {
|
for entry in entries {
|
||||||
let path = entry.path();
|
let path = entry.path();
|
||||||
if !path.is_file() { continue; }
|
if !path.is_file() { continue; }
|
||||||
let file_name = entry.file_name().to_string_lossy().into_owned();
|
let file_name = entry.file_name().to_string_lossy().into_owned();
|
||||||
|
|
||||||
|
// Subtitles have stems like "Title [id].en.vtt" — strip the .vtt and trailing .lang
|
||||||
|
if let Some(sub_stem) = file_name.strip_suffix(".vtt") {
|
||||||
|
if let Some(dot) = sub_stem.rfind('.') {
|
||||||
|
let lang = sub_stem[dot + 1..].to_string();
|
||||||
|
let video_stem = sub_stem[..dot].to_string();
|
||||||
|
pending_subs.push((video_stem, lang, path));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let Some((stem, kind)) = classify(&file_name) else { continue };
|
let Some((stem, kind)) = classify(&file_name) else { continue };
|
||||||
let Some((title, id)) = parse_stem(stem) else { continue };
|
let Some((title, id)) = parse_stem(stem) else { continue };
|
||||||
let raw = by_stem.entry(stem.to_string()).or_insert_with(|| RawVideo {
|
let raw = by_stem.entry(stem.to_string()).or_insert_with(|| RawVideo {
|
||||||
|
|
@ -153,6 +230,7 @@ fn collect_raw_videos(entries: impl Iterator<Item = std::fs::DirEntry>) -> Vec<R
|
||||||
thumb_path: None,
|
thumb_path: None,
|
||||||
description_path: None,
|
description_path: None,
|
||||||
info_path: None,
|
info_path: None,
|
||||||
|
subtitles: Vec::new(),
|
||||||
has_live_chat: false,
|
has_live_chat: false,
|
||||||
});
|
});
|
||||||
match kind {
|
match kind {
|
||||||
|
|
@ -164,6 +242,14 @@ fn collect_raw_videos(entries: impl Iterator<Item = std::fs::DirEntry>) -> Vec<R
|
||||||
FileKind::Other => {}
|
FileKind::Other => {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
for (video_stem, lang, path) in pending_subs {
|
||||||
|
if let Some(raw) = by_stem.get_mut(&video_stem) {
|
||||||
|
raw.subtitles.push(Subtitle { lang, path });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for raw in by_stem.values_mut() {
|
||||||
|
raw.subtitles.sort_by(|a, b| a.lang.cmp(&b.lang));
|
||||||
|
}
|
||||||
by_stem.into_values().collect()
|
by_stem.into_values().collect()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -184,6 +270,8 @@ fn enrich(raws: Vec<RawVideo>) -> Vec<Video> {
|
||||||
video_path: raw.video_path,
|
video_path: raw.video_path,
|
||||||
thumb_path: raw.thumb_path,
|
thumb_path: raw.thumb_path,
|
||||||
description_path: raw.description_path,
|
description_path: raw.description_path,
|
||||||
|
info_path: raw.info_path,
|
||||||
|
subtitles: raw.subtitles,
|
||||||
has_live_chat: raw.has_live_chat,
|
has_live_chat: raw.has_live_chat,
|
||||||
duration_secs,
|
duration_secs,
|
||||||
file_size,
|
file_size,
|
||||||
|
|
@ -193,6 +281,9 @@ fn enrich(raws: Vec<RawVideo>) -> Vec<Video> {
|
||||||
videos
|
videos
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Scan a single flat directory for video files and return enriched `Video` entries.
|
||||||
|
///
|
||||||
|
/// Used when rescanning a playlist directory without a full library reload.
|
||||||
pub fn scan_video_files(dir: &Path) -> Vec<Video> {
|
pub fn scan_video_files(dir: &Path) -> Vec<Video> {
|
||||||
let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() };
|
let Ok(entries) = std::fs::read_dir(dir) else { return Vec::new() };
|
||||||
let raws = collect_raw_videos(entries.flatten().filter(|e| e.path().is_file()));
|
let raws = collect_raw_videos(entries.flatten().filter(|e| e.path().is_file()));
|
||||||
|
|
|
||||||
14
src/main.rs
14
src/main.rs
|
|
@ -1,3 +1,16 @@
|
||||||
|
//! yt-offline — desktop and web app for archiving YouTube content with yt-dlp.
|
||||||
|
//!
|
||||||
|
//! # Usage
|
||||||
|
//!
|
||||||
|
//! * **GUI mode** (default): `yt-offline`
|
||||||
|
//! * **Web mode**: `yt-offline --web [PORT]` — starts a headless HTTP server
|
||||||
|
//! on the configured port (default 8080).
|
||||||
|
//!
|
||||||
|
//! Configuration is read from `config.toml` in the current working directory.
|
||||||
|
//! See [`config`] for all available options.
|
||||||
|
//!
|
||||||
|
//! Licensed under the GNU Affero General Public License v3 or later (AGPL-3.0+).
|
||||||
|
//! Source code must be made available to network users per AGPL §13.
|
||||||
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]
|
||||||
|
|
||||||
mod app;
|
mod app;
|
||||||
|
|
@ -6,7 +19,6 @@ mod database;
|
||||||
mod downloader;
|
mod downloader;
|
||||||
mod library;
|
mod library;
|
||||||
mod theme;
|
mod theme;
|
||||||
mod tray;
|
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
fn main() -> eframe::Result<()> {
|
fn main() -> eframe::Result<()> {
|
||||||
|
|
|
||||||
36
src/tray.rs
36
src/tray.rs
|
|
@ -1,36 +0,0 @@
|
||||||
use tray_icon::{TrayIconBuilder, menu::Menu};
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
pub fn create_tray_icon(icon_path: Option<PathBuf>) -> Result<tray_icon::TrayIcon, Box<dyn std::error::Error>> {
|
|
||||||
let menu = Menu::new();
|
|
||||||
|
|
||||||
let icon = if let Some(path) = icon_path {
|
|
||||||
load_icon_from_file(&path)?
|
|
||||||
} else {
|
|
||||||
create_default_icon()?
|
|
||||||
};
|
|
||||||
|
|
||||||
let tray = TrayIconBuilder::new()
|
|
||||||
.with_menu(Box::new(menu))
|
|
||||||
.with_tooltip("YouTube Backup\nDownload and manage YouTube channel backups")
|
|
||||||
.with_icon(icon)
|
|
||||||
.build()?;
|
|
||||||
|
|
||||||
Ok(tray)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_icon_from_file(path: &PathBuf) -> Result<tray_icon::Icon, Box<dyn std::error::Error>> {
|
|
||||||
let img = image::open(path)?;
|
|
||||||
let rgba = img.to_rgba8();
|
|
||||||
let (w, h) = rgba.dimensions();
|
|
||||||
Ok(tray_icon::Icon::from_rgba(rgba.into_raw(), w, h)?)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn create_default_icon() -> Result<tray_icon::Icon, Box<dyn std::error::Error>> {
|
|
||||||
let mut rgba = vec![0u8; 64 * 64 * 4];
|
|
||||||
for chunk in rgba.chunks_mut(4) {
|
|
||||||
chunk[0] = 255; // red
|
|
||||||
chunk[3] = 255; // alpha
|
|
||||||
}
|
|
||||||
Ok(tray_icon::Icon::from_rgba(rgba, 64, 64)?)
|
|
||||||
}
|
|
||||||
1082
src/web.rs
1082
src/web.rs
File diff suppressed because it is too large
Load diff
|
|
@ -1,6 +1,6 @@
|
||||||
[Desktop Entry]
|
[Desktop Entry]
|
||||||
Type=Application
|
Type=Application
|
||||||
Name=YouTube Backup
|
Name=YouTube Offline
|
||||||
Comment=Download and manage YouTube channel backups
|
Comment=Download and manage YouTube channel backups
|
||||||
Exec=youtube-backup
|
Exec=youtube-backup
|
||||||
Icon=youtube-backup
|
Icon=youtube-backup
|
||||||
|
|
|
||||||
BIN
yt-offline-0.1.0-1-x86_64.pkg.tar.zst
Normal file
BIN
yt-offline-0.1.0-1-x86_64.pkg.tar.zst
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue