diff --git a/.zed/settings.json b/.zed/settings.json
index 557a88e..14b551a 100644
--- a/.zed/settings.json
+++ b/.zed/settings.json
@@ -1,18 +1,10 @@
{
- "project_name": null,
-
- "auto_install_extensions": {
- "tombi": true,
- "cargo-appraiser": true,
- },
-
"languages": {
"TOML": {
"format_on_save": "on",
"formatter": { "language_server": { "name": "tombi" } },
},
},
-
"lsp": {
"rust-analyzer": {
"initialization_options": {
diff --git a/Cargo.lock b/Cargo.lock
index fa4fd2e..4e279f2 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -121,9 +121,9 @@ checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50"
[[package]]
name = "arrow"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4754a624e5ae42081f464514be454b39711daae0458906dacde5f4c632f33a8"
+checksum = "3bd47f2a6ddc39244bd722a27ee5da66c03369d087b9e024eafdb03e98b98ea7"
dependencies = [
"arrow-arith",
"arrow-array",
@@ -139,9 +139,9 @@ dependencies = [
[[package]]
name = "arrow-arith"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7b3141e0ec5145a22d8694ea8b6d6f69305971c4fa1c1a13ef0195aef2d678b"
+checksum = "7c7bbd679c5418b8639b92be01f361d60013c4906574b578b77b63c78356594c"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -153,9 +153,9 @@ dependencies = [
[[package]]
name = "arrow-array"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4c8955af33b25f3b175ee10af580577280b4bd01f7e823d94c7cdef7cf8c9aef"
+checksum = "c8a4ab47b3f3eac60f7fd31b81e9028fda018607bcc63451aca4f2b755269862"
dependencies = [
"ahash 0.8.12",
"arrow-buffer",
@@ -171,9 +171,9 @@ dependencies = [
[[package]]
name = "arrow-buffer"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c697ddca96183182f35b3a18e50b9110b11e916d7b7799cbfd4d34662f2c56c2"
+checksum = "0d18b89b4c4f4811d0858175e79541fe98e33e18db3b011708bc287b1240593f"
dependencies = [
"bytes",
"half",
@@ -183,9 +183,9 @@ dependencies = [
[[package]]
name = "arrow-cast"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "646bbb821e86fd57189c10b4fcdaa941deaf4181924917b0daa92735baa6ada5"
+checksum = "722b5c41dd1d14d0a879a1bce92c6fe33f546101bb2acce57a209825edd075b3"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -204,9 +204,9 @@ dependencies = [
[[package]]
name = "arrow-data"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1fdd994a9d28e6365aa78e15da3f3950c0fdcea6b963a12fa1c391afb637b304"
+checksum = "c1683705c63dcf0d18972759eda48489028cbbff67af7d6bef2c6b7b74ab778a"
dependencies = [
"arrow-buffer",
"arrow-schema",
@@ -217,9 +217,9 @@ dependencies = [
[[package]]
name = "arrow-ord"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f7d8f1870e03d4cbed632959498bcc84083b5a24bded52905ae1695bd29da45b"
+checksum = "082342947d4e5a2bcccf029a0a0397e21cb3bb8421edd9571d34fb5dd2670256"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -230,9 +230,9 @@ dependencies = [
[[package]]
name = "arrow-row"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "18228633bad92bff92a95746bbeb16e5fc318e8382b75619dec26db79e4de4c0"
+checksum = "e3a931b520a2a5e22033e01a6f2486b4cdc26f9106b759abeebc320f125e94d7"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -243,15 +243,15 @@ dependencies = [
[[package]]
name = "arrow-schema"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8c872d36b7bf2a6a6a2b40de9156265f0242910791db366a2c17476ba8330d68"
+checksum = "e4cf0d4a6609679e03002167a61074a21d7b1ad9ea65e462b2c0a97f8a3b2bc6"
[[package]]
name = "arrow-select"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "68bf3e3efbd1278f770d67e5dc410257300b161b93baedb3aae836144edcaf4b"
+checksum = "0b320d86a9806923663bb0fd9baa65ecaba81cb0cd77ff8c1768b9716b4ef891"
dependencies = [
"ahash 0.8.12",
"arrow-array",
@@ -263,9 +263,9 @@ dependencies = [
[[package]]
name = "arrow-string"
-version = "57.3.0"
+version = "57.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "85e968097061b3c0e9fe3079cf2e703e487890700546b5b0647f60fca1b5a8d8"
+checksum = "b493e99162e5764077e7823e50ba284858d365922631c7aaefe9487b1abd02c2"
dependencies = [
"arrow-array",
"arrow-buffer",
@@ -328,15 +328,15 @@ checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
[[package]]
name = "autocfg"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c08606f8c3cbf4ce6ec8e28fb0014a2c086708fe954eaa885384a6165172e7e8"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "aws-lc-rs"
-version = "1.16.3"
+version = "1.17.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0ec6fb3fe69024a75fa7e1bfb48aa6cf59706a101658ea01bfd33b2b248a038f"
+checksum = "5ec2f1fc3ec205783a5da9a7e6c1509cc69dedf09a1949e412c1e18469326d00"
dependencies = [
"aws-lc-sys",
"zeroize",
@@ -344,9 +344,9 @@ dependencies = [
[[package]]
name = "aws-lc-sys"
-version = "0.40.0"
+version = "0.41.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f50037ee5e1e41e7b8f9d161680a725bd1626cb6f8c7e901f91f942850852fe7"
+checksum = "1a2f9779ce85b93ab6170dd940ad0169b5766ff848247aff13bb788b832fe3f4"
dependencies = [
"cc",
"cmake",
@@ -436,9 +436,9 @@ dependencies = [
[[package]]
name = "bumpalo"
-version = "3.20.2"
+version = "3.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
[[package]]
name = "bytecheck"
@@ -476,9 +476,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
[[package]]
name = "cc"
-version = "1.2.61"
+version = "1.2.63"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d"
+checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
dependencies = [
"find-msvc-tools",
"jobserver",
@@ -486,12 +486,6 @@ dependencies = [
"shlex",
]
-[[package]]
-name = "cesu8"
-version = "1.1.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
-
[[package]]
name = "cfg-if"
version = "1.0.4"
@@ -590,6 +584,18 @@ dependencies = [
"crossbeam-utils",
]
+[[package]]
+name = "console"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "unicode-width",
+ "windows-sys 0.61.2",
+]
+
[[package]]
name = "const-oid"
version = "0.9.6"
@@ -764,6 +770,16 @@ dependencies = [
"unicode-xid",
]
+[[package]]
+name = "dialoguer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96"
+dependencies = [
+ "console",
+ "shell-words",
+]
+
[[package]]
name = "digest"
version = "0.10.7"
@@ -778,9 +794,9 @@ dependencies = [
[[package]]
name = "displaydoc"
-version = "0.2.5"
+version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
dependencies = [
"proc-macro2",
"quote",
@@ -801,13 +817,19 @@ checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
[[package]]
name = "either"
-version = "1.15.0"
+version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
dependencies = [
"serde",
]
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
[[package]]
name = "equivalent"
version = "1.0.2"
@@ -844,7 +866,7 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
[[package]]
name = "flix"
-version = "0.0.18"
+version = "0.0.19"
dependencies = [
"flix-db",
"flix-fs",
@@ -854,7 +876,7 @@ dependencies = [
[[package]]
name = "flix-cli"
-version = "0.0.18"
+version = "0.0.19"
dependencies = [
"anyhow",
"chrono",
@@ -871,7 +893,7 @@ dependencies = [
[[package]]
name = "flix-db"
-version = "0.0.18"
+version = "0.0.19"
dependencies = [
"chrono",
"flix-model",
@@ -884,30 +906,44 @@ dependencies = [
[[package]]
name = "flix-fs"
-version = "0.0.18"
+version = "0.0.19"
dependencies = [
"async-stream",
"either",
"flix-model",
"regex",
- "thiserror 2.0.18",
+ "thiserror",
"tokio",
"tokio-stream",
]
[[package]]
name = "flix-model"
-version = "0.0.18"
+version = "0.0.19"
dependencies = [
"itertools",
"seamantic",
"serde",
- "thiserror 2.0.18",
+ "thiserror",
+]
+
+[[package]]
+name = "flix-mux"
+version = "0.0.19"
+dependencies = [
+ "anyhow",
+ "clap",
+ "console",
+ "dialoguer",
+ "indicatif",
+ "serde",
+ "serde_json",
+ "walkdir",
]
[[package]]
name = "flix-tmdb"
-version = "0.0.18"
+version = "0.0.19"
dependencies = [
"bytes",
"chrono",
@@ -920,7 +956,7 @@ dependencies = [
"serde",
"serde_json",
"serde_test",
- "thiserror 2.0.18",
+ "thiserror",
"url",
"url-macro",
]
@@ -1047,9 +1083,9 @@ checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-timer"
-version = "3.0.3"
+version = "3.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
+checksum = "af43fadb8a98512d547e37b4e92e0ced13e205c061b87b4623eff01d918d6968"
[[package]]
name = "futures-util"
@@ -1175,9 +1211,9 @@ dependencies = [
[[package]]
name = "hashbrown"
-version = "0.17.0"
+version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f467dd6dccf739c208452f8014c75c18bb8301b050ad1cfb27153803edb0f51"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "hashlink"
@@ -1235,9 +1271,9 @@ dependencies = [
[[package]]
name = "http"
-version = "1.4.0"
+version = "1.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a"
+checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
dependencies = [
"bytes",
"itoa",
@@ -1274,9 +1310,9 @@ checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "hyper"
-version = "1.9.0"
+version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6299f016b246a94207e63da54dbe807655bf9e00044f73ded42c3ac5305fbcca"
+checksum = "55281c53a1894c864990125767da440a4e630446785086f52523b20033b74498"
dependencies = [
"atomic-waker",
"bytes",
@@ -1455,9 +1491,9 @@ dependencies = [
[[package]]
name = "idna_adapter"
-version = "1.2.1"
+version = "1.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3acae9609540aa318d1bc588455225fb2085b9ed0c4f6bd0d9d5bcd86f1a0344"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
dependencies = [
"icu_normalizer",
"icu_properties",
@@ -1470,7 +1506,18 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
- "hashbrown 0.17.0",
+ "hashbrown 0.17.1",
+]
+
+[[package]]
+name = "indicatif"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unit-prefix",
]
[[package]]
@@ -1508,16 +1555,6 @@ version = "2.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2"
-[[package]]
-name = "iri-string"
-version = "0.7.12"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "25e659a4bb38e810ebc252e53b5814ff908a8c58c2a9ce2fae1bbec24cbf4e20"
-dependencies = [
- "memchr",
- "serde",
-]
-
[[package]]
name = "is_terminal_polyfill"
version = "1.70.2"
@@ -1541,27 +1578,32 @@ checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "jni"
-version = "0.21.1"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498"
dependencies = [
- "cesu8",
"cfg-if",
"combine",
- "jni-sys 0.3.1",
+ "jni-macros",
+ "jni-sys",
"log",
- "thiserror 1.0.69",
+ "simd_cesu8",
+ "thiserror",
"walkdir",
- "windows-sys 0.45.0",
+ "windows-link",
]
[[package]]
-name = "jni-sys"
-version = "0.3.1"
+name = "jni-macros"
+version = "0.22.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3"
dependencies = [
- "jni-sys 0.4.1",
+ "proc-macro2",
+ "quote",
+ "rustc_version",
+ "simd_cesu8",
+ "syn 2.0.117",
]
[[package]]
@@ -1595,9 +1637,9 @@ dependencies = [
[[package]]
name = "js-sys"
-version = "0.3.95"
+version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2964e92d1d9dc3364cae4d718d93f227e3abb088e747d92e0395bfdedf1c12ca"
+checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
dependencies = [
"cfg-if",
"futures-util",
@@ -1685,14 +1727,14 @@ checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
[[package]]
name = "libredox"
-version = "0.1.16"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e02f3bb43d335493c96bf3fd3a321600bf6bd07ed34bc64118e9293bdffea46c"
+checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
dependencies = [
"bitflags",
"libc",
"plain",
- "redox_syscall 0.7.4",
+ "redox_syscall 0.8.0",
]
[[package]]
@@ -1723,9 +1765,9 @@ dependencies = [
[[package]]
name = "log"
-version = "0.4.29"
+version = "0.4.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
+checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
[[package]]
name = "lru-slab"
@@ -1765,9 +1807,9 @@ dependencies = [
[[package]]
name = "memchr"
-version = "2.8.0"
+version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "memoffset"
@@ -1780,9 +1822,9 @@ dependencies = [
[[package]]
name = "mio"
-version = "1.2.0"
+version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "50b7e5b27aa02a74bac8c3f23f448f8d87ff11f92d3aac1a6ed369ee08cc56c1"
+checksum = "02bd0af71c67b473010cbbc60715ee815645a4dc942899111f494b4b737d6fda"
dependencies = [
"libc",
"wasi",
@@ -1845,9 +1887,9 @@ dependencies = [
[[package]]
name = "num-conv"
-version = "0.2.1"
+version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c6673768db2d862beb9b39a78fdcb1a69439615d5794a1be50caa9bc92c81967"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
[[package]]
name = "num-integer"
@@ -1976,9 +2018,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
[[package]]
name = "pgvector"
-version = "0.4.1"
+version = "0.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "fc58e2d255979a31caa7cabfa7aac654af0354220719ab7a68520ae7a91e8c0b"
+checksum = "3673cba5b9a124916096a423b806a9f29620972c6c97b08db5f2053e9428b481"
dependencies = [
"serde",
]
@@ -2149,7 +2191,7 @@ dependencies = [
"rustc-hash",
"rustls",
"socket2",
- "thiserror 2.0.18",
+ "thiserror",
"tokio",
"tracing",
"web-time",
@@ -2171,7 +2213,7 @@ dependencies = [
"rustls",
"rustls-pki-types",
"slab",
- "thiserror 2.0.18",
+ "thiserror",
"tinyvec",
"tracing",
"web-time",
@@ -2291,9 +2333,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
-version = "0.7.4"
+version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f450ad9c3b1da563fb6948a8e0fb0fb9269711c9c73d9ea1de5058c79c8d643a"
+checksum = "7c7591fa2c6b601dfcfe5f043f65a1c39fcdf50efefcd7f1572e538c1f4b398d"
dependencies = [
"bitflags",
]
@@ -2338,9 +2380,9 @@ dependencies = [
[[package]]
name = "reqwest"
-version = "0.13.2"
+version = "0.13.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801"
+checksum = "219c5811de6525e5416c7d5d53bb656d3afdbc6c5af816e0802bcfa42dbdc1c3"
dependencies = [
"base64",
"bytes",
@@ -2438,9 +2480,9 @@ dependencies = [
[[package]]
name = "rust_decimal"
-version = "1.41.0"
+version = "1.42.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ce901f9a19d251159075a4c37af514c3b8ef99c22e02dd8c19161cf397ee94a"
+checksum = "0c5108e3d4d903e21aac27f12ba5377b6b34f9f44b325e4894c7924169d06995"
dependencies = [
"arrayvec",
"borsh",
@@ -2470,9 +2512,9 @@ dependencies = [
[[package]]
name = "rustls"
-version = "0.23.39"
+version = "0.23.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
dependencies = [
"aws-lc-rs",
"once_cell",
@@ -2507,9 +2549,9 @@ dependencies = [
[[package]]
name = "rustls-platform-verifier"
-version = "0.6.2"
+version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
+checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0"
dependencies = [
"core-foundation",
"core-foundation-sys",
@@ -2621,7 +2663,7 @@ dependencies = [
"serde_json",
"sqlx",
"strum",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"url",
@@ -2636,7 +2678,7 @@ checksum = "5c2eee8405f16c1f337fe3a83389361caea83c928d14dbd666a480407072c365"
dependencies = [
"arrow",
"sea-query",
- "thiserror 2.0.18",
+ "thiserror",
]
[[package]]
@@ -2705,16 +2747,16 @@ dependencies = [
[[package]]
name = "sea-query-derive"
-version = "1.0.0-rc.12"
+version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d88ad44b6ad9788c8b9476b6b91f94c7461d1e19d39cd8ea37838b1e6ff5aa8"
+checksum = "a0b0f466921cdd3cf4b89d5c3ac2173dba89a873ab395b123a645de181ec7537"
dependencies = [
"darling",
"heck 0.4.1",
"proc-macro2",
"quote",
"syn 2.0.117",
- "thiserror 2.0.18",
+ "thiserror",
]
[[package]]
@@ -2760,9 +2802,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b"
[[package]]
name = "seamantic"
-version = "0.0.13"
+version = "0.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1a7c9658b032d7bcb6938fb7e16b1425b1ce7f7c25dfaae667e0c30bc6dc3f1b"
+checksum = "04509950290d9b38f521bb7e5a2490330b1e9c4ee433ce33cc2268efb5d06cab"
dependencies = [
"sea-orm",
"sea-orm-migration",
@@ -2829,9 +2871,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.149"
+version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
@@ -2902,10 +2944,16 @@ dependencies = [
]
[[package]]
-name = "shlex"
-version = "1.3.0"
+name = "shell-words"
+version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
[[package]]
name = "signature"
@@ -2917,6 +2965,16 @@ dependencies = [
"rand_core 0.6.4",
]
+[[package]]
+name = "simd_cesu8"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33"
+dependencies = [
+ "rustc_version",
+ "simdutf8",
+]
+
[[package]]
name = "simdutf8"
version = "0.1.5"
@@ -2940,9 +2998,9 @@ dependencies = [
[[package]]
name = "socket2"
-version = "0.6.3"
+version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
+checksum = "52d1cfed4120b4d927bf7c0f86d2087a4a7d6027c906d9f9d525a80573b9be51"
dependencies = [
"libc",
"windows-sys 0.61.2",
@@ -3019,7 +3077,7 @@ dependencies = [
"serde_json",
"sha2",
"smallvec",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tokio",
"tokio-stream",
@@ -3106,7 +3164,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"uuid",
@@ -3147,7 +3205,7 @@ dependencies = [
"smallvec",
"sqlx-core",
"stringprep",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"uuid",
@@ -3174,7 +3232,7 @@ dependencies = [
"serde",
"serde_urlencoded",
"sqlx-core",
- "thiserror 2.0.18",
+ "thiserror",
"time",
"tracing",
"url",
@@ -3270,33 +3328,13 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369"
-[[package]]
-name = "thiserror"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
-dependencies = [
- "thiserror-impl 1.0.69",
-]
-
[[package]]
name = "thiserror"
version = "2.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
dependencies = [
- "thiserror-impl 2.0.18",
-]
-
-[[package]]
-name = "thiserror-impl"
-version = "1.0.69"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.117",
+ "thiserror-impl",
]
[[package]]
@@ -3386,9 +3424,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
-version = "1.52.1"
+version = "1.52.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6"
+checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe"
dependencies = [
"bytes",
"libc",
@@ -3455,9 +3493,9 @@ dependencies = [
[[package]]
name = "toml_edit"
-version = "0.25.11+spec-1.1.0"
+version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0b59c4d22ed448339746c59b905d24568fcbb3ab65a500494f7b8c3e97739f2b"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [
"indexmap",
"toml_datetime",
@@ -3491,20 +3529,20 @@ dependencies = [
[[package]]
name = "tower-http"
-version = "0.6.8"
+version = "0.6.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8"
+checksum = "4cfcf7e2740e6fc6d4d688b4ef00650406bb94adf4731e43c096c3a19fe40840"
dependencies = [
"bitflags",
"bytes",
"futures-util",
"http",
"http-body",
- "iri-string",
"pin-project-lite",
"tower",
"tower-layer",
"tower-service",
+ "url",
]
[[package]]
@@ -3574,9 +3612,9 @@ checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b"
[[package]]
name = "typenum"
-version = "1.20.0"
+version = "1.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
[[package]]
name = "unicode-bidi"
@@ -3605,12 +3643,24 @@ version = "0.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
[[package]]
name = "unicode-xid"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+[[package]]
+name = "unit-prefix"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
+
[[package]]
name = "untrusted"
version = "0.9.0"
@@ -3652,9 +3702,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
[[package]]
name = "uuid"
-version = "1.23.1"
+version = "1.23.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ddd74a9687298c6858e9b88ec8935ec45d22e8fd5e6394fa1bd4e99a87789c76"
+checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
dependencies = [
"js-sys",
"serde_core",
@@ -3715,9 +3765,9 @@ checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
[[package]]
name = "wasm-bindgen"
-version = "0.2.118"
+version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "0bf938a0bacb0469e83c1e148908bd7d5a6010354cf4fb73279b7447422e3a89"
+checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
dependencies = [
"cfg-if",
"once_cell",
@@ -3729,9 +3779,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
-version = "0.4.68"
+version = "0.4.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f371d383f2fb139252e0bfac3b81b265689bf45b6874af544ffa4c975ac1ebf8"
+checksum = "9473dbd2991ae90b6291c3c32c30c6187ac49aa32f9905d1cce280ec1e110b0f"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3739,9 +3789,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro"
-version = "0.2.118"
+version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eeff24f84126c0ec2db7a449f0c2ec963c6a49efe0698c4242929da037ca28ed"
+checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
dependencies = [
"quote",
"wasm-bindgen-macro-support",
@@ -3749,9 +3799,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-macro-support"
-version = "0.2.118"
+version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9d08065faf983b2b80a79fd87d8254c409281cf7de75fc4b773019824196c904"
+checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
dependencies = [
"bumpalo",
"proc-macro2",
@@ -3762,18 +3812,18 @@ dependencies = [
[[package]]
name = "wasm-bindgen-shared"
-version = "0.2.118"
+version = "0.2.122"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fd04d9e306f1907bd13c6361b5c6bfc7b3b3c095ed3f8a9246390f8dbdee129"
+checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
dependencies = [
"unicode-ident",
]
[[package]]
name = "web-sys"
-version = "0.3.95"
+version = "0.3.99"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "4f2dfbb17949fa2088e5d39408c48368947b86f7834484e87b73de55bc14d97d"
+checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
dependencies = [
"js-sys",
"wasm-bindgen",
@@ -3916,15 +3966,6 @@ dependencies = [
"windows-link",
]
-[[package]]
-name = "windows-sys"
-version = "0.45.0"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
-dependencies = [
- "windows-targets 0.42.2",
-]
-
[[package]]
name = "windows-sys"
version = "0.48.0"
@@ -3961,21 +4002,6 @@ dependencies = [
"windows-link",
]
-[[package]]
-name = "windows-targets"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
-dependencies = [
- "windows_aarch64_gnullvm 0.42.2",
- "windows_aarch64_msvc 0.42.2",
- "windows_i686_gnu 0.42.2",
- "windows_i686_msvc 0.42.2",
- "windows_x86_64_gnu 0.42.2",
- "windows_x86_64_gnullvm 0.42.2",
- "windows_x86_64_msvc 0.42.2",
-]
-
[[package]]
name = "windows-targets"
version = "0.48.5"
@@ -4024,12 +4050,6 @@ dependencies = [
"windows_x86_64_msvc 0.53.1",
]
-[[package]]
-name = "windows_aarch64_gnullvm"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
-
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.48.5"
@@ -4048,12 +4068,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
-[[package]]
-name = "windows_aarch64_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
-
[[package]]
name = "windows_aarch64_msvc"
version = "0.48.5"
@@ -4072,12 +4086,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
-[[package]]
-name = "windows_i686_gnu"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
-
[[package]]
name = "windows_i686_gnu"
version = "0.48.5"
@@ -4108,12 +4116,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
-[[package]]
-name = "windows_i686_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
-
[[package]]
name = "windows_i686_msvc"
version = "0.48.5"
@@ -4132,12 +4134,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
-[[package]]
-name = "windows_x86_64_gnu"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
-
[[package]]
name = "windows_x86_64_gnu"
version = "0.48.5"
@@ -4156,12 +4152,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
-[[package]]
-name = "windows_x86_64_gnullvm"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
-
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.48.5"
@@ -4180,12 +4170,6 @@ version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
-[[package]]
-name = "windows_x86_64_msvc"
-version = "0.42.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
-
[[package]]
name = "windows_x86_64_msvc"
version = "0.48.5"
@@ -4206,9 +4190,9 @@ checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
[[package]]
name = "winnow"
-version = "1.0.2"
+version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
dependencies = [
"memchr",
]
@@ -4265,18 +4249,18 @@ dependencies = [
[[package]]
name = "zerocopy"
-version = "0.8.48"
+version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "eed437bf9d6692032087e337407a86f04cd8d6a16a37199ed57949d415bd68e9"
+checksum = "3b065d4f0e55f82fae73202e189638116a87c55ab6b8e6c2721e13dd9d854ad1"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
-version = "0.8.48"
+version = "0.8.50"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70e3cd084b1788766f53af483dd21f93881ff30d7320490ec3ef7526d203bad4"
+checksum = "0b631b19d36a892ab55420c92dbc83ccd79274f25be714855d3074aa71cab639"
dependencies = [
"proc-macro2",
"quote",
@@ -4285,9 +4269,9 @@ dependencies = [
[[package]]
name = "zerofrom"
-version = "0.1.7"
+version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
dependencies = [
"zerofrom-derive",
]
diff --git a/Cargo.toml b/Cargo.toml
index 5cc2edd..3124889 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -9,14 +9,15 @@ edition = "2024"
rust-version = "1.89.0"
[workspace.dependencies]
-flix = { path = "crates/flix", version = "=0.0.18", default-features = false }
-flix-cli = { path = "crates/cli", version = "=0.0.18", default-features = false }
-flix-db = { path = "crates/db", version = "=0.0.18", default-features = false }
-flix-fs = { path = "crates/fs", version = "=0.0.18", default-features = false }
-flix-model = { path = "crates/model", version = "=0.0.18", default-features = false }
-flix-tmdb = { path = "crates/tmdb", version = "=0.0.18", default-features = false }
+flix = { path = "crates/flix", version = "=0.0.19", default-features = false }
+flix-cli = { path = "crates/cli", version = "=0.0.19", default-features = false }
+flix-db = { path = "crates/db", version = "=0.0.19", default-features = false }
+flix-fs = { path = "crates/fs", version = "=0.0.19", default-features = false }
+flix-model = { path = "crates/model", version = "=0.0.19", default-features = false }
+flix-mux = { path = "crates/cli-mux", version = "=0.0.19", default-features = false }
+flix-tmdb = { path = "crates/tmdb", version = "=0.0.19", default-features = false }
-seamantic = { version = "^0.0.13", default-features = false }
+seamantic = { version = "^0.0.14", default-features = false }
sea-orm = { version = "=2.0.0-rc.38", default-features = false }
sea-orm-migration = { version = "=2.0.0-rc.38", default-features = false }
@@ -26,9 +27,12 @@ async-stream = { version = "^0.3", default-features = false }
bytes = { version = "^1", default-features = false }
chrono = { version = "^0.4", default-features = false }
clap = { version = "^4", default-features = false }
+console = { version = "^0.16", default-features = false }
+dialoguer = { version = "^0.12", default-features = false }
either = { version = "^1", default-features = false }
futures = { version = "^0.3", default-features = false }
governor = { version = "^0.10", default-features = false }
+indicatif = { version = "^0.18", default-features = false }
itertools = { version = "^0.14", default-features = false }
nonzero_ext = { version = "^0.3", default-features = false }
redb = { version = "^4", default-features = false }
@@ -45,6 +49,7 @@ tracing = { version = "^0.1", default-features = false }
tracing-subscriber = { version = "^0.3", default-features = false }
url = { version = "^2", default-features = false }
url-macro = { version = "^0.2", default-features = false }
+walkdir = { version = "^2", default-features = false }
[workspace.lints.clippy]
arithmetic_side_effects = "forbid"
diff --git a/README.md b/README.md
index dfdb02e..28d7e2b 100644
--- a/README.md
+++ b/README.md
@@ -11,6 +11,7 @@ Libraries and tools for dealing with media metadata
- fmt: `cargo fmt --check`
- docs: `RUSTDOCFLAGS="--cfg docsrs" cargo +nightly doc --all-features`
- install: `cargo install --path crates/cli`
+- install: `cargo install --path crates/cli-mux`
- semver: `cargo semver-checks --all-features`
- publish: `cargo publish --dry-run --workspace`
diff --git a/crates/cli-mux/Cargo.toml b/crates/cli-mux/Cargo.toml
new file mode 100644
index 0000000..5ee9ea4
--- /dev/null
+++ b/crates/cli-mux/Cargo.toml
@@ -0,0 +1,57 @@
+[package]
+name = "flix-mux"
+version = "0.0.19"
+license-file.workspace = true
+
+description = "CLI for bulk media muxing"
+repository = "https://github.com/QuantumShade/flix"
+categories = ["command-line-utilities"]
+
+edition.workspace = true
+rust-version.workspace = true
+
+[package.metadata.docs.rs]
+all-features = true
+rustdoc-args = ["--cfg", "docsrs"]
+
+[[bin]]
+doc = false
+name = "flix-mux"
+path = "src/main.rs"
+
+[dependencies]
+anyhow = { workspace = true }
+clap = { workspace = true, features = [
+ "color",
+ "derive",
+ "error-context",
+ "help",
+ "std",
+ "suggestions",
+ "usage",
+] }
+console = { workspace = true }
+dialoguer = { workspace = true }
+indicatif = { workspace = true }
+serde = { workspace = true, features = ["derive", "std"] }
+serde_json = { workspace = true, features = ["alloc"] }
+walkdir = { workspace = true }
+
+[lints.clippy]
+arithmetic_side_effects = "deny"
+as_conversions = "deny"
+checked_conversions = "deny"
+default_union_representation = "deny"
+expect_used = "deny"
+indexing_slicing = "deny"
+integer_division = "deny"
+integer_division_remainder_used = "deny"
+transmute_undefined_repr = "deny"
+unchecked_time_subtraction = "deny"
+unwrap_used = "deny"
+
+[lints.rust]
+arithmetic_overflow = "forbid"
+missing_docs = "forbid"
+unsafe_code = "forbid"
+unused_doc_comments = "forbid"
diff --git a/crates/cli-mux/README.md b/crates/cli-mux/README.md
new file mode 100644
index 0000000..46cd49e
--- /dev/null
+++ b/crates/cli-mux/README.md
@@ -0,0 +1,11 @@
+# flix-mux
+
+[](https://crates.io/crates/flix-mux)
+
+CLI for bulk media muxing
+
+## Commands
+
+```sh
+cargo run --
+```
diff --git a/crates/cli-mux/src/cli.rs b/crates/cli-mux/src/cli.rs
new file mode 100644
index 0000000..835f64b
--- /dev/null
+++ b/crates/cli-mux/src/cli.rs
@@ -0,0 +1,49 @@
+use std::path::PathBuf;
+
+use clap::Parser;
+
+use crate::parser::Selector;
+
+#[derive(Parser)]
+#[command(version, about, long_about = None)]
+pub struct Cli {
+ /// Dry run and print commands
+ #[arg(short, long)]
+ dry_run: bool,
+
+ /// Stream selectors
+ #[arg(
+ short,
+ long,
+ required = true,
+ value_name = "SEL",
+ value_delimiter = ';'
+ )]
+ selectors: Vec,
+
+ /// The path to the directory to scan
+ #[arg(value_name = "DIR")]
+ scan_dir: PathBuf,
+}
+
+impl Cli {
+ pub fn is_dry(&self) -> bool {
+ self.dry_run
+ }
+
+ pub fn selectors(&self) -> &[Selector] {
+ &self.selectors
+ }
+
+ pub fn scan_dir_path(&self) -> PathBuf {
+ fn expect_home_dir() -> PathBuf {
+ #[allow(clippy::expect_used)]
+ std::env::home_dir().expect("you do not have a home directory")
+ }
+
+ match self.scan_dir.strip_prefix("~/") {
+ Ok(path) => expect_home_dir().join(path),
+ Err(_) => self.scan_dir.to_owned(),
+ }
+ }
+}
diff --git a/crates/cli-mux/src/main.rs b/crates/cli-mux/src/main.rs
new file mode 100644
index 0000000..db13a88
--- /dev/null
+++ b/crates/cli-mux/src/main.rs
@@ -0,0 +1,66 @@
+//! flix-mux
+
+use core::time::Duration;
+
+use clap::Parser;
+use console::style;
+use indicatif::{HumanBytes, ProgressBar, ProgressStyle};
+
+use crate::mux::mux_files;
+use crate::scan::scan_directory;
+
+mod cli;
+mod model;
+mod mux;
+mod parser;
+mod probe;
+mod scan;
+
+fn main() {
+ let cli = cli::Cli::parse();
+
+ let (files, size) = scan_directory(
+ &cli.scan_dir_path(),
+ || {
+ let progress = ProgressBar::new_spinner();
+ progress.set_message(format!("Scanning {:?}", &cli.scan_dir_path()));
+ progress.enable_steady_tick(Duration::from_millis(50));
+ progress
+ },
+ |progress| progress.finish(),
+ |len| {
+ let progress = ProgressBar::new(u64::try_from(len).unwrap_or(0));
+ progress.set_style(
+ #[expect(clippy::expect_used)]
+ ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
+ .expect("static template"),
+ );
+ progress
+ },
+ |progress| progress.inc(1),
+ |progress| progress.finish_and_clear(),
+ |progress, msg| {
+ progress.suspend(|| eprintln!("{} {}", style("[WARN]").bold().yellow(), msg))
+ },
+ );
+ println!("Found {} files ({})", files.len(), HumanBytes(size));
+
+ mux_files(
+ cli.is_dry(),
+ &files,
+ cli.selectors(),
+ |len| {
+ let progress = ProgressBar::new(u64::try_from(len).unwrap_or(0));
+ progress.set_style(
+ #[expect(clippy::expect_used)]
+ ProgressStyle::with_template("[{elapsed_precise}] {wide_bar} {pos}/{len} ({msg})")
+ .expect("static template"),
+ );
+ progress.enable_steady_tick(Duration::from_secs(1));
+ progress
+ },
+ |progress| progress.inc(1),
+ |progress| progress.finish_with_message("done"),
+ |progress, msg| progress.suspend(|| eprintln!("{} {}", style("[ERR]").bold().red(), msg)),
+ );
+}
diff --git a/crates/cli-mux/src/model.rs b/crates/cli-mux/src/model.rs
new file mode 100644
index 0000000..ffd5756
--- /dev/null
+++ b/crates/cli-mux/src/model.rs
@@ -0,0 +1,101 @@
+use std::path::PathBuf;
+
+use serde::Deserialize;
+
+#[derive(Debug, Clone)]
+pub struct MediaFile {
+ pub path: PathBuf,
+ pub byte_size: u64,
+ pub streams: Streams,
+}
+
+#[derive(Debug, Clone)]
+pub struct Streams {
+ pub video: Vec,
+ pub audio: Vec,
+ pub subtitle: Vec,
+}
+
+pub trait FFStream: serde::de::DeserializeOwned {
+ const FF_TYPE_NAME: &str;
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct VideoStream {
+ codec_name: String,
+ tags: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct VideoTags {
+ language: Option,
+}
+
+impl VideoStream {
+ pub fn codec(&self) -> &str {
+ &self.codec_name
+ }
+
+ pub fn language(&self) -> Option<&str> {
+ self.tags.as_ref()?.language.as_deref()
+ }
+}
+
+impl FFStream for VideoStream {
+ const FF_TYPE_NAME: &str = "v";
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AudioStream {
+ codec_name: String,
+ tags: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct AudioTags {
+ language: Option,
+}
+
+impl AudioStream {
+ pub fn codec(&self) -> &str {
+ &self.codec_name
+ }
+
+ pub fn language(&self) -> Option<&str> {
+ self.tags.as_ref()?.language.as_deref()
+ }
+}
+
+impl FFStream for AudioStream {
+ const FF_TYPE_NAME: &str = "a";
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct SubtitleStream {
+ codec_name: String,
+ tags: Option,
+}
+
+#[derive(Debug, Clone, Deserialize)]
+pub struct SubtitleTags {
+ language: Option,
+ title: Option,
+}
+
+impl SubtitleStream {
+ pub fn codec(&self) -> &str {
+ &self.codec_name
+ }
+
+ pub fn language(&self) -> Option<&str> {
+ self.tags.as_ref()?.language.as_deref()
+ }
+
+ pub fn title(&self) -> Option<&str> {
+ self.tags.as_ref()?.title.as_deref()
+ }
+}
+
+impl FFStream for SubtitleStream {
+ const FF_TYPE_NAME: &str = "s";
+}
diff --git a/crates/cli-mux/src/mux.rs b/crates/cli-mux/src/mux.rs
new file mode 100644
index 0000000..8b4e025
--- /dev/null
+++ b/crates/cli-mux/src/mux.rs
@@ -0,0 +1,227 @@
+use std::process::Command;
+
+use anyhow::{Context as _, Result};
+
+use crate::model::MediaFile;
+use crate::parser::{Matcher, Selector, StreamFlag, StreamType};
+
+pub fn mux_files(
+ dry_run: bool,
+ files: &[MediaFile],
+ selectors: &[Selector],
+ fixed_length_start: impl FnOnce(usize) -> T,
+ fixed_length_update: impl Fn(&mut T),
+ fixed_length_end: impl FnOnce(T),
+ print_fn: impl Fn(&T, &str),
+) {
+ let mut progress = fixed_length_start(files.len());
+ for file in files {
+ if let Err(err) = mux(dry_run, file, selectors) {
+ print_fn(&progress, &format!("{:?}", err));
+ }
+ fixed_length_update(&mut progress);
+ }
+ fixed_length_end(progress);
+}
+
+#[expect(clippy::expect_used)]
+fn mux(dry_run: bool, file: &MediaFile, selectors: &[Selector]) -> Result<()> {
+ let mut command = Command::new("ffmpeg");
+ let mut command = command.args(["-v", "error"]);
+
+ command = command.arg("-i");
+ command = command.arg(file.path.as_os_str());
+
+ for selector in selectors {
+ command = command.args(
+ make_map_args(file, selector)
+ .with_context(|| format!("Failed to mux {:?}", file.path))?,
+ );
+ }
+
+ command = command.args(["-c:v", "copy", "-c:a", "copy", "-c:s", "mov_text"]);
+ command = command.args(["-movflags", "faststart+disable_chpl+write_colr"]);
+ command = command.args(["-map_chapters", "-1"]);
+ command = command.args(["-map_metadata", "-1"]);
+ command = command.args(["-metadata:g", "encoding_tool=Skrundzflix"]);
+
+ for selector in selectors {
+ command = command.args(
+ make_metadata_args(file, selector)
+ .with_context(|| format!("Failed to mux {:?}", file.path))?,
+ );
+ }
+
+ let temp_path = file.path.with_extension("mp4");
+ command = command.arg(temp_path.file_name().expect("file name exists"));
+
+ if dry_run {
+ print_command(command);
+ } else {
+ let output = command
+ .output()
+ .with_context(|| format!("Failed to mux {:?}", file.path))?;
+
+ if !output.status.success() {
+ anyhow::bail!(
+ "ffmpeg failed for {:?}:\n\n{}",
+ file.path,
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+ }
+
+ Ok(())
+}
+
+fn make_map_args(file: &MediaFile, selector: &Selector) -> Result> {
+ let source_index = 0;
+ let stream_type = selector.stream_type.as_ref();
+ let Some(stream_index) = find_stream_index(file, selector) else {
+ if selector.optional {
+ return Ok(vec![]);
+ } else {
+ anyhow::bail!("unsatisfied stream selection");
+ }
+ };
+
+ Ok(vec![
+ String::from("-map"),
+ format!("{}:{}:{}", source_index, stream_type, stream_index),
+ ])
+}
+
+fn make_metadata_args(file: &MediaFile, selector: &Selector) -> Result> {
+ let stream_type = selector.stream_type.as_ref();
+ let Some(stream_index) = find_stream_index(file, selector) else {
+ if selector.optional {
+ return Ok(vec![]);
+ } else {
+ anyhow::bail!("unsatisfied stream selection");
+ }
+ };
+
+ let stream_language = selector.language();
+
+ let mut args = vec![
+ format!("-metadata:s:{}:{}", stream_type, stream_index),
+ format!("language={}", stream_language),
+ ];
+
+ if selector.stream_type == StreamType::Subtitle {
+ let sub_title = match selector.flag {
+ Some(StreamFlag::Forced) => "Forced",
+ Some(StreamFlag::Sdh) => "SDH",
+ None => match stream_language {
+ "eng" => "English",
+ "jpn" => "Japanese",
+ _ => anyhow::bail!("Unhandled subtitle language: {}", stream_language),
+ },
+ };
+
+ args.push(format!("-metadata:s:s:{}", stream_index));
+ args.push(format!("title={}", sub_title));
+ }
+
+ Ok(args)
+}
+
+fn find_stream_index(file: &MediaFile, selector: &Selector) -> Option {
+ let needs_forced = selector.flag == Some(StreamFlag::Forced);
+ let needs_sdh = selector.flag == Some(StreamFlag::Sdh);
+
+ match selector.stream_type {
+ StreamType::Video => match selector.matcher {
+ Matcher::Index(index) => (file.streams.video.len() > index).then_some(index),
+ Matcher::Language(ref language) => file
+ .streams
+ .video
+ .iter()
+ .enumerate()
+ .filter(|(_, c)| c.language() == Some(language.as_str()))
+ .map(|(i, _)| i)
+ .next(),
+ Matcher::Codec(ref codec) => file
+ .streams
+ .video
+ .iter()
+ .enumerate()
+ .filter(|(_, c)| c.codec() == codec)
+ .map(|(i, _)| i)
+ .next(),
+ },
+ StreamType::Audio => match selector.matcher {
+ Matcher::Index(index) => (file.streams.audio.len() > index).then_some(index),
+ Matcher::Language(ref language) => file
+ .streams
+ .audio
+ .iter()
+ .enumerate()
+ .filter(|(_, c)| c.language() == Some(language.as_str()))
+ .map(|(i, _)| i)
+ .next(),
+ Matcher::Codec(ref codec) => file
+ .streams
+ .audio
+ .iter()
+ .enumerate()
+ .filter(|(_, c)| c.codec() == codec)
+ .map(|(i, _)| i)
+ .next(),
+ },
+ StreamType::Subtitle => match selector.matcher {
+ Matcher::Index(index) => (file.streams.subtitle.len() > index).then_some(index),
+ Matcher::Language(ref language) => {
+ file.streams
+ .subtitle
+ .iter()
+ .enumerate()
+ .filter(|(_, c)| c.language() == Some(language.as_str()))
+ .filter(|(_, c)| {
+ c.title()
+ .unwrap_or_default()
+ .to_ascii_lowercase()
+ .contains("forced") == needs_forced
+ })
+ .filter(|(_, c)| {
+ c.title()
+ .unwrap_or_default()
+ .to_ascii_lowercase()
+ .contains("sdh") == needs_sdh
+ })
+ .map(|(i, _)| i)
+ .next()
+ }
+ Matcher::Codec(ref codec) => file
+ .streams
+ .subtitle
+ .iter()
+ .enumerate()
+ .filter(|(_, c)| c.codec() == codec)
+ .map(|(i, _)| i)
+ .next(),
+ },
+ }
+}
+
+fn print_command(cmd: &Command) {
+ let program = cmd.get_program().to_string_lossy();
+
+ let args = cmd
+ .get_args()
+ .map(|a| shell_escape(a.to_string_lossy().as_ref()))
+ .collect::>()
+ .join(" ");
+
+ println!("{} {}", program, args);
+}
+
+fn shell_escape(s: &str) -> String {
+ if s.chars()
+ .all(|c| c.is_ascii_alphanumeric() || "-_./".contains(c))
+ {
+ s.to_string()
+ } else {
+ format!("{:?}", s)
+ }
+}
diff --git a/crates/cli-mux/src/parser.rs b/crates/cli-mux/src/parser.rs
new file mode 100644
index 0000000..9933d22
--- /dev/null
+++ b/crates/cli-mux/src/parser.rs
@@ -0,0 +1,159 @@
+use core::error::Error;
+use core::fmt;
+use core::str::FromStr;
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum StreamType {
+ Video,
+ Audio,
+ Subtitle,
+}
+
+impl AsRef for StreamType {
+ fn as_ref(&self) -> &str {
+ match self {
+ StreamType::Video => "v",
+ StreamType::Audio => "a",
+ StreamType::Subtitle => "s",
+ }
+ }
+}
+
+impl FromStr for StreamType {
+ type Err = ParseSelectorError;
+
+ fn from_str(s: &str) -> Result {
+ match s {
+ "v" => Ok(StreamType::Video),
+ "a" => Ok(StreamType::Audio),
+ "s" => Ok(StreamType::Subtitle),
+ other => Err(ParseSelectorError::InvalidStreamType(other.to_string())),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub enum Matcher {
+ Index(usize),
+ Language(String),
+ Codec(String),
+}
+
+impl FromStr for Matcher {
+ type Err = ParseSelectorError;
+
+ fn from_str(s: &str) -> Result {
+ if let Ok(idx) = s.parse::() {
+ return Ok(Matcher::Index(idx));
+ }
+
+ if s.len() == 3 {
+ return Ok(Matcher::Language(s.to_ascii_lowercase()));
+ }
+ Ok(Matcher::Codec(s.to_ascii_lowercase()))
+ }
+}
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq)]
+pub enum StreamFlag {
+ Forced,
+ Sdh,
+}
+
+impl FromStr for StreamFlag {
+ type Err = ParseSelectorError;
+
+ fn from_str(s: &str) -> Result {
+ match s.to_ascii_lowercase().as_str() {
+ "forced" => Ok(StreamFlag::Forced),
+ "sdh" => Ok(StreamFlag::Sdh),
+ other => Err(ParseSelectorError::InvalidFlag(other.to_string())),
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct Selector {
+ pub stream_type: StreamType,
+ pub matcher: Matcher,
+ pub flag: Option,
+ pub optional: bool,
+ pub out_lang: String,
+}
+
+impl Selector {
+ pub fn language(&self) -> &str {
+ &self.out_lang
+ }
+}
+
+impl FromStr for Selector {
+ type Err = ParseSelectorError;
+
+ fn from_str(input: &str) -> Result {
+ if input.is_empty() {
+ return Err(ParseSelectorError::Empty);
+ }
+
+ let (input, optional) = input
+ .strip_suffix('?')
+ .map_or((input, false), |s| (s, true));
+
+ let (left, out_lang) = input.split_once('>').unwrap_or((input, ""));
+
+ let mut parts = left.splitn(3, ':');
+
+ let stream_type: StreamType = parts
+ .next()
+ .ok_or(ParseSelectorError::InvalidFormat)?
+ .parse()?;
+
+ let matcher: Matcher = parts
+ .next()
+ .ok_or(ParseSelectorError::InvalidFormat)?
+ .parse()?;
+
+ let flag = parts
+ .next()
+ .filter(|s| !s.is_empty())
+ .map(str::parse)
+ .transpose()?;
+
+ let out_lang = match (out_lang.is_empty(), &matcher) {
+ (false, _) => out_lang.to_owned(),
+ (true, Matcher::Language(language)) => language.clone(),
+ (true, _) => return Err(ParseSelectorError::UnspecifiedLanguage),
+ };
+
+ Ok(Selector {
+ stream_type,
+ matcher,
+ flag,
+ optional,
+ out_lang,
+ })
+ }
+}
+
+#[derive(Debug)]
+pub enum ParseSelectorError {
+ Empty,
+ InvalidFormat,
+ InvalidStreamType(String),
+ InvalidFlag(String),
+ UnspecifiedLanguage,
+}
+
+impl Error for ParseSelectorError {}
+
+impl fmt::Display for ParseSelectorError {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ match self {
+ Self::Empty => write!(f, "selector was empty"),
+ Self::InvalidFormat => write!(f, "invalid selector format"),
+ Self::InvalidStreamType(s) => write!(f, "invalid stream type '{}'", s),
+ Self::InvalidFlag(s) => write!(f, "invalid stream flag '{}'", s),
+ Self::UnspecifiedLanguage => write!(f, "unspecified output language"),
+ }
+ }
+}
diff --git a/crates/cli-mux/src/probe.rs b/crates/cli-mux/src/probe.rs
new file mode 100644
index 0000000..595420c
--- /dev/null
+++ b/crates/cli-mux/src/probe.rs
@@ -0,0 +1,53 @@
+use std::os::unix::fs::MetadataExt;
+use std::process::Command;
+
+use anyhow::{Context as _, Result};
+use serde::Deserialize;
+use walkdir::DirEntry;
+
+use crate::model::{FFStream, MediaFile, Streams};
+
+#[derive(Debug, Deserialize)]
+struct FFProbeOutput {
+ streams: Vec,
+}
+
+fn probe_file_streams(entry: &DirEntry) -> Result> {
+ let output = Command::new("ffprobe")
+ .args([
+ "-v",
+ "error",
+ "-output_format",
+ "json",
+ "-show_streams",
+ "-select_streams",
+ S::FF_TYPE_NAME,
+ #[expect(clippy::expect_used)]
+ entry.path().to_str().expect("path should be utf8"),
+ ])
+ .output()
+ .with_context(|| format!("Failed to run ffprobe on {:?}", entry.path()))?;
+
+ if !output.status.success() {
+ anyhow::bail!(
+ "ffprobe failed for {:?}:\n\n{}",
+ entry.path(),
+ String::from_utf8_lossy(&output.stderr)
+ );
+ }
+
+ let parsed: FFProbeOutput = serde_json::from_slice(&output.stdout)?;
+ Ok(parsed.streams)
+}
+
+pub fn probe_file(entry: &DirEntry) -> Result {
+ Ok(MediaFile {
+ path: entry.path().to_path_buf(),
+ byte_size: entry.metadata()?.size(),
+ streams: Streams {
+ video: probe_file_streams(entry)?,
+ audio: probe_file_streams(entry)?,
+ subtitle: probe_file_streams(entry)?,
+ },
+ })
+}
diff --git a/crates/cli-mux/src/scan.rs b/crates/cli-mux/src/scan.rs
new file mode 100644
index 0000000..6e592f6
--- /dev/null
+++ b/crates/cli-mux/src/scan.rs
@@ -0,0 +1,58 @@
+use std::path::Path;
+
+use walkdir::WalkDir;
+
+use crate::model::MediaFile;
+use crate::probe::probe_file;
+
+pub fn scan_directory(
+ path: &Path,
+ unknown_length_start: impl FnOnce() -> T,
+ unknown_length_end: impl FnOnce(T),
+ fixed_length_start: impl FnOnce(usize) -> T,
+ fixed_length_update: impl Fn(&mut T),
+ fixed_length_end: impl FnOnce(T),
+ print_fn: impl Fn(&T, &str),
+) -> (Vec, u64) {
+ let spinner = unknown_length_start();
+ let files: Vec<_> = WalkDir::new(path)
+ .into_iter()
+ .filter_map(Result::ok)
+ .filter(|e| e.file_type().is_file())
+ .filter(|e| {
+ let is_mkv = e
+ .path()
+ .extension()
+ .unwrap_or_default()
+ .eq_ignore_ascii_case("mkv");
+ let is_mp4 = e
+ .path()
+ .extension()
+ .unwrap_or_default()
+ .eq_ignore_ascii_case("mp4");
+ is_mkv || is_mp4
+ })
+ .collect();
+ unknown_length_end(spinner);
+
+ let mut progress = fixed_length_start(files.len());
+ let mut total_byte_size = 0u64;
+ let files: Vec<_> = files
+ .iter()
+ .filter_map(|entry| {
+ let file = match probe_file(entry) {
+ Ok(file) => Some(file),
+ Err(err) => {
+ print_fn(&progress, &format!("{:?}", err));
+ None
+ }
+ };
+ let size = file.as_ref().map(|r| r.byte_size).unwrap_or_default();
+ total_byte_size = total_byte_size.saturating_add(size);
+ fixed_length_update(&mut progress);
+ file
+ })
+ .collect();
+ fixed_length_end(progress);
+ (files, total_byte_size)
+}
diff --git a/crates/cli/Cargo.toml b/crates/cli/Cargo.toml
index 8be4602..2ae07ad 100644
--- a/crates/cli/Cargo.toml
+++ b/crates/cli/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "flix-cli"
-version = "0.0.18"
+version = "0.0.19"
license-file.workspace = true
description = "CLI for interacting with a flix database"
diff --git a/crates/cli/README.md b/crates/cli/README.md
index 7098d7d..59bd92b 100644
--- a/crates/cli/README.md
+++ b/crates/cli/README.md
@@ -5,6 +5,7 @@
CLI for interacting with a flix database
## Commands
+
```sh
cargo run -- init
cargo run -- add tmdb movie
diff --git a/crates/cli/src/cli/mod.rs b/crates/cli/src/cli/mod.rs
index 7352aa3..3d65dc1 100644
--- a/crates/cli/src/cli/mod.rs
+++ b/crates/cli/src/cli/mod.rs
@@ -10,7 +10,12 @@ pub mod tmdb;
#[command(version, about, long_about = None)]
pub struct Cli {
/// Use a custom config file
- #[arg(short, long, value_name = "FILE", default_value = "~/.flix")]
+ #[arg(
+ short,
+ long,
+ value_name = "FILE",
+ default_value = "~/.config/flix/config.toml"
+ )]
config: PathBuf,
/// Use a custom cache file
diff --git a/crates/cli/src/main.rs b/crates/cli/src/main.rs
index 4bc95f2..430e74f 100644
--- a/crates/cli/src/main.rs
+++ b/crates/cli/src/main.rs
@@ -30,11 +30,9 @@ async fn main() -> Result<()> {
let database_path = cli.database_path()?;
- let client = Client::new(
- tmdb::Config::new(config.tmdb().bearer_token().to_owned()),
- Rc::new(RedbCache::new(cli.cache_path())?),
- CachePolicy::Full,
- );
+ let config = tmdb::Config::new(config.tmdb().bearer_token().to_owned());
+ let cache = Rc::new(RedbCache::new(cli.cache_path())?);
+ let client = Client::new(config, cache, CachePolicy::Full);
if cli.trace {
tracing_subscriber::fmt()
diff --git a/crates/cli/src/run/flix.rs b/crates/cli/src/run/flix.rs
index 22e5750..5733ac4 100644
--- a/crates/cli/src/run/flix.rs
+++ b/crates/cli/src/run/flix.rs
@@ -48,12 +48,12 @@ pub async fn add(
})
.await;
- let flix_id = match result {
- Ok(id) => id,
+ match result {
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
- println!("Created Collection: {} [{}]", title, flix_id.into_raw());
+ println!("Created Collection: {}", title);
Ok(())
}
@@ -93,18 +93,12 @@ pub async fn add(
})
.await;
- let (flix_show, season_number, episode_number) = match result {
- Ok(id) => id,
+ match result {
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
- println!(
- "Created Episode: {} [{} S{} E{}]",
- title,
- flix_show.into_raw(),
- season_number,
- episode_number
- );
+ println!("Created Episode: {}", title);
Ok(())
}
diff --git a/crates/cli/src/run/tmdb.rs b/crates/cli/src/run/tmdb.rs
index 92741a0..cdfb03a 100644
--- a/crates/cli/src/run/tmdb.rs
+++ b/crates/cli/src/run/tmdb.rs
@@ -83,12 +83,12 @@ pub async fn add(
})
.await;
- let flix_id = match result {
- Ok(id) => id,
+ match result {
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
- println!("Created Collection: {} [{}]", title, flix_id.into_raw());
+ println!("Created Collection: {}", title);
Ok(())
}
@@ -151,17 +151,12 @@ pub async fn add(
})
.await;
- let flix_id = match result {
- Ok(id) => id,
+ match result {
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
- println!(
- "Created Movie: {} ({}) [{}]",
- title,
- year,
- flix_id.into_raw(),
- );
+ println!("Created Movie: {} ({})", title, year);
Ok(())
}
@@ -215,18 +210,22 @@ pub async fn add(
let mut season_episodes = Vec::new();
for episode in 1..=number_of_episodes {
let episode = EpisodeNumber::new(episode);
- let Ok(episode) = client
+ let episode = match client
.episodes()
.get_details(id, season.season_number, episode, None)
.await
- else {
- eprintln!(
- "skipping episode ({}, {}, {})",
- id.into_raw(),
- season.season_number,
- episode
- );
- break;
+ {
+ Ok(value) => value,
+ Err(err) => {
+ eprintln!(
+ "skipping episode ({}, {}, {}) - {}",
+ id.into_raw(),
+ season.season_number,
+ episode,
+ err
+ );
+ break;
+ }
};
season_episodes.push(episode);
}
@@ -329,17 +328,12 @@ pub async fn add(
})
.await;
- let flix_id = match result {
- Ok(id) => id,
+ match result {
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
- println!(
- "Created Show: {} ({}) [{}]",
- title,
- year,
- flix_id.into_raw()
- );
+ println!("Created Show: {} ({})", title, year);
Ok(())
}
@@ -380,18 +374,22 @@ pub async fn add(
for episode in 1..=number_of_episodes {
let episode = EpisodeNumber::new(episode);
- let Ok(episode) = client
+ let episode = match client
.episodes()
.get_details(id, season.season_number, episode, None)
.await
- else {
- eprintln!(
- "skipping episode ({}, {}, {})",
- id.into_raw(),
- season.season_number,
- episode
- );
- break;
+ {
+ Ok(value) => value,
+ Err(err) => {
+ eprintln!(
+ "skipping episode ({}, {}, {}) - {}",
+ id.into_raw(),
+ season.season_number,
+ episode,
+ err
+ );
+ break;
+ }
};
episodes.push(episode);
}
@@ -451,7 +449,7 @@ pub async fn add(
.await;
match result {
- Ok(_) => (),
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
@@ -541,7 +539,7 @@ pub async fn add(
.await;
match result {
- Ok(_) => (),
+ Ok(_) => {}
Err(TransactionError::Connection(err)) => Err(err)?,
Err(TransactionError::Transaction(err)) => Err(err)?,
};
@@ -637,7 +635,7 @@ pub async fn update(client: Client, db: &DatabaseConnection, command: Command) -
// Err(TransactionError::Connection(err)) => Err(err)?,
// Err(TransactionError::Transaction(err)) => Err(err)?,
// };
- // println!("Created Collection: {} [{}]", title, flix_id.into_raw());
+ // println!("Created Collection: {}", title, flix_id.into_raw());
// Ok(())
// }
@@ -706,7 +704,7 @@ pub async fn update(client: Client, db: &DatabaseConnection, command: Command) -
// Err(TransactionError::Transaction(err)) => Err(err)?,
// };
// println!(
- // "Created Movie: {} ({}) [{}]",
+ // "Created Movie: {} ({})",
// title,
// year,
// flix_id.into_raw(),
@@ -884,7 +882,7 @@ pub async fn update(client: Client, db: &DatabaseConnection, command: Command) -
// Err(TransactionError::Transaction(err)) => Err(err)?,
// };
// println!(
- // "Created Show: {} ({}) [{}]",
+ // "Created Show: {} ({})",
// title,
// year,
// flix_id.into_raw()
diff --git a/crates/db/Cargo.toml b/crates/db/Cargo.toml
index d6bd9cd..4c94051 100644
--- a/crates/db/Cargo.toml
+++ b/crates/db/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "flix-db"
-version = "0.0.18"
+version = "0.0.19"
license-file.workspace = true
description = "Types for storing persistent data about media"
diff --git a/crates/db/src/entity/content.rs b/crates/db/src/entity/content.rs
index 8ef625d..a0da637 100644
--- a/crates/db/src/entity/content.rs
+++ b/crates/db/src/entity/content.rs
@@ -4,6 +4,7 @@
pub mod libraries {
use flix_model::id::LibraryId;
+ use seamantic::model::duration::Seconds;
use seamantic::model::path::PathBytes;
use chrono::{DateTime, Utc};
@@ -20,7 +21,9 @@ pub mod libraries {
/// The library's directory
pub directory: PathBytes,
/// The library's last scan data
- pub last_scan: Option>,
+ pub last_scan_date: Option>,
+ /// The library's last scan duration
+ pub last_scan_duration: Option,
/// Collections that are part of this library
#[sea_orm(has_many)]
@@ -415,7 +418,8 @@ pub mod test {
$crate::entity::content::libraries::ActiveModel {
id: Set(::flix_model::id::LibraryId::from_raw($id)),
directory: Set(::std::path::PathBuf::new().into()),
- last_scan: Set(None),
+ last_scan_date: Set(None),
+ last_scan_duration: Set(None),
}
.insert($db)
.await
@@ -522,10 +526,13 @@ pub mod test {
#[cfg(test)]
mod tests {
+ use core::time::Duration;
use std::path::Path;
use flix_model::id::{CollectionId, LibraryId, MovieId, ShowId};
+ use seamantic::model::duration::Seconds;
+
use chrono::NaiveDate;
use sea_orm::ActiveValue::{NotSet, Set};
use sea_orm::entity::prelude::*;
@@ -566,7 +573,8 @@ mod tests {
assert_eq!(model.id, LibraryId::from_raw($id));
assert_eq!(model.directory, Path::new(concat!("L Directory ", $id)).to_owned().into());
- assert_eq!(model.last_scan, noneable!(last_scan, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?));
+ assert_eq!(model.last_scan_date, noneable!(last_scan_date, NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc() $(, $($skip),+)?));
+ assert_eq!(model.last_scan_duration, noneable!(last_scan_duration, Seconds(Duration::from_secs($id)) $(, $($skip),+)?));
};
($db:expr, $id:literal, $error:ident $(; $($skip:ident),+)?) => {
let model = assert_library!(@insert, $db, $id $(; $($skip),+)?)
@@ -578,7 +586,8 @@ mod tests {
super::libraries::ActiveModel {
id: notsettable!(id, LibraryId::from_raw($id) $(, $($skip),+)?),
directory: notsettable!(directory, Path::new(concat!("L Directory ", $id)).to_owned().into() $(, $($skip),+)?),
- last_scan: notsettable!(last_scan, Some(NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc()) $(, $($skip),+)?),
+ last_scan_date: notsettable!(last_scan_date, Some(NaiveDate::from_yo_opt($id, 1).expect("from_yo_opt").and_hms_opt(0, 0, 0).expect("and_hms_opt").and_utc()) $(, $($skip),+)?),
+ last_scan_duration: notsettable!(last_scan_duration, Some(Seconds(Duration::from_secs($id))) $(, $($skip),+)?),
}.insert($db).await
};
}
@@ -588,7 +597,8 @@ mod tests {
assert_library!(&db, 2, Success);
assert_library!(&db, 3, Success; id);
assert_library!(&db, 4, NotNullViolation; directory);
- assert_library!(&db, 5, Success; last_scan);
+ assert_library!(&db, 5, Success; last_scan_date);
+ assert_library!(&db, 6, Success; last_scan_duration);
}
#[tokio::test]
diff --git a/crates/flix/Cargo.toml b/crates/flix/Cargo.toml
index 0b9db7b..8fc3ac4 100644
--- a/crates/flix/Cargo.toml
+++ b/crates/flix/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "flix"
-version = "0.0.18"
+version = "0.0.19"
license-file.workspace = true
description = "Mechanisms for interacting with flix media"
diff --git a/crates/fs/Cargo.toml b/crates/fs/Cargo.toml
index ecb2800..8cba725 100644
--- a/crates/fs/Cargo.toml
+++ b/crates/fs/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "flix-fs"
-version = "0.0.18"
+version = "0.0.19"
license-file.workspace = true
description = "Filesystem scanner for flix media"
diff --git a/crates/fs/src/scanner/generic.rs b/crates/fs/src/scanner/generic.rs
index 3456b79..c114300 100644
--- a/crates/fs/src/scanner/generic.rs
+++ b/crates/fs/src/scanner/generic.rs
@@ -72,11 +72,12 @@ impl From for Scanner {
impl Scanner {
/// Helper function for stripping allowed numerical prefixes for sorting ("01 - ")
- fn strip_numeric_prefix(mut s: &str) -> &str {
+ fn strip_numeric_prefix(original: &str) -> &str {
+ let mut s = original;
while let Some('0'..='9') = s.chars().next() {
s = &s[1..]
}
- s.strip_prefix(" - ").unwrap_or(s)
+ s.strip_prefix(" - ").unwrap_or(original)
}
/// Detect the type of a folder and call the correct scanner. Use
diff --git a/crates/fs/src/scanner/mod.rs b/crates/fs/src/scanner/mod.rs
index 334ce73..aeddde4 100644
--- a/crates/fs/src/scanner/mod.rs
+++ b/crates/fs/src/scanner/mod.rs
@@ -27,6 +27,16 @@ pub enum MediaRef {
Slug(String),
}
+impl MediaRef {
+ /// Get the slug if it exists
+ pub fn into_slug(self) -> Option {
+ match self {
+ MediaRef::Id(_) => None,
+ MediaRef::Slug(slug) => Some(slug),
+ }
+ }
+}
+
/// A scanned collection
#[derive(Debug)]
pub struct CollectionScan {
diff --git a/crates/model/Cargo.toml b/crates/model/Cargo.toml
index 96f4956..62018ad 100644
--- a/crates/model/Cargo.toml
+++ b/crates/model/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "flix-model"
-version = "0.0.18"
+version = "0.0.19"
license-file.workspace = true
description = "Core types for flix data"
diff --git a/crates/model/src/text.rs b/crates/model/src/text.rs
index c3939a5..b3012c3 100644
--- a/crates/model/src/text.rs
+++ b/crates/model/src/text.rs
@@ -12,22 +12,25 @@ fn split_normalized_words(input: &str) -> impl Iterator- {
panic!("Input is not ASCII: {input}");
}
- input.split_ascii_whitespace().map(|s| {
- let chars = s
- .chars()
- .filter(|c| c.is_ascii_alphanumeric() || *c == '-')
- .map(|c| c.to_ascii_lowercase());
+ input
+ .split_ascii_whitespace()
+ .map(|s| {
+ let chars = s
+ .chars()
+ .filter(|c| c.is_ascii_alphanumeric() || *c == '-')
+ .map(|c| c.to_ascii_lowercase());
- if s.len() > 4
- && s.len().is_multiple_of(2)
- && chars.clone().tuples().all(|(l, r)| l != '.' && r == '.')
- {
- // Collapse acronym
- chars.tuples().map(|(l, _)| l).collect()
- } else {
- chars.collect()
- }
- })
+ if s.len() > 4
+ && s.len().is_multiple_of(2)
+ && chars.clone().tuples().all(|(l, r)| l != '.' && r == '.')
+ {
+ // Collapse acronym
+ chars.tuples().map(|(l, _)| l).collect()
+ } else {
+ chars.collect()
+ }
+ })
+ .filter(|part: &String| !part.is_empty())
}
fn split_leading_article>(iter: I) -> (Option, Peekable) {
diff --git a/crates/tmdb/Cargo.toml b/crates/tmdb/Cargo.toml
index 6365e15..d82b763 100644
--- a/crates/tmdb/Cargo.toml
+++ b/crates/tmdb/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "flix-tmdb"
-version = "0.0.18"
+version = "0.0.19"
license-file.workspace = true
description = "Clients and models for fetching data from TMDB"
diff --git a/crates/tmdb/src/model/episode.rs b/crates/tmdb/src/model/episode.rs
index 866c7e5..40a95b4 100644
--- a/crates/tmdb/src/model/episode.rs
+++ b/crates/tmdb/src/model/episode.rs
@@ -3,8 +3,9 @@ use core::time::Duration;
use flix_model::numbers::EpisodeNumber;
use chrono::NaiveDate;
+use url::Url;
-use super::duration_from_minutes;
+use super::{duration_from_minutes, still_url_from_path};
/// A deserialized Episode from the TMDB API
#[derive(Debug, Clone, serde::Deserialize)]
@@ -18,7 +19,10 @@ pub struct Episode {
pub overview: String,
/// The episode's air date
pub air_date: NaiveDate,
- /// The movie's runtime
+ /// The episode's runtime
#[serde(deserialize_with = "duration_from_minutes")]
pub runtime: Duration,
+ /// The episode's still path
+ #[serde(deserialize_with = "still_url_from_path")]
+ pub still_path: Option,
}
diff --git a/crates/tmdb/src/model/mod.rs b/crates/tmdb/src/model/mod.rs
index 45e50db..d7f2bc5 100644
--- a/crates/tmdb/src/model/mod.rs
+++ b/crates/tmdb/src/model/mod.rs
@@ -1,8 +1,10 @@
//! Deserializable types from the TMDB API
+use core::str::FromStr;
use core::time::Duration;
use serde::{Deserialize, Deserializer};
+use url::Url;
pub mod id;
@@ -22,6 +24,24 @@ fn duration_from_minutes<'de, D>(deserializer: D) -> Result
where
D: Deserializer<'de>,
{
- let minutes = u64::deserialize(deserializer)?;
+ let minutes = u64::deserialize(deserializer).unwrap_or(0);
Ok(Duration::from_secs(minutes.saturating_mul(60)))
}
+
+fn still_url_from_path<'de, D>(deserializer: D) -> Result