diff --git a/.gitignore b/.gitignore index 8eecc97..1e36f98 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,7 @@ src-tauri/binaries/ .env .env.* *.log + +# Local progress tracking +.beads +.beads/ diff --git a/Cargo.lock b/Cargo.lock index f5f5121..472ae1a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -154,6 +154,45 @@ version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d3fb67a6e08acf24fdeccbac2cb6ac4305825bd1f117462e0e6f2f193345ad56" +[[package]] +name = "asn1-rs" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f43a50ac4fdca5df8e885c21b835997f0a1cdee65494a6847694a98652d9d8" +dependencies = [ + "asn1-rs-derive", + "asn1-rs-impl", + "displaydoc", + "nom", + "num-traits", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + +[[package]] +name = "asn1-rs-derive" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3109e49b1e4909e9db6515a30c633684d68cdeaa252f215214cb4fa1a5bfee2c" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", + "synstructure", +] + +[[package]] +name = "asn1-rs-impl" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b18050c2cd6fe86c3a76584ef5e0baf286d038cda203eb6223df2cc413565f7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "async-trait" version = "0.1.89" @@ -266,6 +305,25 @@ dependencies = [ "tokio", ] +[[package]] +name = "bao-tree" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06384416b1825e6e04fde63262fda2dc408f5b64c02d04e0d8b70ae72c17a52b" +dependencies = [ + "blake3", + "bytes", + "futures-lite", + "genawaiter", + "iroh-io", + "positioned-io", + "range-collections", + "self_cell", + "serde", + "smallvec", + "tokio", +] + [[package]] name = "base16ct" version = "1.0.0" @@ -290,13 +348,19 @@ version = "1.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06" +[[package]] +name = "binary-merge" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597bb81c80a54b6a4381b23faba8d7774b144c94cbd1d6fe3f1329bd776554ab" + [[package]] name = "bit-set" version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3" dependencies = [ - "bit-vec", + "bit-vec 0.8.0", ] [[package]] @@ -305,6 +369,15 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" +[[package]] +name = "bit-vec" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b71798fca2c1fe1086445a7258a4bc81e6e49dcd24c8d0dd9a1e57395b603f51" +dependencies = [ + "serde", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -1034,6 +1107,20 @@ dependencies = [ "zeroize", ] +[[package]] +name = "der-parser" +version = "10.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07da5016415d5a3c4dd39b11ed26f915f52fc4e0dc197d87908bc916e51bc1a6" +dependencies = [ + "asn1-rs", + "displaydoc", + "nom", + "num-bigint", + "num-traits", + "rusticata-macros", +] + [[package]] name = "deranged" version = "0.5.8" @@ -1381,6 +1468,18 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" +[[package]] +name = "fastbloom" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef975e30683b2d965054bb0a836f8973857c4ebf6acf274fe46617cd285060d8" +dependencies = [ + "foldhash 0.2.0", + "libm", + "portable-atomic", + "siphasher", +] + [[package]] name = "fastrand" version = "2.4.1" @@ -1418,6 +1517,12 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" +[[package]] +name = "fixedbitset" +version = "0.5.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d674e81391d1e1ab681a28d99df07927c6d4aa5b027d7da16ba32d1d21ecd99" + [[package]] name = "flate2" version = "1.1.9" @@ -1526,6 +1631,19 @@ dependencies = [ "futures-sink", ] +[[package]] +name = "futures-concurrency" +version = "7.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "175cd8cca9e1d45b87f18ffa75088f2099e3c4fe5e2f83e42de112560bea8ea6" +dependencies = [ + "fixedbitset", + "futures-core", + "futures-lite", + "pin-project", + "smallvec", +] + [[package]] name = "futures-core" version = "0.3.32" @@ -1701,6 +1819,37 @@ dependencies = [ "x11", ] +[[package]] +name = "genawaiter" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c86bd0361bcbde39b13475e6e36cb24c329964aa2611be285289d1e4b751c1a0" +dependencies = [ + "futures-core", + "genawaiter-macro", + "genawaiter-proc-macro", + "proc-macro-hack", +] + +[[package]] +name = "genawaiter-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b32dfe1fdfc0bbde1f22a5da25355514b5e450c33a6af6770884c8750aedfbc" + +[[package]] +name = "genawaiter-proc-macro" +version = "0.99.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "784f84eebc366e15251c4a8c3acee82a6a6f427949776ecb88377362a9621738" +dependencies = [ + "proc-macro-error 0.4.12", + "proc-macro-hack", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "generator" version = "0.8.9" @@ -1838,7 +1987,7 @@ checksum = "0bb0228f477c0900c880fd78c8759b95c7636dbd7842707f49e132378aa2acdc" dependencies = [ "heck 0.4.1", "proc-macro-crate 2.0.2", - "proc-macro-error", + "proc-macro-error 1.0.4", "proc-macro2", "quote", "syn 2.0.118", @@ -1929,7 +2078,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "52ff3c5b21f14f0736fed6dcfc0bfb4225ebf5725f3c0209edeec181e4d73e9d" dependencies = [ "proc-macro-crate 1.3.1", - "proc-macro-error", + "proc-macro-error 1.0.4", "proc-macro2", "quote", "syn 2.0.118", @@ -2447,6 +2596,15 @@ dependencies = [ "generic-array", ] +[[package]] +name = "inplace-vec-builder" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf64c2edc8226891a71f127587a2861b132d2b942310843814d5001d99a1d307" +dependencies = [ + "smallvec", +] + [[package]] name = "ipconfig" version = "0.3.4" @@ -2539,6 +2697,46 @@ dependencies = [ "zeroize", ] +[[package]] +name = "iroh-blobs" +version = "0.103.0" +source = "git+https://github.com/n0-computer/iroh-blobs?branch=main#e82cbdcbdac9a78033174aad55e3199b2cf4c0dc" +dependencies = [ + "arrayvec", + "bao-tree", + "bytes", + "cfg_aliases", + "chrono", + "constant_time_eq", + "data-encoding", + "derive_more", + "genawaiter", + "getrandom 0.4.3", + "hex", + "iroh", + "iroh-base", + "iroh-io", + "iroh-metrics", + "iroh-tickets", + "iroh-util", + "irpc", + "n0-error", + "n0-future", + "nested_enum_utils", + "noq", + "postcard", + "rand", + "range-collections", + "redb", + "ref-cast", + "reflink-copy", + "self_cell", + "serde", + "smallvec", + "tokio", + "tracing", +] + [[package]] name = "iroh-dns" version = "1.0.2" @@ -2563,6 +2761,45 @@ dependencies = [ "url", ] +[[package]] +name = "iroh-gossip" +version = "0.101.0" +source = "git+https://github.com/n0-computer/iroh-gossip?branch=main#2ce78afe09d89d41d123f28eac19bdc831609cc8" +dependencies = [ + "blake3", + "bytes", + "data-encoding", + "derive_more", + "futures-concurrency", + "hex", + "indexmap 2.14.0", + "iroh", + "iroh-base", + "iroh-metrics", + "irpc", + "n0-error", + "n0-future", + "postcard", + "rand", + "serde", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "iroh-io" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0a5feb781017b983ff1b155cd1faf8174da2acafd807aa482876da2d7e6577a" +dependencies = [ + "bytes", + "futures-lite", + "pin-project", + "smallvec", + "tokio", +] + [[package]] name = "iroh-metrics" version = "1.0.1" @@ -2650,6 +2887,53 @@ dependencies = [ "serde", ] +[[package]] +name = "iroh-util" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20e41eb982f15230c55f0a70a74a514360e1f565b07861924fd0e8db172b3d00" +dependencies = [ + "derive_more", + "iroh", + "n0-error", + "n0-future", + "tokio", + "tracing", +] + +[[package]] +name = "irpc" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3623d6ff582b415904b29bbe6ebcb4a4f9a262ccdee05a45fdd003ef0950c386" +dependencies = [ + "futures-buffered", + "futures-util", + "irpc-derive", + "n0-error", + "n0-future", + "noq", + "postcard", + "rcgen", + "rustls", + "serde", + "smallvec", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "irpc-derive" +version = "0.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c254013736de16472140d26904e6ac98e8f3887284dcf4af40f88c77411b56" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "is-docker" version = "0.2.0" @@ -2881,6 +3165,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "libm" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981" + [[package]] name = "libredox" version = "0.1.18" @@ -3009,6 +3299,12 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + [[package]] name = "miniz_oxide" version = "0.8.9" @@ -3074,6 +3370,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c37e81176a83a77d2514528b91bdafc70ef88aab428f0e1b91aebb8d99888895" dependencies = [ + "anyhow", "n0-error-macros", "spez", ] @@ -3151,6 +3448,18 @@ dependencies = [ "jni-sys 0.3.1", ] +[[package]] +name = "nested_enum_utils" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d5475271bdd36a4a2769eac1ef88df0f99428ea43e52dfd8b0ee5cb674695f" +dependencies = [ + "proc-macro-crate 3.5.0", + "proc-macro2", + "quote", + "syn 2.0.118", +] + [[package]] name = "netdev" version = "0.45.0" @@ -3269,6 +3578,16 @@ version = "1.0.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086" +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + [[package]] name = "noq" version = "1.0.1" @@ -3301,6 +3620,7 @@ dependencies = [ "bytes", "derive_more", "enum-assoc", + "fastbloom", "getrandom 0.4.3", "identity-hash", "lru-slab", @@ -3310,6 +3630,7 @@ dependencies = [ "rustc-hash", "rustls", "rustls-pki-types", + "rustls-platform-verifier", "slab", "sorted-index-buffer", "thiserror 2.0.18", @@ -3340,6 +3661,16 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "num-bigint" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c89e69e7e0f03bea5ef08013795c25018e101932225a656383bd384495ecc367" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-conv" version = "0.2.2" @@ -3357,6 +3688,15 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "num-integer" +version = "0.1.46" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" +dependencies = [ + "num-traits", +] + [[package]] name = "num-traits" version = "0.2.19" @@ -3693,6 +4033,15 @@ dependencies = [ "objc2-foundation", ] +[[package]] +name = "oid-registry" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12f40cff3dde1b6087cc5d5f5d4d65712f34016a03ed60e9c08dcc392736b5b7" +dependencies = [ + "asn1-rs", +] + [[package]] name = "once_cell" version = "1.21.4" @@ -3823,6 +4172,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d30c53c26bc5b31a98cd02d20f25a7c8567146caf63ed593a9d87b2775291be" +dependencies = [ + "base64 0.22.1", + "serde_core", +] + [[package]] name = "pem-rfc7468" version = "1.0.0" @@ -4032,6 +4391,16 @@ dependencies = [ "url", ] +[[package]] +name = "positioned-io" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4ec4b80060f033312b99b6874025d9503d2af87aef2dd4c516e253fbfcdada7" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "postcard" version = "1.1.3" @@ -4118,19 +4487,45 @@ dependencies = [ "toml_edit 0.25.12+spec-1.1.0", ] +[[package]] +name = "proc-macro-error" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18f33027081eba0a6d8aba6d1b1c3a3be58cbb12106341c2d5759fcd9b5277e7" +dependencies = [ + "proc-macro-error-attr 0.4.12", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + [[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-macro-error-attr 1.0.4", "proc-macro2", "quote", "syn 1.0.109", "version_check", ] +[[package]] +name = "proc-macro-error-attr" +version = "0.4.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a5b4b77fdb63c1eca72173d68d24501c54ab1269409f6b672c85deb18af69de" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", + "syn-mid", + "version_check", +] + [[package]] name = "proc-macro-error-attr" version = "1.0.4" @@ -4142,6 +4537,12 @@ dependencies = [ "version_check", ] +[[package]] +name = "proc-macro-hack" +version = "0.5.20+deprecated" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" + [[package]] name = "proc-macro2" version = "1.0.106" @@ -4279,6 +4680,19 @@ dependencies = [ "rand_core 0.10.1", ] +[[package]] +name = "range-collections" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "861706ea9c4aded7584c5cd1d241cec2ea7f5f50999f236c22b65409a1f1a0d0" +dependencies = [ + "binary-merge", + "inplace-vec-builder", + "ref-cast", + "serde", + "smallvec", +] + [[package]] name = "rau-studio" version = "0.1.19" @@ -4289,6 +4703,8 @@ dependencies = [ "chrono", "cpal", "iroh", + "iroh-blobs", + "iroh-gossip", "iroh-tickets", "regex", "reqwest", @@ -4313,6 +4729,29 @@ version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "20675572f6f24e9e76ef639bc5552774ed45f1c30e2951e1e99c59888861c539" +[[package]] +name = "rcgen" +version = "0.14.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57f6d249aad744e274e682777a50283a225a32705394ee6d5fcc01efa25e4055" +dependencies = [ + "pem", + "ring", + "rustls-pki-types", + "time", + "x509-parser", + "yasna", +] + +[[package]] +name = "redb" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e925444704b5f17d32bf42f5b6e2df050bceebc3dcd6e71cc73dafe8092e839" +dependencies = [ + "libc", +] + [[package]] name = "redox_syscall" version = "0.5.18" @@ -4353,6 +4792,18 @@ dependencies = [ "syn 2.0.118", ] +[[package]] +name = "reflink-copy" +version = "0.1.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9dd7ab4af0363d5ccfd2838d782a28196cf32a5cc2e4fe3c5dc83f2be588b8b" +dependencies = [ + "cfg-if", + "libc", + "rustix", + "windows 0.61.3", +] + [[package]] name = "regex" version = "1.13.0" @@ -4497,6 +4948,15 @@ dependencies = [ "semver", ] +[[package]] +name = "rusticata-macros" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faf0c4a6ece9950b9abdb62b1cfcf2a68b3b67a10ba445b3bb85be2a293d0632" +dependencies = [ + "nom", +] + [[package]] name = "rustix" version = "1.1.4" @@ -4738,6 +5198,12 @@ dependencies = [ "smallvec", ] +[[package]] +name = "self_cell" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" + [[package]] name = "semver" version = "1.0.28" @@ -5083,6 +5549,9 @@ name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" +dependencies = [ + "serde", +] [[package]] name = "socket2" @@ -5265,6 +5734,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" dependencies = [ "proc-macro2", + "quote", "unicode-ident", ] @@ -5279,6 +5749,17 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "syn-mid" +version = "0.5.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea305d57546cc8cd04feb14b62ec84bf17f50e3f7b12560d7bfa9265f39d9ed" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "sync_wrapper" version = "1.0.2" @@ -7249,6 +7730,24 @@ dependencies = [ "pkg-config", ] +[[package]] +name = "x509-parser" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d43b0f71ce057da06bc0851b23ee24f3f86190b07203dd8f567d0b706a185202" +dependencies = [ + "asn1-rs", + "data-encoding", + "der-parser", + "lazy_static", + "nom", + "oid-registry", + "ring", + "rusticata-macros", + "thiserror 2.0.18", + "time", +] + [[package]] name = "xml-rs" version = "0.8.28" @@ -7264,6 +7763,16 @@ dependencies = [ "xml-rs", ] +[[package]] +name = "yasna" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5f6765e852b9b4dc8e2a76843e4d64d1cea8e79bcde0b6901aea8e7c7f08282" +dependencies = [ + "bit-vec 0.9.1", + "time", +] + [[package]] name = "yoke" version = "0.8.3" diff --git a/scripts/prepare-ffmpeg-sidecars.sh b/scripts/prepare-ffmpeg-sidecars.sh index 37d8446..0628c1a 100755 --- a/scripts/prepare-ffmpeg-sidecars.sh +++ b/scripts/prepare-ffmpeg-sidecars.sh @@ -321,6 +321,111 @@ if [[ "$(rustc --print host-tuple)" != "$target_triple" ]]; then configure_flags+=("--enable-cross-compile") fi +if ! command -v pkg-config >/dev/null 2>&1; then + echo "pkg-config not found on system; creating a local Python fallback wrapper..." + local_pkg_config_bin="$cache_dir/bin" + local_pkg_config="$local_pkg_config_bin/pkg-config" + mkdir -p "$local_pkg_config_bin" + cat << 'EOF' > "$local_pkg_config" +#!/usr/bin/env python3 +import sys +import os +import re + +def parse_pc_file(filepath): + variables = {} + fields = {} + with open(filepath, 'r') as f: + for line in f: + line = line.split('#')[0].strip() + if not line: + continue + var_match = re.match(r'^([a-zA-Z0-9_]+)=(.*)$', line) + if var_match: + name, val = var_match.groups() + def repl(m): + return variables.get(m.group(1), '') + prev_val = "" + while val != prev_val: + prev_val = val + val = re.sub(r'\$\{([a-zA-Z0-9_]+)\}', repl, val) + variables[name] = val + continue + field_match = re.match(r'^([a-zA-Z0-9_\.-]+):[ \t]*(.*)$', line) + if field_match: + key, val = field_match.groups() + def repl(m): + return variables.get(m.group(1), '') + prev_val = "" + while val != prev_val: + prev_val = val + val = re.sub(r'\$\{([a-zA-Z0-9_]+)\}', repl, val) + fields[key.lower()] = val + return variables, fields + +def main(): + args = sys.argv[1:] + if '--version' in args: + print("0.29.2") + sys.exit(0) + exists = False + cflags = False + libs = False + static = False + modversion = False + pkg = None + for arg in args: + if arg == '--exists': + exists = True + elif arg == '--cflags': + cflags = True + elif arg == '--libs': + libs = True + elif arg == '--static': + static = True + elif arg == '--modversion': + modversion = True + elif arg.startswith('-'): + pass + else: + pkg = arg + if not pkg: + sys.exit(1) + pkg_config_path = os.environ.get('PKG_CONFIG_PATH', '') + paths = pkg_config_path.split(':') + found_pc = None + for p in paths: + pc_file = os.path.join(p, f"{pkg}.pc") + if os.path.isfile(pc_file): + found_pc = pc_file + break + if not found_pc: + sys.stderr.write(f"Package {pkg} not found in PKG_CONFIG_PATH\n") + sys.exit(1) + if exists: + sys.exit(0) + variables, fields = parse_pc_file(found_pc) + outputs = [] + if modversion: + outputs.append(fields.get('version', '')) + if cflags: + outputs.append(fields.get('cflags', '')) + if libs: + lib_str = fields.get('libs', '') + if static: + lib_private = fields.get('libs.private', '') + if lib_private: + lib_str = f"{lib_str} {lib_private}" + outputs.append(lib_str) + print(" ".join(outputs).strip()) + +if __name__ == '__main__': + main() +EOF + chmod +x "$local_pkg_config" + configure_flags+=("--pkg-config=$local_pkg_config") +fi + echo "Configuring FFmpeg $FFMPEG_VERSION for $target_triple..." configure_log="$build_dir/configure.log" if ! ( diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 690c6cc..6b2d172 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -19,6 +19,8 @@ chrono = { version = "0.4.45", features = ["serde"] } cpal = "0.18.1" base64 = "0.22.1" iroh = "1.0.2" +iroh-gossip = { git = "https://github.com/n0-computer/iroh-gossip", branch = "main" } +iroh-blobs = { git = "https://github.com/n0-computer/iroh-blobs", branch = "main" } iroh-tickets = "1.0.0" regex = "1.13.0" reqwest = { version = "0.13.4", default-features = false, features = ["blocking", "json", "query", "rustls"] } @@ -29,7 +31,7 @@ serde_json = "1" tauri = { version = "2", features = ["protocol-asset"] } tauri-plugin-dialog = "2" tauri-plugin-shell = "2" -tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time"] } +tokio = { version = "1", features = ["fs", "io-util", "macros", "rt-multi-thread", "sync", "time", "process"] } uuid = { version = "1.23.4", features = ["v4", "serde"] } walkdir = "2.5.0" zeroize = "1.8.2" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 51309e3..d12b317 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -1457,6 +1457,8 @@ fn is_inside_converted_folder(root: &Path, path: &Path) -> bool { pub fn run() { tauri::Builder::default() .manage(broadcast::BroadcastManager::default()) + .manage(p2p::stream::BroadcasterState { tracks: std::sync::Arc::new(tokio::sync::RwLock::new(Vec::new())) }) + .manage(p2p::stream::ListenerState { connection: std::sync::Arc::new(tokio::sync::Mutex::new(None)) }) .plugin(tauri_plugin_dialog::init()) .plugin(tauri_plugin_shell::init()) .invoke_handler(tauri::generate_handler![ @@ -1513,6 +1515,10 @@ pub fn run() { p2p::p2p_search_shared_files, p2p::catalog::p2p_remote_search, p2p::catalog::p2p_download_remote_file, + p2p::stream::scan_audio_directory, + p2p::stream::set_broadcaster_tracks, + p2p::stream::get_broadcaster_tracks, + p2p::stream::connect_to_broadcaster, p2p::chat::p2p_chat_list, p2p::chat::p2p_chat_send, enrichment::enrichment_providers, diff --git a/src-tauri/src/p2p.rs b/src-tauri/src/p2p.rs index 7c5f068..1a25374 100644 --- a/src-tauri/src/p2p.rs +++ b/src-tauri/src/p2p.rs @@ -18,7 +18,7 @@ use zeroize::Zeroizing; pub(crate) mod catalog; pub(crate) mod chat; pub(crate) mod network; - +pub(crate) mod stream; const DB_FILE: &str = "aifficator.sqlite3"; const IDENTITY_ROW_ID: i64 = 1; const IDENTITY_CIPHER_VERSION: i64 = 1; @@ -786,6 +786,13 @@ fn init_db(conn: &Connection) -> Result<(), String> { created_at TEXT NOT NULL ); + CREATE TABLE IF NOT EXISTS stream_favorites ( + id TEXT PRIMARY KEY, + endpoint_id TEXT NOT NULL, + track_id TEXT NOT NULL, + created_at TEXT NOT NULL + ); + CREATE INDEX IF NOT EXISTS p2p_shared_files_share_name_idx ON p2p_shared_files(share_id, name); CREATE INDEX IF NOT EXISTS p2p_peers_presence_idx diff --git a/src-tauri/src/p2p/network.rs b/src-tauri/src/p2p/network.rs index 85f13b9..33cab39 100644 --- a/src-tauri/src/p2p/network.rs +++ b/src-tauri/src/p2p/network.rs @@ -1,6 +1,7 @@ use super::{ - catalog, chat, mark_peer_seen, open_db, peer_endpoint_ticket, unlocked_network_identity, + catalog, chat, stream, mark_peer_seen, open_db, peer_endpoint_ticket, unlocked_network_identity, }; +use tauri::Manager; use chrono::Utc; use iroh::{ endpoint::{presets, Connection}, @@ -28,6 +29,8 @@ const NETWORK_EVENT: &str = "p2p-network-event"; struct NetworkRuntime { endpoint: Endpoint, router: Router, + _gossip: iroh_gossip::net::Gossip, + pub(crate) store: iroh_blobs::store::fs::FsStore, display_name: String, started_at: String, } @@ -101,8 +104,27 @@ pub(crate) struct PingResult { } fn network_state() -> &'static Mutex> { - static NETWORK: OnceLock>> = OnceLock::new(); - NETWORK.get_or_init(|| Mutex::new(None)) + static STATE: OnceLock>> = OnceLock::new(); + STATE.get_or_init(|| Mutex::new(None)) +} + +pub(crate) async fn local_endpoint_async() -> Result { + network_state() + .lock() + .await + .as_ref() + .map(|runtime| runtime.endpoint.clone()) + .ok_or_else(|| "La red P2P no está iniciada.".to_string()) +} + + +pub(crate) async fn local_store_async() -> Result { + network_state() + .lock() + .await + .as_ref() + .map(|runtime| runtime.store.clone()) + .ok_or_else(|| "La red P2P no está iniciada.".to_string()) } impl ProtocolHandler for DiagnosticProtocol { @@ -183,9 +205,11 @@ pub(crate) async fn p2p_network_status() -> Result { #[tauri::command] pub(crate) async fn p2p_network_start(app: AppHandle) -> Result { let identity = unlocked_network_identity()?; - let mut state = network_state().lock().await; - if let Some(runtime) = state.as_ref() { - return Ok(network_status(runtime)); + { + let state = network_state().lock().await; + if let Some(runtime) = state.as_ref() { + return Ok(network_status(runtime)); + } } let secret_key = SecretKey::from_bytes(&identity.secret); @@ -204,6 +228,15 @@ pub(crate) async fn p2p_network_start(app: AppHandle) -> Result Result Self { + Self { app, endpoint_id } + } +} + +impl fmt::Debug for StreamProtocol { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter + .debug_struct("StreamProtocol") + .field("endpoint_id", &self.endpoint_id) + .finish_non_exhaustive() + } +} + +#[derive(Serialize, Deserialize, Debug)] +#[serde(tag = "type")] +pub enum StreamRequest { + #[serde(rename = "list_tracks")] + ListTracks, +} + +impl ProtocolHandler for StreamProtocol { + async fn accept( + &self, + conn: Connection, + ) -> Result<(), AcceptError> { + let (mut send, mut recv) = conn.accept_bi().await?; + let app = self.app.clone(); + + tokio::spawn(async move { + let buf = match recv.read_to_end(1024 * 1024).await { + Ok(b) => b, + Err(_) => return, + }; + + if let Ok(req) = serde_json::from_slice::(&buf) { + match req { + StreamRequest::ListTracks => { + let state = app.state::(); + let tracks = state.tracks.read().await.clone(); + if let Ok(json) = serde_json::to_vec(&tracks) { + let _ = send.write_all(&json).await; + let _ = send.finish(); + } + } + } + } + }); + + Ok(()) + } +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct AudioTrack { + pub hash: String, + pub name: String, + pub path: String, +} + +#[tauri::command] +pub async fn scan_audio_directory(path: String) -> Result, String> { + let mut tracks = Vec::new(); + let root = std::path::Path::new(&path); + println!("Scanning directory: {:?}", root); + if !root.exists() || !root.is_dir() { + println!("Invalid directory: {:?}", root); + return Err("Invalid directory".into()); + } + + let store = local_store_async().await.map_err(|e| format!("Store no inicializado: {}", e))?; + println!("Store initialized successfully."); + + let entries: Vec<_> = WalkDir::new(root).follow_links(false).into_iter().collect(); + println!("WalkDir found {} total entries (including errors)", entries.len()); + + for entry_result in entries { + match entry_result { + Ok(entry) => { + if entry.file_type().is_file() { + let file_path = entry.path(); + println!("Checking file: {:?}", file_path); + if let Some(ext) = file_path.extension().and_then(|s| s.to_str()) { + let ext = ext.to_lowercase(); + if matches!(ext.as_str(), "mp3" | "wav" | "flac" | "aiff" | "m4a") { + let path_buf = file_path.to_path_buf(); + println!("Starting hashing for {:?}", path_buf); + + // Workaround: read bytes manually to avoid Iroh FsStore add_path deadlock + match tokio::fs::read(&path_buf).await { + Ok(bytes) => { + match store.blobs().add_bytes(bytes).await { + Ok(tag) => { + let hash = tag.hash.to_string(); + let name = entry.file_name().to_string_lossy().into_owned(); + println!("Added track: {} ({})", name, hash); + tracks.push(AudioTrack { + hash, + name, + path: file_path.to_string_lossy().into_owned(), + }); + } + Err(e) => { + println!("Failed to add bytes {:?}: {}", file_path, e); + } + } + } + Err(e) => { + println!("Failed to read file {:?}: {}", file_path, e); + } + } + } + } + } + } + Err(e) => { + println!("WalkDir entry error: {:?}", e); + } + } + } + + Ok(tracks) +} + +pub struct BroadcasterState { + pub tracks: Arc>>, +} + +#[tauri::command] +pub async fn set_broadcaster_tracks( + app: tauri::AppHandle, + tracks: Vec, +) -> Result<(), String> { + let state = app.state::(); + let mut current = state.tracks.write().await; + *current = tracks; + Ok(()) +} + +#[tauri::command] +pub async fn get_broadcaster_tracks(app: tauri::AppHandle) -> Result, String> { + let state = app.state::(); + let tracks = state.tracks.read().await.clone(); + Ok(tracks) +} + +pub struct ListenerState { + pub connection: Arc>>, +} + +#[tauri::command] +pub async fn connect_to_broadcaster( + app: tauri::AppHandle, + ticket: String, +) -> Result, String> { + let endpoint = local_endpoint_async().await.map_err(|e| e.to_string())?; + let ticket_parsed = ticket + .parse::() + .map_err(|e| format!("Invalid ticket: {e}"))?; + + let broadcaster_id = ticket_parsed.endpoint_addr().id; + + let conn = endpoint + .connect(ticket_parsed.endpoint_addr().clone(), STREAM_ALPN) + .await + .map_err(|e| e.to_string())?; + + let (mut send, mut recv) = conn.open_bi().await.map_err(|e| e.to_string())?; + + let req_json = serde_json::to_vec(&StreamRequest::ListTracks).unwrap(); + send.write_all(&req_json).await.map_err(|e| e.to_string())?; + send.finish().map_err(|e| e.to_string())?; + + let buf = recv.read_to_end(1024 * 1024).await.map_err(|e| e.to_string())?; + let tracks: Vec = serde_json::from_slice(&buf).map_err(|e| e.to_string())?; + + let state = app.state::(); + *state.connection.lock().await = Some(conn.clone()); + + tauri::async_runtime::spawn(async move { + let store = match local_store_async().await { + Ok(s) => s, + Err(_) => return, + }; + let downloader = store.downloader(&endpoint); + + if let Ok(listener) = TcpListener::bind("127.0.0.1:4000").await { + while let Ok((mut tcp_stream, _)) = listener.accept().await { + let store = store.clone(); + let downloader = downloader.clone(); + + tokio::spawn(async move { + let mut http_buf = [0u8; 1024]; + if let Ok(n) = tcp_stream.read(&mut http_buf).await { + let request_str = String::from_utf8_lossy(&http_buf[..n]); + if let Some(hash_str) = request_str.split("hash=").nth(1).and_then(|s| s.split_whitespace().next()) { + if let Ok(hash) = iroh_blobs::Hash::from_str(hash_str) { + let _ = downloader.download(hash, Some(broadcaster_id)).await; + + let mut blob_reader = store.blobs().reader(hash); + let total_size = match blob_reader.seek(SeekFrom::End(0)).await { + Ok(s) => s, + Err(_) => return, + }; + + let mut start_offset = 0; + let mut is_partial = false; + + if let Some(range_line) = request_str.lines().find(|l| l.to_lowercase().starts_with("range:")) { + if let Some(bytes_val) = range_line.split("bytes=").nth(1) { + if let Some(start_str) = bytes_val.split('-').next() { + if let Ok(s) = start_str.trim().parse::() { + start_offset = s; + is_partial = true; + } + } + } + } + + let headers = if is_partial { + format!("HTTP/1.1 206 Partial Content\r\n\ + Content-Type: audio/mpeg\r\n\ + Accept-Ranges: bytes\r\n\ + Content-Range: bytes {}-{}/{}\r\n\ + Content-Length: {}\r\n\ + Connection: close\r\n\r\n", + start_offset, total_size - 1, total_size, total_size - start_offset) + } else { + format!("HTTP/1.1 200 OK\r\n\ + Content-Type: audio/mpeg\r\n\ + Accept-Ranges: bytes\r\n\ + Content-Length: {}\r\n\ + Connection: close\r\n\r\n", total_size) + }; + + if tcp_stream.write_all(headers.as_bytes()).await.is_ok() { + let _ = blob_reader.seek(SeekFrom::Start(start_offset)).await; + let _ = tokio::io::copy(&mut blob_reader, &mut tcp_stream).await; + } + } + } + } + }); + } + } + }); + + Ok(tracks) +} + diff --git a/src/App.tsx b/src/App.tsx index d7b857c..eff89d5 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -46,6 +46,8 @@ import { HashRouter, Navigate, NavLink, Outlet, Route, Routes, useLocation, useO import { Button } from "./components/ui/button"; import { CatalogPage } from "./CatalogPage"; import { BroadcastPage } from "./BroadcastPage"; +import { StreamBroadcaster } from "./pages/StreamBroadcaster"; +import { StreamListener } from "./pages/StreamListener"; import { Card, CardContent, CardHeader, CardTitle } from "./components/ui/card"; import { GlobalAudioPlayerProvider, SidebarAudioPlayer, useGlobalAudioPlayer } from "./components/audio/GlobalAudioPlayer"; import { @@ -311,6 +313,8 @@ export default function App() { } /> } /> } /> + } /> + } /> } @@ -2485,6 +2489,12 @@ function AppSidebar({ }> {t("Broadcast")} + }> + {t("Stream Broadcaster")} + + }> + {t("Stream Listener")} + diff --git a/src/pages/StreamBroadcaster.tsx b/src/pages/StreamBroadcaster.tsx new file mode 100644 index 0000000..c0cbdc9 --- /dev/null +++ b/src/pages/StreamBroadcaster.tsx @@ -0,0 +1,168 @@ +import { useState, useEffect } from "react"; +import { Button } from "../components/ui/button"; +import { open } from "@tauri-apps/plugin-dialog"; +import { invoke } from "@tauri-apps/api/core"; +import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; +import { FolderOpen, Radio, Music, Info, AlertTriangle } from "lucide-react"; + +type Track = { hash: string; name: string; path: string; selected?: boolean }; + +export function StreamBroadcaster() { + const [enabled, setEnabled] = useState(false); + const [directory, setDirectory] = useState(() => localStorage.getItem("stream_broadcaster_dir") || ""); + const [tracks, setTracks] = useState([]); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + + // Restore tracks from backend on mount + useEffect(() => { + invoke("get_broadcaster_tracks") + .then(t => { + if (t && t.length > 0) { + // If we got tracks from the backend, it means they are currently selected/active. + setTracks(t.map(track => ({ ...track, selected: true }))); + setEnabled(true); + } + }) + .catch(console.error); + }, []); + + useEffect(() => { + if (directory) { + localStorage.setItem("stream_broadcaster_dir", directory); + // Only scan automatically if we don't have tracks loaded from backend + if (tracks.length === 0 && !enabled) { + scanDirectory(directory); + } + } + }, [directory]); + + const scanDirectory = (dir: string) => { + setLoading(true); + setError(null); + invoke("scan_audio_directory", { path: dir }) + .then(t => setTracks(t.map(track => ({ ...track, selected: true })))) + .catch(err => { + console.error(err); + setError(String(err)); + }) + .finally(() => setLoading(false)); + }; + + const toggleTrack = (hash: string) => { + setTracks(tracks.map(t => t.hash === hash ? { ...t, selected: !t.selected } : t)); + }; + + const handleEnableToggle = async (checked: boolean) => { + setEnabled(checked); + if (checked) { + const selectedTracks = tracks.filter(t => t.selected); + await invoke("set_broadcaster_tracks", { tracks: selectedTracks }).catch(console.error); + } else { + await invoke("set_broadcaster_tracks", { tracks: [] }).catch(console.error); + } + }; + + const handleBrowse = async () => { + const selected = await open({ + directory: true, + multiple: false, + defaultPath: directory || undefined, + }); + if (selected && typeof selected === "string") { + setDirectory(selected); + scanDirectory(selected); + } + }; + + return ( +
+
+ +
+

Stream Broadcaster

+

Broadcast an audio directory over the P2P network

+
+
+ +
+ + +
+ + How it works +
+
+ +

1. Connect to the P2P network in the Rau Connect tab.

+

2. Select a directory containing .wav or .mp3 files.

+

3. Enable the broadcaster switch below. Your connected peers can now tune in and stream any track on-demand!

+
+
+ + {error && ( +
+ +
+

Scan Error

+

{error}

+
+
+ )} + +
+
+ + {directory || "No directory selected"} +
+ +
+ +
+
+ Enable Broadcasting + Allow peers to stream selected tracks +
+ +
+ + {loading && ( +
+
+ Scanning for audio files... +
+ )} + + {tracks.length > 0 && ( + + +
+ + Available Tracks ({tracks.length}) +
+
+ + {tracks.map(track => ( + + ))} + +
+ )} +
+
+ ); +} diff --git a/src/pages/StreamListener.tsx b/src/pages/StreamListener.tsx new file mode 100644 index 0000000..f270665 --- /dev/null +++ b/src/pages/StreamListener.tsx @@ -0,0 +1,160 @@ +import { useState, useRef, useEffect } from "react"; +import { Button } from "../components/ui/button"; +import { invoke } from "@tauri-apps/api/core"; +import { Card, CardContent, CardHeader, CardTitle } from "../components/ui/card"; +import { Headphones, KeyRound, Play, Music, Info, AlertTriangle } from "lucide-react"; + +type Track = { hash: string; name: string; path: string }; + +export function StreamListener() { + const [ticket, setTicket] = useState(() => localStorage.getItem("stream_listener_ticket") || ""); + const [tracks, setTracks] = useState([]); + const [activeTrack, setActiveTrack] = useState(null); + const [loading, setLoading] = useState(false); + const [error, setError] = useState(null); + const audioRef = useRef(null); + + useEffect(() => { + localStorage.setItem("stream_listener_ticket", ticket); + }, [ticket]); + + const handleLoad = async () => { + if (!ticket) return; + setLoading(true); + setError(null); + try { + const fetchedTracks = await invoke("connect_to_broadcaster", { ticket }); + setTracks(fetchedTracks); + // Automatically play the first track if available + if (fetchedTracks.length > 0) { + setActiveTrack(fetchedTracks[0].hash); + } + } catch (e) { + console.error(e); + setError(String(e)); + } finally { + setLoading(false); + } + }; + + const handleNextTrack = () => { + if (!activeTrack || tracks.length === 0) return; + const currentIndex = tracks.findIndex(t => t.hash === activeTrack); + if (currentIndex >= 0 && currentIndex < tracks.length - 1) { + setActiveTrack(tracks[currentIndex + 1].hash); + } else { + setActiveTrack(null); + } + }; + + return ( +
+
+ +
+

Stream Listener

+

Tune in to a peer's live P2P audio stream

+
+
+ +
+ {tracks.length === 0 && ( + + +
+ + How it works +
+
+ +

1. Ask the broadcaster to start their stream and copy their Rau Connect Ticket.

+

2. Paste the ticket below and click Load to fetch their live playlist.

+

3. The music will stream directly from their device to yours in real-time!

+
+
+ )} + + {error && ( +
+ +
+

Connection Error

+

{error}

+
+
+ )} + +
+
+ + setTicket(e.target.value)} + className="flex-1 bg-transparent border-none focus:outline-none text-foreground placeholder:text-muted-foreground" + disabled={loading} + /> +
+ +
+ + {tracks.length > 0 && ( + + +
+ + Broadcaster Playlist ({tracks.length}) +
+
+ + {tracks.map((track) => ( +
+ + {track.name} + +
+ +
+
+ ))} +
+
+ )} +
+ + {activeTrack && ( +
+

+ {tracks.find(t => t.hash === activeTrack)?.name} +

+
+ )} +
+ ); +}