├── .babelrc ├── .cargo └── config.toml ├── .github └── workflows │ ├── build.yml │ ├── prerelease.yml │ └── publish.yml ├── .gitignore ├── .npmignore ├── Cargo.lock ├── Cargo.toml ├── README.md ├── package-lock.json ├── package.json ├── rust-toolchain.toml ├── scripts └── build.sh └── src ├── bridge.js ├── env.rs ├── event.rs ├── lib.rs ├── model ├── deep_links_ext │ ├── addons_deep_links.rs │ ├── deep_links_ext.rs │ ├── discover_deep_links.rs │ ├── library_deep_links.rs │ ├── library_item_deep_links.rs │ ├── local_search_deep_links.rs │ ├── meta_item_deep_links.rs │ ├── mod.rs │ ├── search_history_deep_links.rs │ ├── stream_deep_links.rs │ └── video_deep_links.rs ├── mod.rs ├── model.rs ├── serialize_catalogs_with_extra.rs ├── serialize_continue_watching_preview.rs ├── serialize_ctx.rs ├── serialize_data_export.rs ├── serialize_discover.rs ├── serialize_installed_addons.rs ├── serialize_library.rs ├── serialize_local_search.rs ├── serialize_meta_details.rs ├── serialize_player.rs ├── serialize_remote_addons.rs └── serialize_streaming_server.rs ├── stremio_core_web.rs └── worker.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": [ 3 | "@babel/preset-env" 4 | ], 5 | "plugins": [ 6 | "@babel/plugin-transform-runtime", 7 | [ 8 | "babel-plugin-bundled-import-meta", 9 | { 10 | "mappings": { 11 | "./wasm_build": "" 12 | }, 13 | "importStyle": "baseURI" 14 | } 15 | ] 16 | ] 17 | } -------------------------------------------------------------------------------- /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | 2 | 3 | [alias] 4 | # Requires cargo-watch 5 | ww = ["watch-wasm"] 6 | watch-wasm = ["watch", "--shell", "./scripts/build.sh --dev"] -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | push: 5 | branches: 6 | - '**' 7 | tags-ignore: 8 | - '**' 9 | 10 | # Stops the running workflow of previous pushes 11 | concurrency: 12 | group: ${{ github.ref }} 13 | cancel-in-progress: true 14 | 15 | env: 16 | NODE_VERSION: 12 17 | WASM_PACK_VERSION: 0.12.1 18 | 19 | jobs: 20 | lint-and-build: 21 | runs-on: ubuntu-latest 22 | steps: 23 | - name: Checkout 24 | uses: actions/checkout@v4 25 | 26 | - name: Rust setup 27 | uses: dtolnay/rust-toolchain@1.77 28 | with: 29 | components: rustfmt, clippy 30 | targets: wasm32-unknown-unknown 31 | 32 | - uses: Swatinem/rust-cache@v2 33 | 34 | - name: Lint - rustfmt 35 | run: cargo fmt --all -- --check 36 | 37 | - name: Lint - clippy 38 | run: cargo clippy --all --no-deps -- -D warnings 39 | 40 | - uses: taiki-e/install-action@v2 41 | with: 42 | tool: wasm-pack@${{ env.WASM_PACK_VERSION }} 43 | - name: Setup chromedriver 44 | uses: nanasess/setup-chromedriver@v2 45 | 46 | - name: Run wasm tests (Chrome) 47 | run: wasm-pack test --chromedriver "$(which chromedriver)" --chrome --headless 48 | 49 | - name: Setup NodeJS 50 | uses: actions/setup-node@v4 51 | with: 52 | node-version: ${{ env.NODE_VERSION }} 53 | cache: "npm" 54 | registry-url: https://registry.npmjs.org/ 55 | 56 | - name: Install NPM dependencies 57 | run: npm ci 58 | 59 | - name: Build 60 | run: npm run build 61 | -------------------------------------------------------------------------------- /.github/workflows/prerelease.yml: -------------------------------------------------------------------------------- 1 | name: Prerelease 2 | 3 | on: 4 | release: 5 | types: [prereleased] 6 | 7 | env: 8 | NODE_VERSION: 12 9 | WASM_PACK_VERSION: 0.12.1 10 | 11 | jobs: 12 | prerelease: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | - name: Setup NodeJS 18 | uses: actions/setup-node@v3 19 | with: 20 | node-version: ${{ env.NODE_VERSION }} 21 | cache: "npm" 22 | registry-url: https://registry.npmjs.org/ 23 | 24 | - name: Rust setup 25 | uses: dtolnay/rust-toolchain@1.77 26 | with: 27 | targets: wasm32-unknown-unknown 28 | 29 | - uses: taiki-e/install-action@v2 30 | with: 31 | tool: wasm-pack@${{ env.WASM_PACK_VERSION }} 32 | 33 | - name: Install NPM dependencies 34 | run: npm ci 35 | 36 | - name: Build 37 | run: npm run build 38 | 39 | - name: Package 40 | run: npm pack 41 | 42 | - name: Upload build artifact to GitHub release assets 43 | uses: svenstaro/upload-release-action@v2 44 | with: 45 | repo_token: ${{ secrets.GITHUB_TOKEN }} 46 | file: ./*.tgz 47 | tag: ${{ github.ref }} 48 | overwrite: true 49 | file_glob: true 50 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish 2 | 3 | on: 4 | release: 5 | types: [released] 6 | 7 | env: 8 | NODE_VERSION: 12 9 | WASM_PACK_VERSION: 0.12.1 10 | 11 | jobs: 12 | publish: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - name: Checkout 16 | uses: actions/checkout@v3 17 | 18 | - name: Setup NodeJS 19 | uses: actions/setup-node@v3 20 | with: 21 | node-version: ${{ env.NODE_VERSION }} 22 | cache: "npm" 23 | registry-url: https://registry.npmjs.org/ 24 | 25 | # For releasing we always use stable 26 | - name: Rust setup 27 | uses: dtolnay/rust-toolchain@1.77 28 | with: 29 | targets: wasm32-unknown-unknown 30 | - uses: taiki-e/install-action@v2 31 | with: 32 | tool: wasm-pack@${{ env.WASM_PACK_VERSION }} 33 | 34 | - name: Install NPM dependencies 35 | run: npm ci 36 | 37 | - name: Build 38 | run: npm run build 39 | 40 | - name: Publish to NPM 41 | run: npm publish --access public 42 | env: 43 | NODE_AUTH_TOKEN: ${{ secrets.NPM_PUBLISH_TOKEN }} 44 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | /node_modules 3 | /wasm_build 4 | stremio_core_web.js 5 | bridge.js 6 | worker.js 7 | stremio_core_web_bg.wasm -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | /.github 2 | /src 3 | /target 4 | /wasm_build 5 | .babelrc 6 | scripts/build.sh 7 | .cargo 8 | Cargo.lock 9 | Cargo.toml 10 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "Inflector" 7 | version = "0.11.4" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" 10 | dependencies = [ 11 | "lazy_static", 12 | "regex", 13 | ] 14 | 15 | [[package]] 16 | name = "adler" 17 | version = "1.0.2" 18 | source = "registry+https://github.com/rust-lang/crates.io-index" 19 | checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" 20 | 21 | [[package]] 22 | name = "aho-corasick" 23 | version = "1.1.3" 24 | source = "registry+https://github.com/rust-lang/crates.io-index" 25 | checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" 26 | dependencies = [ 27 | "memchr", 28 | ] 29 | 30 | [[package]] 31 | name = "android-tzdata" 32 | version = "0.1.1" 33 | source = "registry+https://github.com/rust-lang/crates.io-index" 34 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 35 | 36 | [[package]] 37 | name = "android_system_properties" 38 | version = "0.1.5" 39 | source = "registry+https://github.com/rust-lang/crates.io-index" 40 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 41 | dependencies = [ 42 | "libc", 43 | ] 44 | 45 | [[package]] 46 | name = "anyhow" 47 | version = "1.0.83" 48 | source = "registry+https://github.com/rust-lang/crates.io-index" 49 | checksum = "25bdb32cbbdce2b519a9cd7df3a678443100e265d5e25ca763b7572a5104f5f3" 50 | 51 | [[package]] 52 | name = "array-init" 53 | version = "0.0.4" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "23589ecb866b460d3a0f1278834750268c607e8e28a1b982c907219f3178cd72" 56 | dependencies = [ 57 | "nodrop", 58 | ] 59 | 60 | [[package]] 61 | name = "autocfg" 62 | version = "1.3.0" 63 | source = "registry+https://github.com/rust-lang/crates.io-index" 64 | checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" 65 | 66 | [[package]] 67 | name = "base64" 68 | version = "0.13.1" 69 | source = "registry+https://github.com/rust-lang/crates.io-index" 70 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 71 | 72 | [[package]] 73 | name = "base64" 74 | version = "0.21.7" 75 | source = "registry+https://github.com/rust-lang/crates.io-index" 76 | checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" 77 | 78 | [[package]] 79 | name = "base64" 80 | version = "0.22.1" 81 | source = "registry+https://github.com/rust-lang/crates.io-index" 82 | checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" 83 | 84 | [[package]] 85 | name = "block-buffer" 86 | version = "0.10.4" 87 | source = "registry+https://github.com/rust-lang/crates.io-index" 88 | checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" 89 | dependencies = [ 90 | "generic-array", 91 | ] 92 | 93 | [[package]] 94 | name = "boolinator" 95 | version = "2.4.0" 96 | source = "registry+https://github.com/rust-lang/crates.io-index" 97 | checksum = "cfa8873f51c92e232f9bac4065cddef41b714152812bfc5f7672ba16d6ef8cd9" 98 | 99 | [[package]] 100 | name = "bumpalo" 101 | version = "3.16.0" 102 | source = "registry+https://github.com/rust-lang/crates.io-index" 103 | checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" 104 | 105 | [[package]] 106 | name = "byteorder" 107 | version = "1.5.0" 108 | source = "registry+https://github.com/rust-lang/crates.io-index" 109 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 110 | 111 | [[package]] 112 | name = "bytes" 113 | version = "1.6.0" 114 | source = "registry+https://github.com/rust-lang/crates.io-index" 115 | checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" 116 | 117 | [[package]] 118 | name = "case" 119 | version = "1.0.0" 120 | source = "registry+https://github.com/rust-lang/crates.io-index" 121 | checksum = "fd6c0e7b807d60291f42f33f58480c0bfafe28ed08286446f45e463728cf9c1c" 122 | 123 | [[package]] 124 | name = "cc" 125 | version = "1.0.97" 126 | source = "registry+https://github.com/rust-lang/crates.io-index" 127 | checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4" 128 | 129 | [[package]] 130 | name = "cfg-if" 131 | version = "1.0.0" 132 | source = "registry+https://github.com/rust-lang/crates.io-index" 133 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 134 | 135 | [[package]] 136 | name = "chrono" 137 | version = "0.4.38" 138 | source = "registry+https://github.com/rust-lang/crates.io-index" 139 | checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401" 140 | dependencies = [ 141 | "android-tzdata", 142 | "iana-time-zone", 143 | "js-sys", 144 | "num-traits", 145 | "serde", 146 | "wasm-bindgen", 147 | "windows-targets", 148 | ] 149 | 150 | [[package]] 151 | name = "console_error_panic_hook" 152 | version = "0.1.7" 153 | source = "registry+https://github.com/rust-lang/crates.io-index" 154 | checksum = "a06aeb73f470f66dcdbf7223caeebb85984942f22f1adb2a088cf9668146bbbc" 155 | dependencies = [ 156 | "cfg-if", 157 | "wasm-bindgen", 158 | ] 159 | 160 | [[package]] 161 | name = "convert_case" 162 | version = "0.4.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" 165 | 166 | [[package]] 167 | name = "core-foundation-sys" 168 | version = "0.8.6" 169 | source = "registry+https://github.com/rust-lang/crates.io-index" 170 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 171 | 172 | [[package]] 173 | name = "cpufeatures" 174 | version = "0.2.12" 175 | source = "registry+https://github.com/rust-lang/crates.io-index" 176 | checksum = "53fe5e26ff1b7aef8bca9c6080520cfb8d9333c7568e1829cef191a9723e5504" 177 | dependencies = [ 178 | "libc", 179 | ] 180 | 181 | [[package]] 182 | name = "crc32fast" 183 | version = "1.4.0" 184 | source = "registry+https://github.com/rust-lang/crates.io-index" 185 | checksum = "b3855a8a784b474f333699ef2bbca9db2c4a1f6d9088a90a2d25b1eb53111eaa" 186 | dependencies = [ 187 | "cfg-if", 188 | ] 189 | 190 | [[package]] 191 | name = "crypto-common" 192 | version = "0.1.6" 193 | source = "registry+https://github.com/rust-lang/crates.io-index" 194 | checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" 195 | dependencies = [ 196 | "generic-array", 197 | "typenum", 198 | ] 199 | 200 | [[package]] 201 | name = "darling" 202 | version = "0.20.8" 203 | source = "registry+https://github.com/rust-lang/crates.io-index" 204 | checksum = "54e36fcd13ed84ffdfda6f5be89b31287cbb80c439841fe69e04841435464391" 205 | dependencies = [ 206 | "darling_core", 207 | "darling_macro", 208 | ] 209 | 210 | [[package]] 211 | name = "darling_core" 212 | version = "0.20.8" 213 | source = "registry+https://github.com/rust-lang/crates.io-index" 214 | checksum = "9c2cf1c23a687a1feeb728783b993c4e1ad83d99f351801977dd809b48d0a70f" 215 | dependencies = [ 216 | "fnv", 217 | "ident_case", 218 | "proc-macro2", 219 | "quote", 220 | "strsim", 221 | "syn 2.0.61", 222 | ] 223 | 224 | [[package]] 225 | name = "darling_macro" 226 | version = "0.20.8" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "a668eda54683121533a393014d8692171709ff57a7d61f187b6e782719f8933f" 229 | dependencies = [ 230 | "darling_core", 231 | "quote", 232 | "syn 2.0.61", 233 | ] 234 | 235 | [[package]] 236 | name = "deranged" 237 | version = "0.3.11" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" 240 | dependencies = [ 241 | "powerfmt", 242 | "serde", 243 | ] 244 | 245 | [[package]] 246 | name = "derivative" 247 | version = "2.2.0" 248 | source = "registry+https://github.com/rust-lang/crates.io-index" 249 | checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" 250 | dependencies = [ 251 | "proc-macro2", 252 | "quote", 253 | "syn 1.0.109", 254 | ] 255 | 256 | [[package]] 257 | name = "derive_more" 258 | version = "0.99.17" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 261 | dependencies = [ 262 | "convert_case", 263 | "proc-macro2", 264 | "quote", 265 | "rustc_version", 266 | "syn 1.0.109", 267 | ] 268 | 269 | [[package]] 270 | name = "digest" 271 | version = "0.10.7" 272 | source = "registry+https://github.com/rust-lang/crates.io-index" 273 | checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" 274 | dependencies = [ 275 | "block-buffer", 276 | "crypto-common", 277 | ] 278 | 279 | [[package]] 280 | name = "either" 281 | version = "1.6.1" 282 | source = "registry+https://github.com/rust-lang/crates.io-index" 283 | checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" 284 | 285 | [[package]] 286 | name = "enclose" 287 | version = "1.1.8" 288 | source = "registry+https://github.com/rust-lang/crates.io-index" 289 | checksum = "1056f553da426e9c025a662efa48b52e62e0a3a7648aa2d15aeaaf7f0d329357" 290 | 291 | [[package]] 292 | name = "equivalent" 293 | version = "1.0.1" 294 | source = "registry+https://github.com/rust-lang/crates.io-index" 295 | checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" 296 | 297 | [[package]] 298 | name = "flate2" 299 | version = "1.0.30" 300 | source = "registry+https://github.com/rust-lang/crates.io-index" 301 | checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae" 302 | dependencies = [ 303 | "crc32fast", 304 | "miniz_oxide", 305 | ] 306 | 307 | [[package]] 308 | name = "fnv" 309 | version = "1.0.7" 310 | source = "registry+https://github.com/rust-lang/crates.io-index" 311 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 312 | 313 | [[package]] 314 | name = "form_urlencoded" 315 | version = "1.2.1" 316 | source = "registry+https://github.com/rust-lang/crates.io-index" 317 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 318 | dependencies = [ 319 | "percent-encoding", 320 | ] 321 | 322 | [[package]] 323 | name = "fst" 324 | version = "0.4.7" 325 | source = "registry+https://github.com/rust-lang/crates.io-index" 326 | checksum = "7ab85b9b05e3978cc9a9cf8fea7f01b494e1a09ed3037e16ba39edc7a29eb61a" 327 | 328 | [[package]] 329 | name = "futures" 330 | version = "0.3.30" 331 | source = "registry+https://github.com/rust-lang/crates.io-index" 332 | checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" 333 | dependencies = [ 334 | "futures-channel", 335 | "futures-core", 336 | "futures-executor", 337 | "futures-io", 338 | "futures-sink", 339 | "futures-task", 340 | "futures-util", 341 | ] 342 | 343 | [[package]] 344 | name = "futures-channel" 345 | version = "0.3.30" 346 | source = "registry+https://github.com/rust-lang/crates.io-index" 347 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 348 | dependencies = [ 349 | "futures-core", 350 | "futures-sink", 351 | ] 352 | 353 | [[package]] 354 | name = "futures-core" 355 | version = "0.3.30" 356 | source = "registry+https://github.com/rust-lang/crates.io-index" 357 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 358 | 359 | [[package]] 360 | name = "futures-executor" 361 | version = "0.3.30" 362 | source = "registry+https://github.com/rust-lang/crates.io-index" 363 | checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" 364 | dependencies = [ 365 | "futures-core", 366 | "futures-task", 367 | "futures-util", 368 | ] 369 | 370 | [[package]] 371 | name = "futures-io" 372 | version = "0.3.30" 373 | source = "registry+https://github.com/rust-lang/crates.io-index" 374 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 375 | 376 | [[package]] 377 | name = "futures-macro" 378 | version = "0.3.30" 379 | source = "registry+https://github.com/rust-lang/crates.io-index" 380 | checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" 381 | dependencies = [ 382 | "proc-macro2", 383 | "quote", 384 | "syn 2.0.61", 385 | ] 386 | 387 | [[package]] 388 | name = "futures-sink" 389 | version = "0.3.30" 390 | source = "registry+https://github.com/rust-lang/crates.io-index" 391 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 392 | 393 | [[package]] 394 | name = "futures-task" 395 | version = "0.3.30" 396 | source = "registry+https://github.com/rust-lang/crates.io-index" 397 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 398 | 399 | [[package]] 400 | name = "futures-util" 401 | version = "0.3.30" 402 | source = "registry+https://github.com/rust-lang/crates.io-index" 403 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 404 | dependencies = [ 405 | "futures-channel", 406 | "futures-core", 407 | "futures-io", 408 | "futures-macro", 409 | "futures-sink", 410 | "futures-task", 411 | "memchr", 412 | "pin-project-lite", 413 | "pin-utils", 414 | "slab", 415 | ] 416 | 417 | [[package]] 418 | name = "fxhash" 419 | version = "0.2.1" 420 | source = "registry+https://github.com/rust-lang/crates.io-index" 421 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 422 | dependencies = [ 423 | "byteorder", 424 | ] 425 | 426 | [[package]] 427 | name = "generic-array" 428 | version = "0.14.7" 429 | source = "registry+https://github.com/rust-lang/crates.io-index" 430 | checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" 431 | dependencies = [ 432 | "typenum", 433 | "version_check", 434 | ] 435 | 436 | [[package]] 437 | name = "getrandom" 438 | version = "0.2.15" 439 | source = "registry+https://github.com/rust-lang/crates.io-index" 440 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 441 | dependencies = [ 442 | "cfg-if", 443 | "js-sys", 444 | "libc", 445 | "wasi", 446 | "wasm-bindgen", 447 | ] 448 | 449 | [[package]] 450 | name = "gloo-utils" 451 | version = "0.2.0" 452 | source = "registry+https://github.com/rust-lang/crates.io-index" 453 | checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" 454 | dependencies = [ 455 | "js-sys", 456 | "serde", 457 | "serde_json", 458 | "wasm-bindgen", 459 | "web-sys", 460 | ] 461 | 462 | [[package]] 463 | name = "hashbrown" 464 | version = "0.12.3" 465 | source = "registry+https://github.com/rust-lang/crates.io-index" 466 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 467 | 468 | [[package]] 469 | name = "hashbrown" 470 | version = "0.14.5" 471 | source = "registry+https://github.com/rust-lang/crates.io-index" 472 | checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" 473 | 474 | [[package]] 475 | name = "heck" 476 | version = "0.4.1" 477 | source = "registry+https://github.com/rust-lang/crates.io-index" 478 | checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" 479 | 480 | [[package]] 481 | name = "hex" 482 | version = "0.4.3" 483 | source = "registry+https://github.com/rust-lang/crates.io-index" 484 | checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" 485 | 486 | [[package]] 487 | name = "http" 488 | version = "0.2.12" 489 | source = "registry+https://github.com/rust-lang/crates.io-index" 490 | checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" 491 | dependencies = [ 492 | "bytes", 493 | "fnv", 494 | "itoa", 495 | ] 496 | 497 | [[package]] 498 | name = "iana-time-zone" 499 | version = "0.1.60" 500 | source = "registry+https://github.com/rust-lang/crates.io-index" 501 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 502 | dependencies = [ 503 | "android_system_properties", 504 | "core-foundation-sys", 505 | "iana-time-zone-haiku", 506 | "js-sys", 507 | "wasm-bindgen", 508 | "windows-core", 509 | ] 510 | 511 | [[package]] 512 | name = "iana-time-zone-haiku" 513 | version = "0.1.2" 514 | source = "registry+https://github.com/rust-lang/crates.io-index" 515 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 516 | dependencies = [ 517 | "cc", 518 | ] 519 | 520 | [[package]] 521 | name = "ident_case" 522 | version = "1.0.1" 523 | source = "registry+https://github.com/rust-lang/crates.io-index" 524 | checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" 525 | 526 | [[package]] 527 | name = "idna" 528 | version = "0.4.0" 529 | source = "registry+https://github.com/rust-lang/crates.io-index" 530 | checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" 531 | dependencies = [ 532 | "unicode-bidi", 533 | "unicode-normalization", 534 | ] 535 | 536 | [[package]] 537 | name = "indexmap" 538 | version = "1.9.3" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 541 | dependencies = [ 542 | "autocfg", 543 | "hashbrown 0.12.3", 544 | "serde", 545 | ] 546 | 547 | [[package]] 548 | name = "indexmap" 549 | version = "2.2.6" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" 552 | dependencies = [ 553 | "equivalent", 554 | "hashbrown 0.14.5", 555 | "serde", 556 | ] 557 | 558 | [[package]] 559 | name = "itertools" 560 | version = "0.10.5" 561 | source = "registry+https://github.com/rust-lang/crates.io-index" 562 | checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473" 563 | dependencies = [ 564 | "either", 565 | ] 566 | 567 | [[package]] 568 | name = "itertools" 569 | version = "0.11.0" 570 | source = "registry+https://github.com/rust-lang/crates.io-index" 571 | checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" 572 | dependencies = [ 573 | "either", 574 | ] 575 | 576 | [[package]] 577 | name = "itoa" 578 | version = "1.0.11" 579 | source = "registry+https://github.com/rust-lang/crates.io-index" 580 | checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" 581 | 582 | [[package]] 583 | name = "js-sys" 584 | version = "0.3.55" 585 | source = "registry+https://github.com/rust-lang/crates.io-index" 586 | checksum = "7cc9ffccd38c451a86bf13657df244e9c3f37493cce8e5e21e940963777acc84" 587 | dependencies = [ 588 | "wasm-bindgen", 589 | ] 590 | 591 | [[package]] 592 | name = "lazy_static" 593 | version = "1.4.0" 594 | source = "registry+https://github.com/rust-lang/crates.io-index" 595 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 596 | 597 | [[package]] 598 | name = "lazysort" 599 | version = "0.2.1" 600 | source = "registry+https://github.com/rust-lang/crates.io-index" 601 | checksum = "d0e22ff43b231e0e2f87d74984e53ebc73b90ae13397e041214fb07efc64168f" 602 | 603 | [[package]] 604 | name = "libc" 605 | version = "0.2.154" 606 | source = "registry+https://github.com/rust-lang/crates.io-index" 607 | checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346" 608 | 609 | [[package]] 610 | name = "localsearch" 611 | version = "0.1.0" 612 | source = "git+https://github.com/Stremio/local-search?branch=main#74fefe1da2fa17d2b4ef7e3e629da00f3946ee72" 613 | dependencies = [ 614 | "fst", 615 | "fxhash", 616 | "num-traits", 617 | "utf8-ranges", 618 | ] 619 | 620 | [[package]] 621 | name = "log" 622 | version = "0.4.21" 623 | source = "registry+https://github.com/rust-lang/crates.io-index" 624 | checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c" 625 | 626 | [[package]] 627 | name = "magnet-url" 628 | version = "2.0.0" 629 | source = "registry+https://github.com/rust-lang/crates.io-index" 630 | checksum = "ac3b4c4004e88aca00cc0c60782e5642c8fc628deca19e530ce58aa76e737d74" 631 | dependencies = [ 632 | "lazy_static", 633 | "regex", 634 | ] 635 | 636 | [[package]] 637 | name = "maybe-uninit" 638 | version = "2.0.0" 639 | source = "registry+https://github.com/rust-lang/crates.io-index" 640 | checksum = "60302e4db3a61da70c0cb7991976248362f30319e88850c487b9b95bbf059e00" 641 | 642 | [[package]] 643 | name = "memchr" 644 | version = "2.7.2" 645 | source = "registry+https://github.com/rust-lang/crates.io-index" 646 | checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" 647 | 648 | [[package]] 649 | name = "miniz_oxide" 650 | version = "0.7.2" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" 653 | dependencies = [ 654 | "adler", 655 | ] 656 | 657 | [[package]] 658 | name = "nodrop" 659 | version = "0.1.14" 660 | source = "registry+https://github.com/rust-lang/crates.io-index" 661 | checksum = "72ef4a56884ca558e5ddb05a1d1e7e1bfd9a68d9ed024c21704cc98872dae1bb" 662 | 663 | [[package]] 664 | name = "num" 665 | version = "0.4.3" 666 | source = "registry+https://github.com/rust-lang/crates.io-index" 667 | checksum = "35bd024e8b2ff75562e5f34e7f4905839deb4b22955ef5e73d2fea1b9813cb23" 668 | dependencies = [ 669 | "num-complex", 670 | "num-integer", 671 | "num-iter", 672 | "num-rational", 673 | "num-traits", 674 | ] 675 | 676 | [[package]] 677 | name = "num-complex" 678 | version = "0.4.6" 679 | source = "registry+https://github.com/rust-lang/crates.io-index" 680 | checksum = "73f88a1307638156682bada9d7604135552957b7818057dcef22705b4d509495" 681 | dependencies = [ 682 | "num-traits", 683 | ] 684 | 685 | [[package]] 686 | name = "num-conv" 687 | version = "0.1.0" 688 | source = "registry+https://github.com/rust-lang/crates.io-index" 689 | checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 690 | 691 | [[package]] 692 | name = "num-integer" 693 | version = "0.1.46" 694 | source = "registry+https://github.com/rust-lang/crates.io-index" 695 | checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" 696 | dependencies = [ 697 | "num-traits", 698 | ] 699 | 700 | [[package]] 701 | name = "num-iter" 702 | version = "0.1.45" 703 | source = "registry+https://github.com/rust-lang/crates.io-index" 704 | checksum = "1429034a0490724d0075ebb2bc9e875d6503c3cf69e235a8941aa757d83ef5bf" 705 | dependencies = [ 706 | "autocfg", 707 | "num-integer", 708 | "num-traits", 709 | ] 710 | 711 | [[package]] 712 | name = "num-rational" 713 | version = "0.4.2" 714 | source = "registry+https://github.com/rust-lang/crates.io-index" 715 | checksum = "f83d14da390562dca69fc84082e73e548e1ad308d24accdedd2720017cb37824" 716 | dependencies = [ 717 | "num-integer", 718 | "num-traits", 719 | ] 720 | 721 | [[package]] 722 | name = "num-traits" 723 | version = "0.2.19" 724 | source = "registry+https://github.com/rust-lang/crates.io-index" 725 | checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" 726 | dependencies = [ 727 | "autocfg", 728 | ] 729 | 730 | [[package]] 731 | name = "once_cell" 732 | version = "1.19.0" 733 | source = "registry+https://github.com/rust-lang/crates.io-index" 734 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 735 | 736 | [[package]] 737 | name = "percent-encoding" 738 | version = "2.3.1" 739 | source = "registry+https://github.com/rust-lang/crates.io-index" 740 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 741 | 742 | [[package]] 743 | name = "pin-project-lite" 744 | version = "0.2.14" 745 | source = "registry+https://github.com/rust-lang/crates.io-index" 746 | checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" 747 | 748 | [[package]] 749 | name = "pin-utils" 750 | version = "0.1.0" 751 | source = "registry+https://github.com/rust-lang/crates.io-index" 752 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 753 | 754 | [[package]] 755 | name = "powerfmt" 756 | version = "0.2.0" 757 | source = "registry+https://github.com/rust-lang/crates.io-index" 758 | checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" 759 | 760 | [[package]] 761 | name = "proc-macro-crate" 762 | version = "1.3.1" 763 | source = "registry+https://github.com/rust-lang/crates.io-index" 764 | checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" 765 | dependencies = [ 766 | "once_cell", 767 | "toml_edit", 768 | ] 769 | 770 | [[package]] 771 | name = "proc-macro2" 772 | version = "1.0.82" 773 | source = "registry+https://github.com/rust-lang/crates.io-index" 774 | checksum = "8ad3d49ab951a01fbaafe34f2ec74122942fe18a3f9814c3268f1bb72042131b" 775 | dependencies = [ 776 | "unicode-ident", 777 | ] 778 | 779 | [[package]] 780 | name = "proc_macro_roids" 781 | version = "0.8.0" 782 | source = "registry+https://github.com/rust-lang/crates.io-index" 783 | checksum = "d0c2a098cd8aaa29f66da27a684ad19f4b7bc886f576abf12f7df4a7391071c7" 784 | dependencies = [ 785 | "proc-macro2", 786 | "quote", 787 | "syn 2.0.61", 788 | ] 789 | 790 | [[package]] 791 | name = "quote" 792 | version = "1.0.36" 793 | source = "registry+https://github.com/rust-lang/crates.io-index" 794 | checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" 795 | dependencies = [ 796 | "proc-macro2", 797 | ] 798 | 799 | [[package]] 800 | name = "regex" 801 | version = "1.10.4" 802 | source = "registry+https://github.com/rust-lang/crates.io-index" 803 | checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" 804 | dependencies = [ 805 | "aho-corasick", 806 | "memchr", 807 | "regex-automata", 808 | "regex-syntax", 809 | ] 810 | 811 | [[package]] 812 | name = "regex-automata" 813 | version = "0.4.6" 814 | source = "registry+https://github.com/rust-lang/crates.io-index" 815 | checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" 816 | dependencies = [ 817 | "aho-corasick", 818 | "memchr", 819 | "regex-syntax", 820 | ] 821 | 822 | [[package]] 823 | name = "regex-syntax" 824 | version = "0.8.3" 825 | source = "registry+https://github.com/rust-lang/crates.io-index" 826 | checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" 827 | 828 | [[package]] 829 | name = "rustc_version" 830 | version = "0.4.0" 831 | source = "registry+https://github.com/rust-lang/crates.io-index" 832 | checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" 833 | dependencies = [ 834 | "semver", 835 | ] 836 | 837 | [[package]] 838 | name = "rustversion" 839 | version = "1.0.16" 840 | source = "registry+https://github.com/rust-lang/crates.io-index" 841 | checksum = "092474d1a01ea8278f69e6a358998405fae5b8b963ddaeb2b0b04a128bf1dfb0" 842 | 843 | [[package]] 844 | name = "ryu" 845 | version = "1.0.18" 846 | source = "registry+https://github.com/rust-lang/crates.io-index" 847 | checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f" 848 | 849 | [[package]] 850 | name = "scoped-tls" 851 | version = "1.0.1" 852 | source = "registry+https://github.com/rust-lang/crates.io-index" 853 | checksum = "e1cf6437eb19a8f4a6cc0f7dca544973b0b78843adbfeb3683d1a94a0024a294" 854 | 855 | [[package]] 856 | name = "semver" 857 | version = "1.0.23" 858 | source = "registry+https://github.com/rust-lang/crates.io-index" 859 | checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" 860 | dependencies = [ 861 | "serde", 862 | ] 863 | 864 | [[package]] 865 | name = "serde" 866 | version = "1.0.201" 867 | source = "registry+https://github.com/rust-lang/crates.io-index" 868 | checksum = "780f1cebed1629e4753a1a38a3c72d30b97ec044f0aef68cb26650a3c5cf363c" 869 | dependencies = [ 870 | "serde_derive", 871 | ] 872 | 873 | [[package]] 874 | name = "serde_bencode" 875 | version = "0.2.4" 876 | source = "registry+https://github.com/rust-lang/crates.io-index" 877 | checksum = "a70dfc7b7438b99896e7f8992363ab8e2c4ba26aa5ec675d32d1c3c2c33d413e" 878 | dependencies = [ 879 | "serde", 880 | "serde_bytes", 881 | ] 882 | 883 | [[package]] 884 | name = "serde_bytes" 885 | version = "0.11.14" 886 | source = "registry+https://github.com/rust-lang/crates.io-index" 887 | checksum = "8b8497c313fd43ab992087548117643f6fcd935cbf36f176ffda0aacf9591734" 888 | dependencies = [ 889 | "serde", 890 | ] 891 | 892 | [[package]] 893 | name = "serde_derive" 894 | version = "1.0.201" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "c5e405930b9796f1c00bee880d03fc7e0bb4b9a11afc776885ffe84320da2865" 897 | dependencies = [ 898 | "proc-macro2", 899 | "quote", 900 | "syn 2.0.61", 901 | ] 902 | 903 | [[package]] 904 | name = "serde_json" 905 | version = "1.0.117" 906 | source = "registry+https://github.com/rust-lang/crates.io-index" 907 | checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3" 908 | dependencies = [ 909 | "itoa", 910 | "ryu", 911 | "serde", 912 | ] 913 | 914 | [[package]] 915 | name = "serde_path_to_error" 916 | version = "0.1.16" 917 | source = "registry+https://github.com/rust-lang/crates.io-index" 918 | checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" 919 | dependencies = [ 920 | "itoa", 921 | "serde", 922 | ] 923 | 924 | [[package]] 925 | name = "serde_url_params" 926 | version = "0.2.1" 927 | source = "registry+https://github.com/rust-lang/crates.io-index" 928 | checksum = "2c43307d0640738af32fe8d01e47119bc0fc8a686be470a44a586caff76dfb34" 929 | dependencies = [ 930 | "serde", 931 | "url", 932 | ] 933 | 934 | [[package]] 935 | name = "serde_with" 936 | version = "3.8.1" 937 | source = "registry+https://github.com/rust-lang/crates.io-index" 938 | checksum = "0ad483d2ab0149d5a5ebcd9972a3852711e0153d863bf5a5d0391d28883c4a20" 939 | dependencies = [ 940 | "base64 0.22.1", 941 | "chrono", 942 | "hex", 943 | "indexmap 1.9.3", 944 | "indexmap 2.2.6", 945 | "serde", 946 | "serde_derive", 947 | "serde_json", 948 | "serde_with_macros", 949 | "time", 950 | ] 951 | 952 | [[package]] 953 | name = "serde_with_macros" 954 | version = "3.8.1" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "65569b702f41443e8bc8bbb1c5779bd0450bbe723b56198980e80ec45780bce2" 957 | dependencies = [ 958 | "darling", 959 | "proc-macro2", 960 | "quote", 961 | "syn 2.0.61", 962 | ] 963 | 964 | [[package]] 965 | name = "sha1" 966 | version = "0.10.6" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" 969 | dependencies = [ 970 | "cfg-if", 971 | "cpufeatures", 972 | "digest", 973 | ] 974 | 975 | [[package]] 976 | name = "sha2" 977 | version = "0.10.8" 978 | source = "registry+https://github.com/rust-lang/crates.io-index" 979 | checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" 980 | dependencies = [ 981 | "cfg-if", 982 | "cpufeatures", 983 | "digest", 984 | ] 985 | 986 | [[package]] 987 | name = "sharded-slab" 988 | version = "0.1.7" 989 | source = "registry+https://github.com/rust-lang/crates.io-index" 990 | checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" 991 | dependencies = [ 992 | "lazy_static", 993 | ] 994 | 995 | [[package]] 996 | name = "slab" 997 | version = "0.4.9" 998 | source = "registry+https://github.com/rust-lang/crates.io-index" 999 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1000 | dependencies = [ 1001 | "autocfg", 1002 | ] 1003 | 1004 | [[package]] 1005 | name = "smallvec" 1006 | version = "0.6.14" 1007 | source = "registry+https://github.com/rust-lang/crates.io-index" 1008 | checksum = "b97fcaeba89edba30f044a10c6a3cc39df9c3f17d7cd829dd1446cab35f890e0" 1009 | dependencies = [ 1010 | "maybe-uninit", 1011 | ] 1012 | 1013 | [[package]] 1014 | name = "stremio-core" 1015 | version = "0.1.0" 1016 | source = "git+https://github.com/Stremio/stremio-core?branch=development#9f5b46a48af41ac9ba8ef526733864123e434d82" 1017 | dependencies = [ 1018 | "anyhow", 1019 | "base64 0.21.7", 1020 | "boolinator", 1021 | "chrono", 1022 | "derivative", 1023 | "derive_more", 1024 | "either", 1025 | "enclose", 1026 | "flate2", 1027 | "futures", 1028 | "hex", 1029 | "http", 1030 | "itertools 0.11.0", 1031 | "lazy_static", 1032 | "lazysort", 1033 | "localsearch", 1034 | "magnet-url", 1035 | "num", 1036 | "once_cell", 1037 | "percent-encoding", 1038 | "regex", 1039 | "semver", 1040 | "serde", 1041 | "serde_bencode", 1042 | "serde_json", 1043 | "serde_path_to_error", 1044 | "serde_url_params", 1045 | "serde_with", 1046 | "sha1", 1047 | "sha2", 1048 | "stremio-derive", 1049 | "stremio-official-addons", 1050 | "stremio-serde-hex", 1051 | "stremio-watched-bitfield", 1052 | "strum", 1053 | "thiserror", 1054 | "tracing", 1055 | "url", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "stremio-core-web" 1060 | version = "0.47.7" 1061 | dependencies = [ 1062 | "Inflector", 1063 | "boolinator", 1064 | "cfg-if", 1065 | "chrono", 1066 | "console_error_panic_hook", 1067 | "either", 1068 | "enclose", 1069 | "futures", 1070 | "getrandom", 1071 | "gloo-utils", 1072 | "hex", 1073 | "http", 1074 | "itertools 0.10.5", 1075 | "js-sys", 1076 | "lazy_static", 1077 | "regex", 1078 | "semver", 1079 | "serde", 1080 | "serde_json", 1081 | "serde_path_to_error", 1082 | "stremio-core", 1083 | "tracing", 1084 | "tracing-wasm", 1085 | "url", 1086 | "wasm-bindgen", 1087 | "wasm-bindgen-futures", 1088 | "wasm-bindgen-test", 1089 | "web-sys", 1090 | ] 1091 | 1092 | [[package]] 1093 | name = "stremio-derive" 1094 | version = "0.1.0" 1095 | source = "git+https://github.com/Stremio/stremio-core?branch=development#9f5b46a48af41ac9ba8ef526733864123e434d82" 1096 | dependencies = [ 1097 | "case", 1098 | "proc-macro-crate", 1099 | "proc-macro2", 1100 | "proc_macro_roids", 1101 | "quote", 1102 | "syn 2.0.61", 1103 | ] 1104 | 1105 | [[package]] 1106 | name = "stremio-official-addons" 1107 | version = "2.0.12" 1108 | source = "registry+https://github.com/rust-lang/crates.io-index" 1109 | checksum = "5b03b5836f294463f2ad0eeebac3bcde05dcbecfefa4fc6da2029c5e15d17ad9" 1110 | dependencies = [ 1111 | "once_cell", 1112 | "serde_json", 1113 | ] 1114 | 1115 | [[package]] 1116 | name = "stremio-serde-hex" 1117 | version = "0.1.0" 1118 | source = "registry+https://github.com/rust-lang/crates.io-index" 1119 | checksum = "5a5600c4d6b97a1e6b4c0a9fa6e14d94eba885f2aa7610fc52a1557a9a140e69" 1120 | dependencies = [ 1121 | "array-init", 1122 | "serde", 1123 | "smallvec", 1124 | ] 1125 | 1126 | [[package]] 1127 | name = "stremio-watched-bitfield" 1128 | version = "0.1.0" 1129 | source = "git+https://github.com/Stremio/stremio-core?branch=development#9f5b46a48af41ac9ba8ef526733864123e434d82" 1130 | dependencies = [ 1131 | "base64 0.13.1", 1132 | "flate2", 1133 | "serde", 1134 | ] 1135 | 1136 | [[package]] 1137 | name = "strsim" 1138 | version = "0.10.0" 1139 | source = "registry+https://github.com/rust-lang/crates.io-index" 1140 | checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623" 1141 | 1142 | [[package]] 1143 | name = "strum" 1144 | version = "0.25.0" 1145 | source = "registry+https://github.com/rust-lang/crates.io-index" 1146 | checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" 1147 | dependencies = [ 1148 | "strum_macros", 1149 | ] 1150 | 1151 | [[package]] 1152 | name = "strum_macros" 1153 | version = "0.25.3" 1154 | source = "registry+https://github.com/rust-lang/crates.io-index" 1155 | checksum = "23dc1fa9ac9c169a78ba62f0b841814b7abae11bdd047b9c58f893439e309ea0" 1156 | dependencies = [ 1157 | "heck", 1158 | "proc-macro2", 1159 | "quote", 1160 | "rustversion", 1161 | "syn 2.0.61", 1162 | ] 1163 | 1164 | [[package]] 1165 | name = "syn" 1166 | version = "1.0.109" 1167 | source = "registry+https://github.com/rust-lang/crates.io-index" 1168 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1169 | dependencies = [ 1170 | "proc-macro2", 1171 | "quote", 1172 | "unicode-ident", 1173 | ] 1174 | 1175 | [[package]] 1176 | name = "syn" 1177 | version = "2.0.61" 1178 | source = "registry+https://github.com/rust-lang/crates.io-index" 1179 | checksum = "c993ed8ccba56ae856363b1845da7266a7cb78e1d146c8a32d54b45a8b831fc9" 1180 | dependencies = [ 1181 | "proc-macro2", 1182 | "quote", 1183 | "unicode-ident", 1184 | ] 1185 | 1186 | [[package]] 1187 | name = "thiserror" 1188 | version = "1.0.60" 1189 | source = "registry+https://github.com/rust-lang/crates.io-index" 1190 | checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18" 1191 | dependencies = [ 1192 | "thiserror-impl", 1193 | ] 1194 | 1195 | [[package]] 1196 | name = "thiserror-impl" 1197 | version = "1.0.60" 1198 | source = "registry+https://github.com/rust-lang/crates.io-index" 1199 | checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524" 1200 | dependencies = [ 1201 | "proc-macro2", 1202 | "quote", 1203 | "syn 2.0.61", 1204 | ] 1205 | 1206 | [[package]] 1207 | name = "thread_local" 1208 | version = "1.1.8" 1209 | source = "registry+https://github.com/rust-lang/crates.io-index" 1210 | checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" 1211 | dependencies = [ 1212 | "cfg-if", 1213 | "once_cell", 1214 | ] 1215 | 1216 | [[package]] 1217 | name = "time" 1218 | version = "0.3.36" 1219 | source = "registry+https://github.com/rust-lang/crates.io-index" 1220 | checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" 1221 | dependencies = [ 1222 | "deranged", 1223 | "itoa", 1224 | "num-conv", 1225 | "powerfmt", 1226 | "serde", 1227 | "time-core", 1228 | "time-macros", 1229 | ] 1230 | 1231 | [[package]] 1232 | name = "time-core" 1233 | version = "0.1.2" 1234 | source = "registry+https://github.com/rust-lang/crates.io-index" 1235 | checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" 1236 | 1237 | [[package]] 1238 | name = "time-macros" 1239 | version = "0.2.18" 1240 | source = "registry+https://github.com/rust-lang/crates.io-index" 1241 | checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" 1242 | dependencies = [ 1243 | "num-conv", 1244 | "time-core", 1245 | ] 1246 | 1247 | [[package]] 1248 | name = "tinyvec" 1249 | version = "1.6.0" 1250 | source = "registry+https://github.com/rust-lang/crates.io-index" 1251 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1252 | dependencies = [ 1253 | "tinyvec_macros", 1254 | ] 1255 | 1256 | [[package]] 1257 | name = "tinyvec_macros" 1258 | version = "0.1.1" 1259 | source = "registry+https://github.com/rust-lang/crates.io-index" 1260 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1261 | 1262 | [[package]] 1263 | name = "toml_datetime" 1264 | version = "0.6.5" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1" 1267 | 1268 | [[package]] 1269 | name = "toml_edit" 1270 | version = "0.19.15" 1271 | source = "registry+https://github.com/rust-lang/crates.io-index" 1272 | checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" 1273 | dependencies = [ 1274 | "indexmap 2.2.6", 1275 | "toml_datetime", 1276 | "winnow", 1277 | ] 1278 | 1279 | [[package]] 1280 | name = "tracing" 1281 | version = "0.1.40" 1282 | source = "registry+https://github.com/rust-lang/crates.io-index" 1283 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1284 | dependencies = [ 1285 | "pin-project-lite", 1286 | "tracing-attributes", 1287 | "tracing-core", 1288 | ] 1289 | 1290 | [[package]] 1291 | name = "tracing-attributes" 1292 | version = "0.1.27" 1293 | source = "registry+https://github.com/rust-lang/crates.io-index" 1294 | checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" 1295 | dependencies = [ 1296 | "proc-macro2", 1297 | "quote", 1298 | "syn 2.0.61", 1299 | ] 1300 | 1301 | [[package]] 1302 | name = "tracing-core" 1303 | version = "0.1.32" 1304 | source = "registry+https://github.com/rust-lang/crates.io-index" 1305 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1306 | dependencies = [ 1307 | "once_cell", 1308 | ] 1309 | 1310 | [[package]] 1311 | name = "tracing-subscriber" 1312 | version = "0.3.18" 1313 | source = "registry+https://github.com/rust-lang/crates.io-index" 1314 | checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" 1315 | dependencies = [ 1316 | "sharded-slab", 1317 | "thread_local", 1318 | "tracing-core", 1319 | ] 1320 | 1321 | [[package]] 1322 | name = "tracing-wasm" 1323 | version = "0.2.1" 1324 | source = "registry+https://github.com/rust-lang/crates.io-index" 1325 | checksum = "4575c663a174420fa2d78f4108ff68f65bf2fbb7dd89f33749b6e826b3626e07" 1326 | dependencies = [ 1327 | "tracing", 1328 | "tracing-subscriber", 1329 | "wasm-bindgen", 1330 | ] 1331 | 1332 | [[package]] 1333 | name = "typenum" 1334 | version = "1.17.0" 1335 | source = "registry+https://github.com/rust-lang/crates.io-index" 1336 | checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" 1337 | 1338 | [[package]] 1339 | name = "unicode-bidi" 1340 | version = "0.3.15" 1341 | source = "registry+https://github.com/rust-lang/crates.io-index" 1342 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1343 | 1344 | [[package]] 1345 | name = "unicode-ident" 1346 | version = "1.0.12" 1347 | source = "registry+https://github.com/rust-lang/crates.io-index" 1348 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1349 | 1350 | [[package]] 1351 | name = "unicode-normalization" 1352 | version = "0.1.23" 1353 | source = "registry+https://github.com/rust-lang/crates.io-index" 1354 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1355 | dependencies = [ 1356 | "tinyvec", 1357 | ] 1358 | 1359 | [[package]] 1360 | name = "url" 1361 | version = "2.4.1" 1362 | source = "registry+https://github.com/rust-lang/crates.io-index" 1363 | checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" 1364 | dependencies = [ 1365 | "form_urlencoded", 1366 | "idna", 1367 | "percent-encoding", 1368 | "serde", 1369 | ] 1370 | 1371 | [[package]] 1372 | name = "utf8-ranges" 1373 | version = "1.0.5" 1374 | source = "registry+https://github.com/rust-lang/crates.io-index" 1375 | checksum = "7fcfc827f90e53a02eaef5e535ee14266c1d569214c6aa70133a624d8a3164ba" 1376 | 1377 | [[package]] 1378 | name = "version_check" 1379 | version = "0.9.4" 1380 | source = "registry+https://github.com/rust-lang/crates.io-index" 1381 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1382 | 1383 | [[package]] 1384 | name = "wasi" 1385 | version = "0.11.0+wasi-snapshot-preview1" 1386 | source = "registry+https://github.com/rust-lang/crates.io-index" 1387 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1388 | 1389 | [[package]] 1390 | name = "wasm-bindgen" 1391 | version = "0.2.78" 1392 | source = "registry+https://github.com/rust-lang/crates.io-index" 1393 | checksum = "632f73e236b219150ea279196e54e610f5dbafa5d61786303d4da54f84e47fce" 1394 | dependencies = [ 1395 | "cfg-if", 1396 | "serde", 1397 | "serde_json", 1398 | "wasm-bindgen-macro", 1399 | ] 1400 | 1401 | [[package]] 1402 | name = "wasm-bindgen-backend" 1403 | version = "0.2.78" 1404 | source = "registry+https://github.com/rust-lang/crates.io-index" 1405 | checksum = "a317bf8f9fba2476b4b2c85ef4c4af8ff39c3c7f0cdfeed4f82c34a880aa837b" 1406 | dependencies = [ 1407 | "bumpalo", 1408 | "lazy_static", 1409 | "log", 1410 | "proc-macro2", 1411 | "quote", 1412 | "syn 1.0.109", 1413 | "wasm-bindgen-shared", 1414 | ] 1415 | 1416 | [[package]] 1417 | name = "wasm-bindgen-futures" 1418 | version = "0.4.28" 1419 | source = "registry+https://github.com/rust-lang/crates.io-index" 1420 | checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39" 1421 | dependencies = [ 1422 | "cfg-if", 1423 | "js-sys", 1424 | "wasm-bindgen", 1425 | "web-sys", 1426 | ] 1427 | 1428 | [[package]] 1429 | name = "wasm-bindgen-macro" 1430 | version = "0.2.78" 1431 | source = "registry+https://github.com/rust-lang/crates.io-index" 1432 | checksum = "d56146e7c495528bf6587663bea13a8eb588d39b36b679d83972e1a2dbbdacf9" 1433 | dependencies = [ 1434 | "quote", 1435 | "wasm-bindgen-macro-support", 1436 | ] 1437 | 1438 | [[package]] 1439 | name = "wasm-bindgen-macro-support" 1440 | version = "0.2.78" 1441 | source = "registry+https://github.com/rust-lang/crates.io-index" 1442 | checksum = "7803e0eea25835f8abdc585cd3021b3deb11543c6fe226dcd30b228857c5c5ab" 1443 | dependencies = [ 1444 | "proc-macro2", 1445 | "quote", 1446 | "syn 1.0.109", 1447 | "wasm-bindgen-backend", 1448 | "wasm-bindgen-shared", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "wasm-bindgen-shared" 1453 | version = "0.2.78" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "0237232789cf037d5480773fe568aac745bfe2afbc11a863e97901780a6b47cc" 1456 | 1457 | [[package]] 1458 | name = "wasm-bindgen-test" 1459 | version = "0.3.28" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "96f1aa7971fdf61ef0f353602102dbea75a56e225ed036c1e3740564b91e6b7e" 1462 | dependencies = [ 1463 | "console_error_panic_hook", 1464 | "js-sys", 1465 | "scoped-tls", 1466 | "wasm-bindgen", 1467 | "wasm-bindgen-futures", 1468 | "wasm-bindgen-test-macro", 1469 | ] 1470 | 1471 | [[package]] 1472 | name = "wasm-bindgen-test-macro" 1473 | version = "0.3.28" 1474 | source = "registry+https://github.com/rust-lang/crates.io-index" 1475 | checksum = "6006f79628dfeb96a86d4db51fbf1344cd7fd8408f06fc9aa3c84913a4789688" 1476 | dependencies = [ 1477 | "proc-macro2", 1478 | "quote", 1479 | ] 1480 | 1481 | [[package]] 1482 | name = "web-sys" 1483 | version = "0.3.55" 1484 | source = "registry+https://github.com/rust-lang/crates.io-index" 1485 | checksum = "38eb105f1c59d9eaa6b5cdc92b859d85b926e82cb2e0945cd0c9259faa6fe9fb" 1486 | dependencies = [ 1487 | "js-sys", 1488 | "wasm-bindgen", 1489 | ] 1490 | 1491 | [[package]] 1492 | name = "windows-core" 1493 | version = "0.52.0" 1494 | source = "registry+https://github.com/rust-lang/crates.io-index" 1495 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 1496 | dependencies = [ 1497 | "windows-targets", 1498 | ] 1499 | 1500 | [[package]] 1501 | name = "windows-targets" 1502 | version = "0.52.5" 1503 | source = "registry+https://github.com/rust-lang/crates.io-index" 1504 | checksum = "6f0713a46559409d202e70e28227288446bf7841d3211583a4b53e3f6d96e7eb" 1505 | dependencies = [ 1506 | "windows_aarch64_gnullvm", 1507 | "windows_aarch64_msvc", 1508 | "windows_i686_gnu", 1509 | "windows_i686_gnullvm", 1510 | "windows_i686_msvc", 1511 | "windows_x86_64_gnu", 1512 | "windows_x86_64_gnullvm", 1513 | "windows_x86_64_msvc", 1514 | ] 1515 | 1516 | [[package]] 1517 | name = "windows_aarch64_gnullvm" 1518 | version = "0.52.5" 1519 | source = "registry+https://github.com/rust-lang/crates.io-index" 1520 | checksum = "7088eed71e8b8dda258ecc8bac5fb1153c5cffaf2578fc8ff5d61e23578d3263" 1521 | 1522 | [[package]] 1523 | name = "windows_aarch64_msvc" 1524 | version = "0.52.5" 1525 | source = "registry+https://github.com/rust-lang/crates.io-index" 1526 | checksum = "9985fd1504e250c615ca5f281c3f7a6da76213ebd5ccc9561496568a2752afb6" 1527 | 1528 | [[package]] 1529 | name = "windows_i686_gnu" 1530 | version = "0.52.5" 1531 | source = "registry+https://github.com/rust-lang/crates.io-index" 1532 | checksum = "88ba073cf16d5372720ec942a8ccbf61626074c6d4dd2e745299726ce8b89670" 1533 | 1534 | [[package]] 1535 | name = "windows_i686_gnullvm" 1536 | version = "0.52.5" 1537 | source = "registry+https://github.com/rust-lang/crates.io-index" 1538 | checksum = "87f4261229030a858f36b459e748ae97545d6f1ec60e5e0d6a3d32e0dc232ee9" 1539 | 1540 | [[package]] 1541 | name = "windows_i686_msvc" 1542 | version = "0.52.5" 1543 | source = "registry+https://github.com/rust-lang/crates.io-index" 1544 | checksum = "db3c2bf3d13d5b658be73463284eaf12830ac9a26a90c717b7f771dfe97487bf" 1545 | 1546 | [[package]] 1547 | name = "windows_x86_64_gnu" 1548 | version = "0.52.5" 1549 | source = "registry+https://github.com/rust-lang/crates.io-index" 1550 | checksum = "4e4246f76bdeff09eb48875a0fd3e2af6aada79d409d33011886d3e1581517d9" 1551 | 1552 | [[package]] 1553 | name = "windows_x86_64_gnullvm" 1554 | version = "0.52.5" 1555 | source = "registry+https://github.com/rust-lang/crates.io-index" 1556 | checksum = "852298e482cd67c356ddd9570386e2862b5673c85bd5f88df9ab6802b334c596" 1557 | 1558 | [[package]] 1559 | name = "windows_x86_64_msvc" 1560 | version = "0.52.5" 1561 | source = "registry+https://github.com/rust-lang/crates.io-index" 1562 | checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0" 1563 | 1564 | [[package]] 1565 | name = "winnow" 1566 | version = "0.5.40" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "f593a95398737aeed53e489c785df13f3618e41dbcd6718c6addbf1395aa6876" 1569 | dependencies = [ 1570 | "memchr", 1571 | ] 1572 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "stremio-core-web" 3 | version = "0.47.7" 4 | authors = ["Smart Code OOD"] 5 | edition = "2021" 6 | resolver = "2" 7 | 8 | [lib] 9 | crate-type = ["cdylib"] 10 | 11 | [profile.release] 12 | lto = true 13 | opt-level = 's' 14 | 15 | [features] 16 | default = [] 17 | # enable TRACE level of logging in the crate using `tracing`. 18 | log-trace = [] 19 | 20 | [dependencies] 21 | stremio-core = { git = "https://github.com/Stremio/stremio-core", features = ["derive", "analytics"], branch = "development" } 22 | serde = { version = "1.0.*", features = ["derive"] } 23 | serde_json = "1.0.*" 24 | futures = "0.3.*" 25 | http = "0.2.*" 26 | url = { version = "2.4.*", features = ["serde"] } 27 | chrono = "0.4.*" 28 | semver = { version = "1", features = ["serde"] } 29 | regex = "1.8" 30 | hex = "0.4.*" 31 | either = "1.6.*" 32 | lazy_static = "1.4.*" 33 | enclose = "1.1.*" 34 | itertools = "0.10.*" 35 | boolinator = "2.4.*" 36 | Inflector = "0.11.*" 37 | wasm-bindgen = { version = "=0.2.78", features = ["serde-serialize"] } 38 | wasm-bindgen-futures = "0.4" 39 | gloo-utils = { version = "0.2", features = ["serde"] } 40 | 41 | # panic hook for wasm 42 | console_error_panic_hook = "0.1.*" 43 | 44 | js-sys = "0.3" 45 | web-sys = { version = "0.3", features = [ 46 | "WorkerGlobalScope", 47 | "WorkerNavigator", 48 | "Request", 49 | "RequestInit", 50 | "Response", 51 | "console", 52 | ] } 53 | getrandom = { version = "0.2.*", features = ["js"] } 54 | cfg-if = "1.0" 55 | serde_path_to_error = "0.1.*" 56 | 57 | # Tracing 58 | tracing = "0.1" 59 | tracing-wasm = "0.2" 60 | 61 | [dev-dependencies] 62 | wasm-bindgen-test = "0.3.0" 63 | 64 | # A way to quickly test with local version of `core` crates 65 | # [patch.'https://github.com/Stremio/stremio-core'] 66 | # stremio-core = { path = "../core" } 67 | 68 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Stremio Core Web 2 | 3 | > [!WARNING] 4 | >Moved to https://github.com/Stremio/stremio-core/tree/development/stremio-core-web 5 | 6 | [![npm](https://img.shields.io/npm/v/@stremio/stremio-core-web?style=flat-square)](https://www.npmjs.com/package/@stremio/stremio-core-web) 7 | 8 | Bridge between [stremio-core](https://github.com/stremio/stremio-core) and [stremio-web](https://github.com/stremio/stremio-web) 9 | 10 | 11 | ## Build 12 | 13 | Builds a production wasm package and prepares the rest of the dependencies for the npm package. 14 | 15 | ```bash 16 | npm install 17 | npm run build 18 | ``` 19 | 20 | ### Development 21 | 22 | Building the package using [`./scripts/build.sh`](./scripts/build.sh) with `--dev` would allow you to see more logging messages being emitted, this is intended **only** for debugging as it will log messages with sensitive information! 23 | 24 | ```bash 25 | ./scripts/build.sh --dev 26 | ``` 27 | 28 | Or you can also use the development-specific Rust's `wasm-watch` alias from [`./.cargo/config.toml`](./.cargo/config.toml). 29 | It will automatically re-compile the package when a change on the files or dependencies is detected, 30 | including when you're using a local patch for `stremio-core`. 31 | 32 | 1. Install `cargo-watch` 33 | - `cargo install cargo-watch` 34 | - With `cargo-binstall` (prebuilt binaries): `cargo binstall cargo-watch` 35 | 2. Run `cargo wasm-watch` 36 | 37 | ## Publishing 38 | 39 | 1. Update version to the next minor/major/patch version in Cargo (`Cargo.toml` and `Cargo.lock`) and npm (`package.json` and `package-lock.json`), e.g. from `0.44.13` to `0.44.14`. 40 | 2. Commit the change with the new version as a message, e.g. `0.44.14` 41 | 3. Wait for CI to build successfully 42 | 4. Push a new tag starting with `v`, e.g. `git tag v0.44.14` `git push origin v0.44.14` 43 | 5. Create a [new Release](https://github.com/Stremio/stremio-core-web/releases/new) with the created tag and the tag name as a title, e.g. `v0.44.14` 44 | 6. Publish the Release 45 | 7. CI will automatically build and release the `npm` package to the registry 46 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@stremio/stremio-core-web", 3 | "version": "0.47.7", 4 | "description": "Bridge between stremio-core and stremio-web", 5 | "author": "Smart Code OOD", 6 | "main": "stremio_core_web.js", 7 | "license": "MIT", 8 | "repository": { 9 | "type": "git", 10 | "url": "https://github.com/stremio/stremio-core-web.git" 11 | }, 12 | "scripts": { 13 | "build": "./scripts/build.sh" 14 | }, 15 | "dependencies": { 16 | "@babel/runtime": "7.24.1" 17 | }, 18 | "devDependencies": { 19 | "@babel/cli": "7.24.1", 20 | "@babel/core": "7.24.3", 21 | "@babel/plugin-transform-runtime": "7.24.3", 22 | "@babel/preset-env": "7.24.3", 23 | "babel-plugin-bundled-import-meta": "0.3.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /rust-toolchain.toml: -------------------------------------------------------------------------------- 1 | [toolchain] 2 | # 1.78 introduces a check which fails on the older 3 | # version of wasm-bindgen, while the newest wasm-bindgen 4 | # introduces an unknown panic when transpiled using 5 | # wasm2js 6 | channel = "1.77" -------------------------------------------------------------------------------- /scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -ex 3 | MODE=${1:---release} 4 | wasm-pack build --no-typescript --no-pack --out-dir wasm_build $MODE --target web 5 | mv ./wasm_build/stremio_core_web_bg.wasm stremio_core_web_bg.wasm 6 | npx babel wasm_build/stremio_core_web.js --config-file ./.babelrc --out-file stremio_core_web.js 7 | npx babel src/bridge.js --config-file ./.babelrc --out-file bridge.js 8 | npx babel src/worker.js --config-file ./.babelrc --out-file worker.js 9 | -------------------------------------------------------------------------------- /src/bridge.js: -------------------------------------------------------------------------------- 1 | function getId() { 2 | return Math.random().toString(32).slice(2); 3 | } 4 | 5 | function Bridge(scope, handler) { 6 | handler.addEventListener('message', async ({ data: { request } }) => { 7 | if (!request) return; 8 | 9 | const { id, path, args } = request; 10 | try { 11 | const value = path.reduce((value, prop) => value[prop], scope); 12 | let data; 13 | if (typeof value === 'function') { 14 | const thisArg = path.slice(0, path.length - 1).reduce((value, prop) => value[prop], scope); 15 | data = await value.apply(thisArg, args); 16 | } else { 17 | data = await value; 18 | } 19 | 20 | handler.postMessage({ response: { id, result: { data } } }); 21 | } catch (error) { 22 | handler.postMessage({ response: { id, result: { error } } }); 23 | } 24 | }); 25 | 26 | this.call = async (path, args) => { 27 | const id = getId(); 28 | return new Promise((resolve, reject) => { 29 | const onMessage = ({ data: { response } }) => { 30 | if (!response || response.id !== id) return; 31 | 32 | handler.removeEventListener('message', onMessage); 33 | if ('error' in response.result) { 34 | reject(response.result.error); 35 | } else { 36 | resolve(response.result.data); 37 | } 38 | }; 39 | handler.addEventListener('message', onMessage); 40 | handler.postMessage({ request: { id, path, args } }); 41 | }); 42 | }; 43 | } 44 | 45 | module.exports = Bridge; 46 | -------------------------------------------------------------------------------- /src/env.rs: -------------------------------------------------------------------------------- 1 | use std::{collections::HashMap, sync::RwLock}; 2 | 3 | use chrono::{offset::TimeZone, DateTime, Utc}; 4 | use futures::{future, Future, FutureExt, TryFutureExt}; 5 | use gloo_utils::format::JsValueSerdeExt; 6 | use http::{Method, Request}; 7 | use lazy_static::lazy_static; 8 | use regex::Regex; 9 | use serde::{Deserialize, Serialize}; 10 | use serde_json::json; 11 | 12 | use tracing::trace; 13 | use url::Url; 14 | 15 | use wasm_bindgen::{closure::Closure, prelude::wasm_bindgen, JsCast, JsValue}; 16 | use wasm_bindgen_futures::{spawn_local, JsFuture}; 17 | use web_sys::WorkerGlobalScope; 18 | 19 | use stremio_core::{ 20 | analytics::Analytics, 21 | models::{ctx::Ctx, streaming_server::StreamingServer}, 22 | runtime::{ 23 | msg::{Action, ActionCtx, Event}, 24 | Env, EnvError, EnvFuture, EnvFutureExt, TryEnvFuture, 25 | }, 26 | types::{api::AuthRequest, resource::StreamSource}, 27 | }; 28 | 29 | use crate::{ 30 | event::{UIEvent, WebEvent}, 31 | model::WebModel, 32 | }; 33 | 34 | const UNKNOWN_ERROR: &str = "Unknown Error"; 35 | const INSTALLATION_ID_STORAGE_KEY: &str = "installation_id"; 36 | 37 | #[wasm_bindgen] 38 | extern "C" { 39 | #[wasm_bindgen(js_namespace = ["self"], js_name = app_version)] 40 | static APP_VERSION: String; 41 | #[wasm_bindgen(js_namespace = ["self"], js_name = shell_version)] 42 | static SHELL_VERSION: Option; 43 | #[wasm_bindgen(catch, js_namespace = ["self"])] 44 | async fn get_location_hash() -> Result; 45 | #[wasm_bindgen(catch, js_namespace = ["self"])] 46 | async fn local_storage_get_item(key: String) -> Result; 47 | #[wasm_bindgen(catch, js_namespace = ["self"])] 48 | async fn local_storage_set_item(key: String, value: String) -> Result<(), JsValue>; 49 | #[wasm_bindgen(catch, js_namespace = ["self"])] 50 | async fn local_storage_remove_item(key: String) -> Result<(), JsValue>; 51 | } 52 | 53 | lazy_static! { 54 | static ref INSTALLATION_ID: RwLock> = Default::default(); 55 | static ref VISIT_ID: String = hex::encode(WebEnv::random_buffer(10)); 56 | static ref ANALYTICS: Analytics = Default::default(); 57 | static ref PLAYER_REGEX: Regex = 58 | Regex::new(r"^/player/([^/]*)(?:/([^/]*)/([^/]*)/([^/]*)/([^/]*)/([^/]*))?$") 59 | .expect("Player Regex failed to build"); 60 | } 61 | 62 | #[derive(Serialize)] 63 | #[serde(rename_all = "camelCase")] 64 | struct AnalyticsContext { 65 | app_type: String, 66 | app_version: String, 67 | server_version: Option, 68 | shell_version: Option, 69 | system_language: Option, 70 | app_language: String, 71 | #[serde(rename = "installationID")] 72 | installation_id: String, 73 | #[serde(rename = "visitID")] 74 | visit_id: String, 75 | #[serde(rename = "url")] 76 | path: String, 77 | } 78 | 79 | pub enum WebEnv {} 80 | 81 | impl WebEnv { 82 | /// Sets panic hook, enables logging 83 | pub fn init() -> TryEnvFuture<()> { 84 | WebEnv::migrate_storage_schema() 85 | .inspect(|migration_result| trace!("Migration result: {migration_result:?}",)) 86 | .and_then(|_| WebEnv::get_storage::(INSTALLATION_ID_STORAGE_KEY)) 87 | .inspect(|installation_id_result| trace!("Migration: {installation_id_result:?}")) 88 | .map_ok(|installation_id| { 89 | installation_id.or_else(|| Some(hex::encode(WebEnv::random_buffer(10)))) 90 | }) 91 | .and_then(|installation_id| { 92 | *INSTALLATION_ID 93 | .write() 94 | .expect("installation id write failed") = installation_id; 95 | WebEnv::set_storage( 96 | INSTALLATION_ID_STORAGE_KEY, 97 | Some(&*INSTALLATION_ID.read().expect("installation id read failed")), 98 | ) 99 | }) 100 | .inspect_ok(|_| { 101 | WebEnv::set_interval( 102 | || WebEnv::exec_concurrent(WebEnv::send_next_analytics_batch()), 103 | 30 * 1000, 104 | ); 105 | }) 106 | .boxed_local() 107 | } 108 | pub fn get_location_hash() -> EnvFuture<'static, String> { 109 | get_location_hash() 110 | .map(|location_hash| { 111 | location_hash 112 | .ok() 113 | .and_then(|location_hash| location_hash.as_string()) 114 | .unwrap_or_default() 115 | }) 116 | .boxed_env() 117 | } 118 | pub fn emit_to_analytics(event: &WebEvent, model: &WebModel, path: &str) { 119 | let (name, data) = match event { 120 | WebEvent::UIEvent(UIEvent::LocationPathChanged { prev_path }) => ( 121 | "stateChange".to_owned(), 122 | json!({ "previousURL": sanitize_location_path(prev_path) }), 123 | ), 124 | WebEvent::UIEvent(UIEvent::Search { 125 | query, 126 | responses_count, 127 | }) => ( 128 | "search".to_owned(), 129 | json!({ "query": query, "rows": responses_count }), 130 | ), 131 | WebEvent::UIEvent(UIEvent::Share { url }) => { 132 | ("share".to_owned(), json!({ "url": url })) 133 | } 134 | WebEvent::UIEvent(UIEvent::StreamClicked { stream }) => ( 135 | "streamClicked".to_owned(), 136 | json!({ 137 | "type": match &stream.source { 138 | StreamSource::Url { .. } => "Url", 139 | StreamSource::YouTube { .. } => "YouTube", 140 | StreamSource::Torrent { .. } => "Torrent", 141 | StreamSource::Rar { .. } => "Rar", 142 | StreamSource::Zip { .. } => "Zip", 143 | StreamSource::External { .. } => "External", 144 | StreamSource::PlayerFrame { .. } => "PlayerFrame" 145 | } 146 | }), 147 | ), 148 | WebEvent::CoreEvent(core_event) => match core_event.as_ref() { 149 | Event::UserAuthenticated { auth_request } => ( 150 | "login".to_owned(), 151 | json!({ 152 | "type": match auth_request { 153 | AuthRequest::Login { facebook, .. } if *facebook => "facebook", 154 | AuthRequest::Login { .. } => "login", 155 | AuthRequest::Facebook { .. } => "authWithFacebook", 156 | AuthRequest::LoginWithToken { .. } => "loginWithToken", 157 | AuthRequest::Register { .. } => "register", 158 | }, 159 | }), 160 | ), 161 | Event::AddonInstalled { transport_url, id } => ( 162 | "installAddon".to_owned(), 163 | json!({ 164 | "addonTransportUrl": transport_url, 165 | "addonID": id 166 | }), 167 | ), 168 | Event::AddonUninstalled { transport_url, id } => ( 169 | "removeAddon".to_owned(), 170 | json!({ 171 | "addonTransportUrl": transport_url, 172 | "addonID": id 173 | }), 174 | ), 175 | Event::PlayerPlaying { load_time, context } => ( 176 | "playerPlaying".to_owned(), 177 | json!({ 178 | "loadTime": load_time, 179 | "player": context 180 | }), 181 | ), 182 | Event::PlayerStopped { context } => { 183 | ("playerStopped".to_owned(), json!({ "player": context })) 184 | } 185 | Event::PlayerEnded { 186 | context, 187 | is_binge_enabled, 188 | is_playing_next_video, 189 | } => ( 190 | "playerEnded".to_owned(), 191 | json!({ 192 | "player": context, 193 | "isBingeEnabled": is_binge_enabled, 194 | "isPlayingNextVideo": is_playing_next_video 195 | }), 196 | ), 197 | Event::TraktPlaying { context } => { 198 | ("traktPlaying".to_owned(), json!({ "player": context })) 199 | } 200 | Event::TraktPaused { context } => { 201 | ("traktPaused".to_owned(), json!({ "player": context })) 202 | } 203 | _ => return, 204 | }, 205 | WebEvent::CoreAction(core_action) => match core_action.as_ref() { 206 | Action::Ctx(ActionCtx::AddToLibrary(meta_preview)) => { 207 | let library_item = model.ctx.library.items.get(&meta_preview.id); 208 | ( 209 | "addToLib".to_owned(), 210 | json!({ 211 | "libItemID": &meta_preview.id, 212 | "libItemType": &meta_preview.r#type, 213 | "libItemName": &meta_preview.name, 214 | "wasTemp": library_item.map(|library_item| library_item.temp).unwrap_or_default(), 215 | "isReadded": library_item.map(|library_item| library_item.removed).unwrap_or_default(), 216 | }), 217 | ) 218 | } 219 | Action::Ctx(ActionCtx::RemoveFromLibrary(id)) => { 220 | match model.ctx.library.items.get(id) { 221 | Some(library_item) => ( 222 | "removeFromLib".to_owned(), 223 | json!({ 224 | "libItemID": &library_item.id, 225 | "libItemType": &library_item.r#type, 226 | "libItemName": &library_item.name, 227 | }), 228 | ), 229 | _ => return, 230 | } 231 | } 232 | Action::Ctx(ActionCtx::Logout) => ("logout".to_owned(), serde_json::Value::Null), 233 | _ => return, 234 | }, 235 | }; 236 | ANALYTICS.emit(name, data, &model.ctx, &model.streaming_server, path); 237 | } 238 | pub fn send_next_analytics_batch() -> impl Future { 239 | ANALYTICS.send_next_batch() 240 | } 241 | pub fn set_interval(func: F, timeout: i32) -> i32 { 242 | let func = Closure::wrap(Box::new(func) as Box); 243 | let interval_id = global() 244 | .set_interval_with_callback_and_timeout_and_arguments_0( 245 | func.as_ref().unchecked_ref(), 246 | timeout, 247 | ) 248 | .expect("set interval failed"); 249 | func.forget(); 250 | interval_id 251 | } 252 | #[allow(dead_code)] 253 | pub fn clear_interval(id: i32) { 254 | global().clear_interval_with_handle(id); 255 | } 256 | pub fn random_buffer(len: usize) -> Vec { 257 | let mut buffer = vec![0u8; len]; 258 | getrandom::getrandom(buffer.as_mut_slice()).expect("generate random buffer failed"); 259 | buffer 260 | } 261 | } 262 | 263 | impl Env for WebEnv { 264 | fn fetch(request: Request) -> TryEnvFuture 265 | where 266 | IN: Serialize, 267 | for<'de> OUT: Deserialize<'de> + 'static, 268 | { 269 | let (parts, body) = request.into_parts(); 270 | let url = parts.uri.to_string(); 271 | let method = parts.method.as_str(); 272 | let headers = { 273 | let mut headers = HashMap::new(); 274 | for (key, value) in parts.headers.iter() { 275 | let key = key.as_str().to_owned(); 276 | let value = String::from_utf8_lossy(value.as_bytes()).into_owned(); 277 | headers.entry(key).or_insert_with(Vec::new).push(value); 278 | } 279 | ::from_serde(&headers) 280 | .expect("WebEnv::fetch: JsValue from Headers failed to be built") 281 | }; 282 | let body = match serde_json::to_string(&body) { 283 | Ok(ref body) if body != "null" && parts.method != Method::GET => { 284 | Some(JsValue::from_str(body)) 285 | } 286 | _ => None, 287 | }; 288 | let mut request_options = web_sys::RequestInit::new(); 289 | request_options 290 | .method(method) 291 | .headers(&headers) 292 | .body(body.as_ref()); 293 | 294 | let request = web_sys::Request::new_with_str_and_init(&url, &request_options) 295 | .expect("request builder failed"); 296 | let promise = global().fetch_with_request(&request); 297 | async { 298 | let resp = JsFuture::from(promise).await.map_err(|error| { 299 | EnvError::Fetch( 300 | error 301 | .dyn_into::() 302 | .map(|error| String::from(error.message())) 303 | .unwrap_or_else(|_| UNKNOWN_ERROR.to_owned()), 304 | ) 305 | })?; 306 | 307 | let resp = resp 308 | .dyn_into::() 309 | .expect("WebEnv::fetch: Response into web_sys::Response failed to be built"); 310 | // status check and JSON extraction from response. 311 | let resp = if resp.status() != 200 { 312 | return Err(EnvError::Fetch(format!( 313 | "Unexpected HTTP status code {}", 314 | resp.status(), 315 | ))); 316 | } else { 317 | // Response.json() to JSON::Stringify 318 | 319 | JsFuture::from( 320 | resp.text() 321 | .expect("WebEnv::fetch: Response text failed to be retrieved"), 322 | ) 323 | .map_err(|error| { 324 | EnvError::Fetch( 325 | error 326 | .dyn_into::() 327 | .map(|error| String::from(error.message())) 328 | .unwrap_or_else(|_| UNKNOWN_ERROR.to_owned()), 329 | ) 330 | }) 331 | .await 332 | .and_then(|js_value| { 333 | js_value.dyn_into::().map_err(|error| { 334 | EnvError::Fetch( 335 | error 336 | .dyn_into::() 337 | .map(|error| String::from(error.message())) 338 | .unwrap_or_else(|_| UNKNOWN_ERROR.to_owned()), 339 | ) 340 | }) 341 | })? 342 | }; 343 | 344 | response_deserialize(resp) 345 | } 346 | .boxed_local() 347 | } 348 | 349 | fn get_storage(key: &str) -> TryEnvFuture> 350 | where 351 | for<'de> T: Deserialize<'de> + 'static, 352 | { 353 | local_storage_get_item(key.to_owned()) 354 | .map_err(|error| { 355 | EnvError::StorageReadError( 356 | error 357 | .dyn_into::() 358 | .map(|error| String::from(error.message())) 359 | .unwrap_or_else(|_| UNKNOWN_ERROR.to_owned()), 360 | ) 361 | }) 362 | .and_then(|value| async move { 363 | value 364 | .as_string() 365 | .map(|value| serde_json::from_str(&value)) 366 | .transpose() 367 | .map_err(EnvError::from) 368 | }) 369 | .boxed_local() 370 | } 371 | 372 | fn set_storage(key: &str, value: Option<&T>) -> TryEnvFuture<()> { 373 | let key = key.to_owned(); 374 | match value { 375 | Some(value) => future::ready(serde_json::to_string(value)) 376 | .map_err(EnvError::from) 377 | .and_then(|value| { 378 | local_storage_set_item(key, value).map_err(|error| { 379 | EnvError::StorageWriteError( 380 | error 381 | .dyn_into::() 382 | .map(|error| String::from(error.message())) 383 | .unwrap_or_else(|_| UNKNOWN_ERROR.to_owned()), 384 | ) 385 | }) 386 | }) 387 | .boxed_local(), 388 | None => local_storage_remove_item(key) 389 | .map_err(|error| { 390 | EnvError::StorageWriteError( 391 | error 392 | .dyn_into::() 393 | .map(|error| String::from(error.message())) 394 | .unwrap_or_else(|_| UNKNOWN_ERROR.to_owned()), 395 | ) 396 | }) 397 | .boxed_local(), 398 | } 399 | } 400 | 401 | fn exec_concurrent(future: F) 402 | where 403 | F: Future + 'static, 404 | { 405 | spawn_local(future) 406 | } 407 | 408 | fn exec_sequential(future: F) 409 | where 410 | F: Future + 'static, 411 | { 412 | spawn_local(future) 413 | } 414 | 415 | fn now() -> DateTime { 416 | let msecs = js_sys::Date::now() as i64; 417 | let (secs, nsecs) = (msecs / 1000, msecs % 1000 * 1_000_000); 418 | Utc.timestamp_opt(secs, nsecs as u32) 419 | .single() 420 | .expect("Invalid timestamp") 421 | } 422 | 423 | fn flush_analytics() -> EnvFuture<'static, ()> { 424 | ANALYTICS.flush().boxed_local() 425 | } 426 | 427 | fn analytics_context( 428 | ctx: &Ctx, 429 | streaming_server: &StreamingServer, 430 | path: &str, 431 | ) -> serde_json::Value { 432 | serde_json::to_value(AnalyticsContext { 433 | app_type: "stremio-web".to_owned(), 434 | app_version: APP_VERSION.to_owned(), 435 | server_version: streaming_server 436 | .settings 437 | .as_ref() 438 | .ready() 439 | .map(|settings| settings.server_version.to_owned()), 440 | shell_version: SHELL_VERSION.to_owned(), 441 | system_language: global() 442 | .navigator() 443 | .language() 444 | .map(|language| language.to_lowercase()), 445 | app_language: ctx.profile.settings.interface_language.to_owned(), 446 | installation_id: INSTALLATION_ID 447 | .read() 448 | .expect("installation id read failed") 449 | .as_ref() 450 | .expect("installation id not available") 451 | .to_owned(), 452 | visit_id: VISIT_ID.to_owned(), 453 | path: sanitize_location_path(path), 454 | }) 455 | .expect("AnalyticsContext to JSON") 456 | } 457 | 458 | #[cfg(debug_assertions)] 459 | fn log(message: String) { 460 | web_sys::console::log_1(&JsValue::from(message)); 461 | } 462 | } 463 | 464 | fn sanitize_location_path(path: &str) -> String { 465 | match Url::parse(&format!("stremio://{}", path)) { 466 | Ok(url) => { 467 | let query = url 468 | .query() 469 | .map(|query| format!("?{}", query)) 470 | .unwrap_or_default(); 471 | let path = match PLAYER_REGEX.captures(url.path()) { 472 | Some(captures) => { 473 | match ( 474 | captures.get(3), 475 | captures.get(4), 476 | captures.get(5), 477 | captures.get(6), 478 | ) { 479 | (Some(match_3), Some(match_4), Some(match_5), Some(match_6)) => { 480 | format!( 481 | "/player/***/***/{cap_3}/{cap_4}/{cap_5}/{cap_6}", 482 | cap_3 = match_3.as_str(), 483 | cap_4 = match_4.as_str(), 484 | cap_5 = match_5.as_str(), 485 | cap_6 = match_6.as_str(), 486 | ) 487 | } 488 | _ => "/player/***".to_owned(), 489 | } 490 | } 491 | _ => url.path().to_owned(), 492 | }; 493 | format!("{}{}", path, query) 494 | } 495 | _ => path.to_owned(), 496 | } 497 | } 498 | 499 | fn global() -> WorkerGlobalScope { 500 | js_sys::global() 501 | .dyn_into::() 502 | .expect("worker global scope is not available") 503 | } 504 | 505 | fn response_deserialize(response: js_sys::JsString) -> Result 506 | where 507 | for<'de> OUT: Deserialize<'de> + 'static, 508 | { 509 | let response = Into::::into(response); 510 | let mut deserializer = serde_json::Deserializer::from_str(response.as_str()); 511 | 512 | // deserialize into the final OUT struct 513 | 514 | serde_path_to_error::deserialize::<_, OUT>(&mut deserializer) 515 | .map_err(|error| EnvError::Fetch(error.to_string())) 516 | } 517 | 518 | /// > One other difference is that the tests must be in the root of the crate, 519 | /// > or within a pub mod. Putting them inside a private module will not work. 520 | #[cfg(test)] 521 | pub mod tests { 522 | wasm_bindgen_test::wasm_bindgen_test_configure!(run_in_browser); 523 | 524 | use wasm_bindgen_test::wasm_bindgen_test; 525 | 526 | use stremio_core::{ 527 | runtime::EnvError, 528 | types::{ 529 | addon::ResourceResponse, 530 | api::{APIResult, CollectionResponse}, 531 | }, 532 | }; 533 | 534 | use super::response_deserialize; 535 | 536 | #[wasm_bindgen_test] 537 | fn test_deserialization_path_error() { 538 | let json_string = serde_json::json!({ 539 | "result": [] 540 | }) 541 | .to_string(); 542 | let result = response_deserialize::>>(json_string.into()); 543 | assert!(result.is_ok()); 544 | 545 | // Bad ApiResult response, non-existing variant 546 | { 547 | let json_string = serde_json::json!({ 548 | "unknown_variant": {"test": 1} 549 | }) 550 | .to_string(); 551 | let result = response_deserialize::>(json_string.into()); 552 | 553 | assert_eq!( 554 | result.expect_err("Should be an error"), 555 | EnvError::Fetch("unknown variant `unknown_variant`, expected `error` or `result` at line 1 column 18".to_string()), 556 | "Message does not include the text 'unknown variant `unknown_variant`, expected `error` or `result` at line 1 column 18'" 557 | ); 558 | } 559 | 560 | // Addon ResourceResponse error, bad variant values 561 | { 562 | let json_string = serde_json::json!({ 563 | "metas": {"object_key": "value"} 564 | }) 565 | .to_string(); 566 | let result = response_deserialize::(json_string.into()); 567 | 568 | assert_eq!( 569 | result.expect_err("Should be an error"), 570 | EnvError::Fetch("invalid type: map, expected a sequence".to_string()), 571 | "Message does not include the text 'Cannot deserialize as ResourceResponse'" 572 | ); 573 | } 574 | } 575 | } 576 | -------------------------------------------------------------------------------- /src/event.rs: -------------------------------------------------------------------------------- 1 | use serde::Deserialize; 2 | use stremio_core::runtime::msg::{Action, Event}; 3 | use stremio_core::types::resource::Stream; 4 | 5 | #[derive(Deserialize)] 6 | #[serde(tag = "event", content = "args")] 7 | pub enum UIEvent { 8 | #[serde(rename_all = "camelCase")] 9 | LocationPathChanged { 10 | prev_path: String, 11 | }, 12 | #[serde(rename_all = "camelCase")] 13 | Search { 14 | query: String, 15 | responses_count: u32, 16 | }, 17 | Share { 18 | url: String, 19 | }, 20 | StreamClicked { 21 | stream: Box, 22 | }, 23 | } 24 | 25 | pub enum WebEvent { 26 | CoreAction(Box), 27 | CoreEvent(Box), 28 | UIEvent(UIEvent), 29 | } 30 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[allow(clippy::module_inception)] 2 | pub mod model; 3 | 4 | pub mod env; 5 | pub mod event; 6 | pub mod stremio_core_web; 7 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/addons_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::AddonsDeepLinks; 3 | 4 | impl DeepLinksExt for AddonsDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | addons: self.addons.replace("stremio://", "#"), 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/deep_links_ext.rs: -------------------------------------------------------------------------------- 1 | pub trait DeepLinksExt { 2 | fn into_web_deep_links(self) -> Self; 3 | } 4 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/discover_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::DiscoverDeepLinks; 3 | 4 | impl DeepLinksExt for DiscoverDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | discover: self.discover.replace("stremio://", "#"), 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/library_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::LibraryDeepLinks; 3 | 4 | impl DeepLinksExt for LibraryDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | library: self.library.replace("stremio://", "#"), 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/library_item_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::LibraryItemDeepLinks; 3 | 4 | impl DeepLinksExt for LibraryItemDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | meta_details_videos: self 8 | .meta_details_videos 9 | .map(|meta_details_videos| meta_details_videos.replace("stremio://", "#")), 10 | meta_details_streams: self 11 | .meta_details_streams 12 | .map(|meta_details_streams| meta_details_streams.replace("stremio://", "#")), 13 | player: self.player.map(|player| player.replace("stremio://", "#")), 14 | external_player: self.external_player, 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/local_search_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::LocalSearchItemDeepLinks; 3 | 4 | impl DeepLinksExt for LocalSearchItemDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | search: self.search.replace("stremio://", "#"), 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/meta_item_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::MetaItemDeepLinks; 3 | 4 | impl DeepLinksExt for MetaItemDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | meta_details_videos: self 8 | .meta_details_videos 9 | .map(|meta_details_videos| meta_details_videos.replace("stremio://", "#")), 10 | meta_details_streams: self 11 | .meta_details_streams 12 | .map(|meta_details_streams| meta_details_streams.replace("stremio://", "#")), 13 | player: self.player.map(|player| player.replace("stremio://", "#")), 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/mod.rs: -------------------------------------------------------------------------------- 1 | mod deep_links_ext; 2 | pub use deep_links_ext::*; 3 | 4 | mod addons_deep_links; 5 | mod discover_deep_links; 6 | mod library_deep_links; 7 | mod library_item_deep_links; 8 | mod local_search_deep_links; 9 | mod meta_item_deep_links; 10 | mod search_history_deep_links; 11 | mod stream_deep_links; 12 | mod video_deep_links; 13 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/search_history_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::SearchHistoryItemDeepLinks; 3 | 4 | impl DeepLinksExt for SearchHistoryItemDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | search: self.search.replace("stremio://", "#"), 8 | } 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/stream_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::StreamDeepLinks; 3 | 4 | impl DeepLinksExt for StreamDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | player: self.player.replace("stremio://", "#"), 8 | external_player: self.external_player, 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/model/deep_links_ext/video_deep_links.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use stremio_core::deep_links::VideoDeepLinks; 3 | 4 | impl DeepLinksExt for VideoDeepLinks { 5 | fn into_web_deep_links(self) -> Self { 6 | Self { 7 | meta_details_videos: self.meta_details_videos.replace("stremio://", "#"), 8 | meta_details_streams: self.meta_details_streams.replace("stremio://", "#"), 9 | player: self.player.map(|player| player.replace("stremio://", "#")), 10 | external_player: self.external_player, 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/model/mod.rs: -------------------------------------------------------------------------------- 1 | pub mod deep_links_ext; 2 | 3 | mod serialize_catalogs_with_extra; 4 | use serialize_catalogs_with_extra::*; 5 | 6 | mod serialize_continue_watching_preview; 7 | use serialize_continue_watching_preview::*; 8 | 9 | mod serialize_ctx; 10 | use serialize_ctx::*; 11 | 12 | mod serialize_discover; 13 | use serialize_discover::*; 14 | 15 | mod serialize_installed_addons; 16 | use serialize_installed_addons::*; 17 | 18 | mod serialize_library; 19 | use serialize_library::*; 20 | 21 | mod serialize_local_search; 22 | use serialize_local_search::*; 23 | 24 | mod serialize_meta_details; 25 | use serialize_meta_details::*; 26 | 27 | mod serialize_player; 28 | use serialize_player::*; 29 | 30 | mod serialize_remote_addons; 31 | use serialize_remote_addons::*; 32 | 33 | mod serialize_streaming_server; 34 | use serialize_streaming_server::*; 35 | 36 | mod serialize_data_export; 37 | use serialize_data_export::*; 38 | 39 | mod model; 40 | pub use model::*; 41 | -------------------------------------------------------------------------------- /src/model/model.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::format::JsValueSerdeExt; 2 | #[cfg(debug_assertions)] 3 | use serde::Serialize; 4 | 5 | use wasm_bindgen::JsValue; 6 | 7 | use stremio_core::{ 8 | models::{ 9 | addon_details::AddonDetails, 10 | catalog_with_filters::CatalogWithFilters, 11 | catalogs_with_extra::CatalogsWithExtra, 12 | continue_watching_preview::ContinueWatchingPreview, 13 | ctx::Ctx, 14 | data_export::DataExport, 15 | installed_addons_with_filters::InstalledAddonsWithFilters, 16 | library_with_filters::{ContinueWatchingFilter, LibraryWithFilters, NotRemovedFilter}, 17 | link::Link, 18 | local_search::LocalSearch, 19 | meta_details::MetaDetails, 20 | player::Player, 21 | streaming_server::StreamingServer, 22 | }, 23 | runtime::Effects, 24 | types::{ 25 | addon::DescriptorPreview, api::LinkAuthKey, events::DismissedEventsBucket, 26 | library::LibraryBucket, notifications::NotificationsBucket, profile::Profile, 27 | resource::MetaItemPreview, search_history::SearchHistoryBucket, streams::StreamsBucket, 28 | }, 29 | Model, 30 | }; 31 | 32 | use crate::{ 33 | env::WebEnv, 34 | model::{ 35 | serialize_catalogs_with_extra, serialize_continue_watching_preview, serialize_ctx, 36 | serialize_data_export, serialize_discover, serialize_installed_addons, serialize_library, 37 | serialize_local_search, serialize_meta_details, serialize_player, serialize_remote_addons, 38 | serialize_streaming_server, 39 | }, 40 | }; 41 | 42 | #[derive(Model, Clone)] 43 | #[cfg_attr(debug_assertions, derive(Serialize))] 44 | #[model(WebEnv)] 45 | pub struct WebModel { 46 | pub ctx: Ctx, 47 | pub auth_link: Link, 48 | pub data_export: DataExport, 49 | pub continue_watching_preview: ContinueWatchingPreview, 50 | pub board: CatalogsWithExtra, 51 | pub discover: CatalogWithFilters, 52 | pub library: LibraryWithFilters, 53 | pub continue_watching: LibraryWithFilters, 54 | pub search: CatalogsWithExtra, 55 | /// Pre-loaded results for local search 56 | pub local_search: LocalSearch, 57 | pub meta_details: MetaDetails, 58 | pub remote_addons: CatalogWithFilters, 59 | pub installed_addons: InstalledAddonsWithFilters, 60 | pub addon_details: AddonDetails, 61 | pub streaming_server: StreamingServer, 62 | pub player: Player, 63 | } 64 | 65 | impl WebModel { 66 | pub fn new( 67 | profile: Profile, 68 | library: LibraryBucket, 69 | streams: StreamsBucket, 70 | notifications: NotificationsBucket, 71 | search_history: SearchHistoryBucket, 72 | dismissed_events: DismissedEventsBucket, 73 | ) -> (WebModel, Effects) { 74 | let (continue_watching_preview, continue_watching_preview_effects) = 75 | ContinueWatchingPreview::new(&library, ¬ifications); 76 | let (discover, discover_effects) = CatalogWithFilters::::new(&profile); 77 | let (library_, library_effects) = 78 | LibraryWithFilters::::new(&library, ¬ifications); 79 | let (continue_watching, continue_watching_effects) = 80 | LibraryWithFilters::::new(&library, ¬ifications); 81 | let (remote_addons, remote_addons_effects) = 82 | CatalogWithFilters::::new(&profile); 83 | let (installed_addons, installed_addons_effects) = 84 | InstalledAddonsWithFilters::new(&profile); 85 | let (streaming_server, streaming_server_effects) = StreamingServer::new::(&profile); 86 | let (local_search, local_search_effects) = LocalSearch::new::(); 87 | let model = WebModel { 88 | ctx: Ctx::new( 89 | profile, 90 | library, 91 | streams, 92 | notifications, 93 | search_history, 94 | dismissed_events, 95 | ), 96 | auth_link: Default::default(), 97 | data_export: Default::default(), 98 | local_search, 99 | continue_watching_preview, 100 | board: Default::default(), 101 | discover, 102 | library: library_, 103 | continue_watching, 104 | search: Default::default(), 105 | meta_details: Default::default(), 106 | remote_addons, 107 | installed_addons, 108 | addon_details: Default::default(), 109 | streaming_server, 110 | player: Default::default(), 111 | }; 112 | ( 113 | model, 114 | continue_watching_preview_effects 115 | .join(discover_effects) 116 | .join(library_effects) 117 | .join(continue_watching_effects) 118 | .join(remote_addons_effects) 119 | .join(installed_addons_effects) 120 | .join(streaming_server_effects) 121 | .join(local_search_effects), 122 | ) 123 | } 124 | pub fn get_state(&self, field: &WebModelField) -> JsValue { 125 | match field { 126 | WebModelField::Ctx => serialize_ctx(&self.ctx), 127 | WebModelField::AuthLink => ::from_serde(&self.auth_link) 128 | .expect("JsValue from AuthLink"), 129 | WebModelField::DataExport => serialize_data_export(&self.data_export), 130 | WebModelField::ContinueWatchingPreview => serialize_continue_watching_preview( 131 | &self.continue_watching_preview, 132 | &self.ctx.streams, 133 | self.streaming_server.base_url.as_ref(), 134 | &self.ctx.profile.settings, 135 | ), 136 | WebModelField::Board => serialize_catalogs_with_extra(&self.board, &self.ctx), 137 | WebModelField::Discover => { 138 | serialize_discover(&self.discover, &self.ctx, &self.streaming_server) 139 | } 140 | WebModelField::Library => serialize_library( 141 | &self.library, 142 | &self.ctx, 143 | self.streaming_server.base_url.as_ref(), 144 | "library".to_owned(), 145 | ), 146 | WebModelField::ContinueWatching => serialize_library( 147 | &self.continue_watching, 148 | &self.ctx, 149 | self.streaming_server.base_url.as_ref(), 150 | "continuewatching".to_owned(), 151 | ), 152 | WebModelField::Search => serialize_catalogs_with_extra(&self.search, &self.ctx), 153 | WebModelField::LocalSearch => serialize_local_search(&self.local_search), 154 | WebModelField::MetaDetails => { 155 | serialize_meta_details(&self.meta_details, &self.ctx, &self.streaming_server) 156 | } 157 | WebModelField::RemoteAddons => serialize_remote_addons(&self.remote_addons, &self.ctx), 158 | WebModelField::InstalledAddons => serialize_installed_addons(&self.installed_addons), 159 | WebModelField::AddonDetails => { 160 | ::from_serde(&self.addon_details) 161 | .expect("JsValue from AddonDetails") 162 | } 163 | WebModelField::StreamingServer => serialize_streaming_server(&self.streaming_server), 164 | WebModelField::Player => { 165 | serialize_player(&self.player, &self.ctx, &self.streaming_server) 166 | } 167 | } 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /src/model/serialize_catalogs_with_extra.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use itertools::Itertools; 4 | use serde::Serialize; 5 | use stremio_core::deep_links::{DiscoverDeepLinks, MetaItemDeepLinks}; 6 | use stremio_core::models::catalogs_with_extra::{CatalogsWithExtra, Selected}; 7 | use stremio_core::models::common::Loadable; 8 | use stremio_core::models::ctx::Ctx; 9 | use stremio_core::types::resource::PosterShape; 10 | use wasm_bindgen::JsValue; 11 | 12 | mod model { 13 | 14 | use super::*; 15 | #[derive(Serialize)] 16 | #[serde(rename_all = "camelCase")] 17 | pub struct ManifestPreview<'a> { 18 | pub id: &'a str, 19 | pub name: &'a str, 20 | } 21 | #[derive(Serialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct DescriptorPreview<'a> { 24 | pub manifest: ManifestPreview<'a>, 25 | } 26 | #[derive(Serialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct MetaItemPreview<'a> { 29 | #[serde(flatten)] 30 | pub meta_item: &'a stremio_core::types::resource::MetaItemPreview, 31 | pub poster_shape: &'a PosterShape, 32 | pub watched: bool, 33 | pub deep_links: MetaItemDeepLinks, 34 | } 35 | #[derive(Serialize)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct ResourceLoadable<'a> { 38 | pub id: String, 39 | pub name: String, 40 | pub r#type: String, 41 | pub addon: DescriptorPreview<'a>, 42 | pub content: Option>, String>>, 43 | pub deep_links: DiscoverDeepLinks, 44 | } 45 | #[derive(Serialize)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct CatalogsWithExtra<'a> { 48 | pub selected: &'a Option, 49 | pub catalogs: Vec>, 50 | } 51 | } 52 | 53 | pub fn serialize_catalogs_with_extra( 54 | catalogs_with_extra: &CatalogsWithExtra, 55 | ctx: &Ctx, 56 | ) -> JsValue { 57 | ::from_serde(&model::CatalogsWithExtra { 58 | selected: &catalogs_with_extra.selected, 59 | catalogs: catalogs_with_extra 60 | .catalogs 61 | .iter() 62 | .filter_map(|catalog| catalog.first()) 63 | .filter_map(|catalog| { 64 | ctx.profile 65 | .addons 66 | .iter() 67 | .find(|addon| addon.transport_url == catalog.request.base) 68 | .and_then(|addon| { 69 | addon 70 | .manifest 71 | .catalogs 72 | .iter() 73 | .find(|manifest_catalog| { 74 | manifest_catalog.id == catalog.request.path.id 75 | && manifest_catalog.r#type == catalog.request.path.r#type 76 | }) 77 | .map(|manifest_catalog| (addon, manifest_catalog, catalog)) 78 | }) 79 | }) 80 | .map( 81 | |(addon, manifest_catalog, catalog)| model::ResourceLoadable { 82 | id: manifest_catalog.id.to_string(), 83 | name: manifest_catalog 84 | .name 85 | .as_ref() 86 | .unwrap_or(&addon.manifest.name) 87 | .to_string(), 88 | r#type: manifest_catalog.r#type.to_string(), 89 | addon: model::DescriptorPreview { 90 | manifest: model::ManifestPreview { 91 | id: &addon.manifest.id, 92 | name: &addon.manifest.name, 93 | }, 94 | }, 95 | content: match &catalog.content { 96 | Some(Loadable::Ready(meta_items)) => { 97 | let poster_shape = 98 | meta_items.first().map(|meta_item| &meta_item.poster_shape); 99 | Some(Loadable::Ready( 100 | meta_items 101 | .iter() 102 | .unique_by(|meta_item| &meta_item.id) 103 | .take(10) 104 | .map(|meta_item| model::MetaItemPreview { 105 | meta_item, 106 | poster_shape: poster_shape 107 | .unwrap_or(&meta_item.poster_shape), 108 | watched: ctx 109 | .library 110 | .items 111 | .get(&meta_item.id) 112 | .map(|library_item| library_item.watched()) 113 | .unwrap_or_default(), 114 | deep_links: MetaItemDeepLinks::from(( 115 | meta_item, 116 | &catalog.request, 117 | )) 118 | .into_web_deep_links(), 119 | }) 120 | .collect::>(), 121 | )) 122 | } 123 | Some(Loadable::Loading) => Some(Loadable::Loading), 124 | Some(Loadable::Err(error)) => Some(Loadable::Err(error.to_string())), 125 | None => None, 126 | }, 127 | deep_links: DiscoverDeepLinks::from(&catalog.request).into_web_deep_links(), 128 | }, 129 | ) 130 | .collect::>(), 131 | }) 132 | .expect("JsValue from model::CatalogsWithExtra") 133 | } 134 | -------------------------------------------------------------------------------- /src/model/serialize_continue_watching_preview.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::format::JsValueSerdeExt; 2 | use url::Url; 3 | use wasm_bindgen::JsValue; 4 | 5 | use stremio_core::{ 6 | models::continue_watching_preview::ContinueWatchingPreview, 7 | types::{profile::Settings, streams::StreamsBucket}, 8 | }; 9 | 10 | pub fn serialize_continue_watching_preview( 11 | continue_watching_preview: &ContinueWatchingPreview, 12 | streams_bucket: &StreamsBucket, 13 | streaming_server_url: Option<&Url>, 14 | settings: &Settings, 15 | ) -> JsValue { 16 | ::from_serde(&model::ContinueWatchingPreview::from(( 17 | continue_watching_preview, 18 | streams_bucket, 19 | streaming_server_url, 20 | settings, 21 | ))) 22 | .expect("JsValue from model::ContinueWatchingPreview") 23 | } 24 | 25 | mod model { 26 | use serde::Serialize; 27 | use url::Url; 28 | 29 | use stremio_core::{ 30 | deep_links::{LibraryDeepLinks, LibraryItemDeepLinks}, 31 | types::{ 32 | profile::Settings, 33 | resource::PosterShape, 34 | streams::{StreamsBucket, StreamsItem, StreamsItemKey}, 35 | }, 36 | }; 37 | 38 | use crate::model::deep_links_ext::DeepLinksExt; 39 | 40 | #[derive(Serialize)] 41 | #[serde(rename_all = "camelCase")] 42 | pub struct ContinueWatchingPreview<'a> { 43 | pub items: Vec>, 44 | pub deep_links: LibraryDeepLinks, 45 | } 46 | 47 | impl<'a> 48 | From<( 49 | &'a stremio_core::models::continue_watching_preview::ContinueWatchingPreview, 50 | &StreamsBucket, 51 | Option<&Url>, 52 | &Settings, 53 | )> for ContinueWatchingPreview<'a> 54 | { 55 | fn from( 56 | (continue_watching_preview, streams_bucket, streaming_server_url, settings): ( 57 | &'a stremio_core::models::continue_watching_preview::ContinueWatchingPreview, 58 | &StreamsBucket, 59 | Option<&Url>, 60 | &Settings, 61 | ), 62 | ) -> Self { 63 | Self { 64 | items: continue_watching_preview 65 | .items 66 | .iter() 67 | .map(|core_cw_item| { 68 | let library_item_stream = core_cw_item 69 | .library_item 70 | .state 71 | .video_id 72 | .clone() 73 | .and_then(|video_id| { 74 | streams_bucket.items.get(&StreamsItemKey { 75 | meta_id: core_cw_item.library_item.id.clone(), 76 | video_id, 77 | }) 78 | }); 79 | 80 | Item::from(( 81 | core_cw_item, 82 | library_item_stream, 83 | streaming_server_url, 84 | settings, 85 | )) 86 | }) 87 | .collect::>(), 88 | deep_links: LibraryDeepLinks::from(&"continuewatching".to_owned()) 89 | .into_web_deep_links(), 90 | } 91 | } 92 | } 93 | 94 | #[derive(Serialize)] 95 | #[serde(rename_all = "camelCase")] 96 | pub struct Item<'a> { 97 | #[serde(flatten)] 98 | library_item: LibraryItem<'a>, 99 | /// a count of the total notifications we have for this item 100 | notifications: usize, 101 | } 102 | 103 | impl<'a> 104 | From<( 105 | &'a stremio_core::models::continue_watching_preview::Item, 106 | Option<&StreamsItem>, 107 | Option<&Url>, 108 | &Settings, 109 | )> for Item<'a> 110 | { 111 | fn from( 112 | (item, stream_item, streaming_server_url, settings): ( 113 | &'a stremio_core::models::continue_watching_preview::Item, 114 | Option<&StreamsItem>, 115 | Option<&Url>, 116 | &Settings, 117 | ), 118 | ) -> Self { 119 | Self { 120 | library_item: LibraryItem::from(( 121 | &item.library_item, 122 | stream_item, 123 | streaming_server_url, 124 | settings, 125 | )), 126 | notifications: item.notifications, 127 | } 128 | } 129 | } 130 | 131 | #[derive(Serialize)] 132 | #[serde(rename_all = "camelCase")] 133 | pub struct LibraryItem<'a> { 134 | #[serde(rename = "_id")] 135 | pub id: &'a String, 136 | pub name: &'a String, 137 | pub r#type: &'a String, 138 | pub poster: &'a Option, 139 | pub poster_shape: &'a PosterShape, 140 | pub progress: f64, 141 | pub deep_links: LibraryItemDeepLinks, 142 | pub state: LibraryItemState<'a>, 143 | } 144 | 145 | impl<'a> 146 | From<( 147 | &'a stremio_core::types::library::LibraryItem, 148 | Option<&StreamsItem>, 149 | Option<&Url>, 150 | &Settings, 151 | )> for LibraryItem<'a> 152 | { 153 | fn from( 154 | (library_item, streams_item, streaming_server_url, settings): ( 155 | &'a stremio_core::types::library::LibraryItem, 156 | Option<&StreamsItem>, 157 | Option<&Url>, 158 | &Settings, 159 | ), 160 | ) -> Self { 161 | LibraryItem { 162 | id: &library_item.id, 163 | name: &library_item.name, 164 | r#type: &library_item.r#type, 165 | poster: &library_item.poster, 166 | poster_shape: match library_item.poster_shape { 167 | // override poster shape if it's Landscape to over be a Square. 168 | PosterShape::Landscape => &PosterShape::Square, 169 | // else use the provided shape 170 | _ => &library_item.poster_shape, 171 | }, 172 | progress: library_item.progress(), 173 | deep_links: LibraryItemDeepLinks::from(( 174 | library_item, 175 | streams_item, 176 | streaming_server_url, 177 | settings, 178 | )) 179 | .into_web_deep_links(), 180 | state: LibraryItemState::from(&library_item.state), 181 | } 182 | } 183 | } 184 | 185 | #[derive(Serialize)] 186 | #[serde(rename_all = "camelCase")] 187 | pub struct LibraryItemState<'a> { 188 | pub video_id: Option<&'a String>, 189 | } 190 | 191 | impl<'a> From<&'a stremio_core::types::library::LibraryItemState> for LibraryItemState<'a> { 192 | fn from(state: &'a stremio_core::types::library::LibraryItemState) -> Self { 193 | Self { 194 | video_id: state.video_id.as_ref(), 195 | } 196 | } 197 | } 198 | } 199 | -------------------------------------------------------------------------------- /src/model/serialize_ctx.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::format::JsValueSerdeExt; 2 | use wasm_bindgen::JsValue; 3 | 4 | use stremio_core::models::ctx::Ctx; 5 | 6 | pub fn serialize_ctx(ctx: &Ctx) -> JsValue { 7 | ::from_serde(&model::Ctx::from(ctx)).expect("JsValue from Ctx") 8 | } 9 | 10 | mod model { 11 | use std::collections::HashMap; 12 | 13 | use chrono::{DateTime, Utc}; 14 | use itertools::Itertools; 15 | use serde::Serialize; 16 | 17 | use stremio_core::deep_links::SearchHistoryItemDeepLinks; 18 | use stremio_core::types::{ 19 | events::Events, notifications::NotificationItem, profile::Profile, resource::MetaItemId, 20 | }; 21 | 22 | use crate::model::deep_links_ext::DeepLinksExt; 23 | 24 | #[derive(Serialize)] 25 | #[serde(rename_all = "camelCase")] 26 | pub struct Ctx<'a> { 27 | /// keep the original Profile model inside. 28 | pub profile: &'a Profile, 29 | pub notifications: Notifications<'a>, 30 | pub search_history: Vec>, 31 | pub events: &'a Events, 32 | } 33 | 34 | #[derive(Serialize)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct Notifications<'a> { 37 | /// Override the notifications to simplify the mapping 38 | pub items: HashMap>, 39 | pub last_updated: Option>, 40 | pub created: DateTime, 41 | } 42 | 43 | #[derive(Serialize)] 44 | #[serde(rename_all = "camelCase")] 45 | pub struct SearchHistoryItem<'a> { 46 | pub query: &'a String, 47 | pub deep_links: SearchHistoryItemDeepLinks, 48 | } 49 | 50 | impl<'a> From<&'a stremio_core::models::ctx::Ctx> for Ctx<'a> { 51 | fn from(ctx: &'a stremio_core::models::ctx::Ctx) -> Self { 52 | Self { 53 | profile: &ctx.profile, 54 | notifications: Notifications { 55 | items: ctx 56 | .notifications 57 | .items 58 | .iter() 59 | .map(|(meta_id, notifications)| { 60 | (meta_id.to_owned(), notifications.values().collect()) 61 | }) 62 | .collect(), 63 | last_updated: ctx.notifications.last_updated, 64 | created: ctx.notifications.created, 65 | }, 66 | search_history: ctx 67 | .search_history 68 | .items 69 | .iter() 70 | .sorted_by(|(_, a_date), (_, b_date)| Ord::cmp(b_date, a_date)) 71 | .map(|(query, ..)| SearchHistoryItem { 72 | query, 73 | deep_links: SearchHistoryItemDeepLinks::from(query).into_web_deep_links(), 74 | }) 75 | .collect(), 76 | events: &ctx.events, 77 | } 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/model/serialize_data_export.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::format::JsValueSerdeExt; 2 | use serde::Serialize; 3 | use stremio_core::models::common::Loadable; 4 | use stremio_core::models::ctx::CtxError; 5 | use stremio_core::models::data_export::DataExport; 6 | use url::Url; 7 | use wasm_bindgen::JsValue; 8 | 9 | mod model { 10 | use super::*; 11 | #[derive(Serialize)] 12 | #[serde(rename_all = "camelCase")] 13 | pub struct DataExport<'a> { 14 | pub export_url: Option<&'a Loadable>, 15 | } 16 | } 17 | 18 | pub fn serialize_data_export(data_export: &DataExport) -> JsValue { 19 | ::from_serde(&model::DataExport { 20 | export_url: data_export 21 | .export_url 22 | .as_ref() 23 | .map(|(_auth_key, loadable)| loadable), 24 | }) 25 | .expect("JsValue from model::DataExport") 26 | } 27 | -------------------------------------------------------------------------------- /src/model/serialize_discover.rs: -------------------------------------------------------------------------------- 1 | use boolinator::Boolinator; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use itertools::Itertools; 4 | 5 | use serde::Serialize; 6 | use wasm_bindgen::JsValue; 7 | 8 | use stremio_core::deep_links::{DiscoverDeepLinks, MetaItemDeepLinks, StreamDeepLinks}; 9 | use stremio_core::models::catalog_with_filters::{ 10 | CatalogWithFilters, Selected as CatalogWithFiltersSelected, 11 | }; 12 | use stremio_core::models::common::Loadable; 13 | use stremio_core::models::ctx::Ctx; 14 | use stremio_core::models::streaming_server::StreamingServer; 15 | use stremio_core::types::resource::MetaItemPreview; 16 | 17 | use crate::model::deep_links_ext::DeepLinksExt; 18 | 19 | mod model { 20 | use super::*; 21 | #[derive(Serialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct ManifestPreview<'a> { 24 | pub id: &'a str, 25 | pub name: &'a String, 26 | } 27 | #[derive(Serialize)] 28 | #[serde(rename_all = "camelCase")] 29 | pub struct DescriptorPreview<'a> { 30 | pub manifest: ManifestPreview<'a>, 31 | } 32 | #[derive(Serialize)] 33 | #[serde(rename_all = "camelCase")] 34 | pub struct SelectableExtraOption<'a> { 35 | pub value: &'a Option, 36 | pub selected: &'a bool, 37 | pub deep_links: DiscoverDeepLinks, 38 | } 39 | #[derive(Serialize)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct SelectableExtra<'a> { 42 | pub name: &'a String, 43 | pub is_required: &'a bool, 44 | pub options: Vec>, 45 | } 46 | #[derive(Serialize)] 47 | #[serde(rename_all = "camelCase")] 48 | pub struct SelectableCatalog<'a> { 49 | pub id: &'a String, 50 | pub name: &'a String, 51 | pub r#type: &'a str, 52 | pub addon: DescriptorPreview<'a>, 53 | pub selected: &'a bool, 54 | pub deep_links: DiscoverDeepLinks, 55 | } 56 | #[derive(Serialize)] 57 | #[serde(rename_all = "camelCase")] 58 | pub struct SelectableType<'a> { 59 | pub r#type: &'a String, 60 | pub selected: &'a bool, 61 | pub deep_links: DiscoverDeepLinks, 62 | } 63 | #[derive(Serialize)] 64 | #[serde(rename_all = "camelCase")] 65 | pub struct Selectable<'a> { 66 | pub types: Vec>, 67 | pub catalogs: Vec>, 68 | pub extra: Vec>, 69 | pub next_page: bool, 70 | } 71 | #[derive(Serialize)] 72 | #[serde(rename_all = "camelCase")] 73 | pub struct Stream<'a> { 74 | #[serde(flatten)] 75 | pub stream: &'a stremio_core::types::resource::Stream, 76 | pub deep_links: StreamDeepLinks, 77 | } 78 | #[derive(Serialize)] 79 | #[serde(rename_all = "camelCase")] 80 | pub struct MetaItemPreview<'a> { 81 | #[serde(flatten)] 82 | pub meta_item: &'a stremio_core::types::resource::MetaItemPreview, 83 | pub trailer_streams: Vec>, 84 | pub watched: bool, 85 | pub in_library: bool, 86 | pub deep_links: MetaItemDeepLinks, 87 | } 88 | #[derive(Serialize)] 89 | #[serde(rename_all = "camelCase")] 90 | pub struct ResourceLoadable<'a> { 91 | pub content: Loadable>, String>, 92 | pub installed: bool, 93 | } 94 | #[derive(Serialize)] 95 | #[serde(rename_all = "camelCase")] 96 | pub struct CatalogWithFilters<'a> { 97 | pub selected: &'a Option, 98 | pub selectable: Selectable<'a>, 99 | pub catalog: Option>, 100 | } 101 | } 102 | 103 | pub fn serialize_discover( 104 | discover: &CatalogWithFilters, 105 | ctx: &Ctx, 106 | streaming_server: &StreamingServer, 107 | ) -> JsValue { 108 | ::from_serde(&model::CatalogWithFilters { 109 | selected: &discover.selected, 110 | selectable: model::Selectable { 111 | types: discover 112 | .selectable 113 | .types 114 | .iter() 115 | .map(|selectable_type| model::SelectableType { 116 | r#type: &selectable_type.r#type, 117 | selected: &selectable_type.selected, 118 | deep_links: DiscoverDeepLinks::from(&selectable_type.request) 119 | .into_web_deep_links(), 120 | }) 121 | .collect(), 122 | catalogs: discover 123 | .selectable 124 | .catalogs 125 | .iter() 126 | .filter_map(|selectable_catalog| { 127 | ctx.profile 128 | .addons 129 | .iter() 130 | .find(|addon| addon.transport_url == selectable_catalog.request.base) 131 | .map(|addon| (addon, selectable_catalog)) 132 | }) 133 | .map(|(addon, selectable_catalog)| model::SelectableCatalog { 134 | id: &selectable_catalog.request.path.id, 135 | name: &selectable_catalog.catalog, 136 | r#type: &selectable_catalog.request.path.r#type, 137 | addon: model::DescriptorPreview { 138 | manifest: model::ManifestPreview { 139 | id: &addon.manifest.id, 140 | name: &addon.manifest.name, 141 | }, 142 | }, 143 | selected: &selectable_catalog.selected, 144 | deep_links: DiscoverDeepLinks::from(&selectable_catalog.request) 145 | .into_web_deep_links(), 146 | }) 147 | .collect(), 148 | extra: discover 149 | .selectable 150 | .extra 151 | .iter() 152 | .map(|selectable_extra| model::SelectableExtra { 153 | name: &selectable_extra.name, 154 | is_required: &selectable_extra.is_required, 155 | options: selectable_extra 156 | .options 157 | .iter() 158 | .map(|option| model::SelectableExtraOption { 159 | value: &option.value, 160 | selected: &option.selected, 161 | deep_links: DiscoverDeepLinks::from(&option.request) 162 | .into_web_deep_links(), 163 | }) 164 | .collect(), 165 | }) 166 | .collect(), 167 | next_page: discover.selectable.next_page.is_some(), 168 | }, 169 | catalog: (!discover.catalog.is_empty()).as_option().map(|_| { 170 | let first_page = discover 171 | .catalog 172 | .first() 173 | .expect("discover catalog first page"); 174 | model::ResourceLoadable { 175 | content: match &first_page.content { 176 | Some(Loadable::Ready(_)) => Loadable::Ready( 177 | discover 178 | .catalog 179 | .iter() 180 | .filter_map(|page| page.content.as_ref()) 181 | .filter_map(|page_content| page_content.ready()) 182 | .flat_map(|meta_items| { 183 | meta_items.iter().map(|meta_item| model::MetaItemPreview { 184 | meta_item, 185 | trailer_streams: meta_item 186 | .trailer_streams 187 | .iter() 188 | .take(1) 189 | .map(|stream| model::Stream { 190 | stream, 191 | deep_links: StreamDeepLinks::from(( 192 | stream, 193 | &streaming_server.base_url, 194 | &ctx.profile.settings, 195 | )) 196 | .into_web_deep_links(), 197 | }) 198 | .collect::>(), 199 | watched: ctx 200 | .library 201 | .items 202 | .get(&meta_item.id) 203 | .map(|library_item| library_item.watched()) 204 | .unwrap_or_default(), 205 | in_library: ctx 206 | .library 207 | .items 208 | .get(&meta_item.id) 209 | .map(|library_item| !library_item.removed) 210 | .unwrap_or_default(), 211 | deep_links: MetaItemDeepLinks::from(( 212 | meta_item, 213 | &first_page.request, 214 | )) 215 | .into_web_deep_links(), 216 | }) 217 | }) 218 | // it is possible that they are duplicates returned in 2 different pages 219 | // so we deduplicate all the results at once 220 | .unique_by(|meta| &meta.meta_item.id) 221 | .collect::>(), 222 | ), 223 | Some(Loadable::Loading) | None => Loadable::Loading, 224 | Some(Loadable::Err(error)) => Loadable::Err(error.to_string()), 225 | }, 226 | installed: ctx 227 | .profile 228 | .addons 229 | .iter() 230 | .any(|addon| addon.transport_url == first_page.request.base), 231 | } 232 | }), 233 | }) 234 | .expect("JsValue from Discover model::CatalogWithFilters") 235 | } 236 | -------------------------------------------------------------------------------- /src/model/serialize_installed_addons.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use serde::Serialize; 4 | use stremio_core::deep_links::AddonsDeepLinks; 5 | use stremio_core::models::installed_addons_with_filters::{ 6 | InstalledAddonsRequest, InstalledAddonsWithFilters, Selected, 7 | }; 8 | use wasm_bindgen::JsValue; 9 | 10 | mod model { 11 | use super::*; 12 | #[derive(Serialize)] 13 | #[serde(rename_all = "camelCase")] 14 | pub struct DescriptorPreview<'a> { 15 | #[serde(flatten)] 16 | pub addon: &'a stremio_core::types::addon::DescriptorPreview, 17 | pub installed: bool, 18 | } 19 | #[derive(Serialize)] 20 | #[serde(rename_all = "camelCase")] 21 | pub struct SelectableType<'a> { 22 | pub r#type: &'a Option, 23 | pub selected: &'a bool, 24 | pub deep_links: AddonsDeepLinks, 25 | } 26 | #[derive(Serialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct SelectableCatalog { 29 | pub name: String, 30 | pub selected: bool, 31 | pub deep_links: AddonsDeepLinks, 32 | } 33 | #[derive(Serialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct Selectable<'a> { 36 | pub types: Vec>, 37 | pub catalogs: Vec, 38 | } 39 | #[derive(Serialize)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct InstalledAddonsWithFilters<'a> { 42 | pub selected: &'a Option, 43 | pub selectable: Selectable<'a>, 44 | pub catalog: Vec>, 45 | } 46 | } 47 | 48 | pub fn serialize_installed_addons(installed_addons: &InstalledAddonsWithFilters) -> JsValue { 49 | ::from_serde(&model::InstalledAddonsWithFilters { 50 | selected: &installed_addons.selected, 51 | selectable: model::Selectable { 52 | types: installed_addons 53 | .selectable 54 | .types 55 | .iter() 56 | .map(|selectable_type| model::SelectableType { 57 | r#type: &selectable_type.r#type, 58 | selected: &selectable_type.selected, 59 | deep_links: AddonsDeepLinks::from(&selectable_type.request) 60 | .into_web_deep_links(), 61 | }) 62 | .collect(), 63 | catalogs: vec![model::SelectableCatalog { 64 | name: "Installed".to_owned(), 65 | selected: installed_addons.selected.is_some(), 66 | deep_links: AddonsDeepLinks::from(&InstalledAddonsRequest { r#type: None }) 67 | .into_web_deep_links(), 68 | }], 69 | }, 70 | catalog: installed_addons 71 | .catalog 72 | .iter() 73 | .map(|addon| model::DescriptorPreview { 74 | addon, 75 | installed: true, 76 | }) 77 | .collect(), 78 | }) 79 | .expect("JsValue from model::InstalledAddonsWithFilters") 80 | } 81 | -------------------------------------------------------------------------------- /src/model/serialize_library.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use serde::Serialize; 4 | use stremio_core::deep_links::{LibraryDeepLinks, LibraryItemDeepLinks}; 5 | use stremio_core::models::ctx::Ctx; 6 | use stremio_core::models::library_with_filters::{LibraryWithFilters, Selected, Sort}; 7 | use stremio_core::types::resource::PosterShape; 8 | use stremio_core::types::streams::StreamsItemKey; 9 | use url::Url; 10 | use wasm_bindgen::JsValue; 11 | 12 | mod model { 13 | use super::*; 14 | #[derive(Serialize)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct LibraryItem<'a> { 17 | #[serde(rename = "_id")] 18 | pub id: &'a String, 19 | pub name: &'a String, 20 | pub r#type: &'a String, 21 | pub poster: &'a Option, 22 | pub poster_shape: &'a PosterShape, 23 | pub notifications: usize, 24 | pub progress: f64, 25 | pub watched: bool, 26 | pub deep_links: LibraryItemDeepLinks, 27 | } 28 | #[derive(Serialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct SelectableType<'a> { 31 | pub r#type: &'a Option, 32 | pub selected: &'a bool, 33 | pub deep_links: LibraryDeepLinks, 34 | } 35 | #[derive(Serialize)] 36 | #[serde(rename_all = "camelCase")] 37 | pub struct SelectableSort<'a> { 38 | pub sort: &'a Sort, 39 | pub selected: &'a bool, 40 | pub deep_links: LibraryDeepLinks, 41 | } 42 | #[derive(Serialize)] 43 | #[serde(rename_all = "camelCase")] 44 | pub struct SelectablePage { 45 | pub deep_links: LibraryDeepLinks, 46 | } 47 | #[derive(Serialize)] 48 | #[serde(rename_all = "camelCase")] 49 | pub struct Selectable<'a> { 50 | pub types: Vec>, 51 | pub sorts: Vec>, 52 | pub next_page: bool, 53 | } 54 | #[derive(Serialize)] 55 | pub struct LibraryWithFilters<'a> { 56 | pub selected: &'a Option, 57 | pub selectable: Selectable<'a>, 58 | pub catalog: Vec>, 59 | } 60 | } 61 | 62 | pub fn serialize_library( 63 | library: &LibraryWithFilters, 64 | ctx: &Ctx, 65 | streaming_server_url: Option<&Url>, 66 | root: String, 67 | ) -> JsValue { 68 | ::from_serde(&model::LibraryWithFilters { 69 | selected: &library.selected, 70 | selectable: model::Selectable { 71 | types: library 72 | .selectable 73 | .types 74 | .iter() 75 | .map(|selectable_type| model::SelectableType { 76 | r#type: &selectable_type.r#type, 77 | selected: &selectable_type.selected, 78 | deep_links: LibraryDeepLinks::from((&root, &selectable_type.request)) 79 | .into_web_deep_links(), 80 | }) 81 | .collect(), 82 | sorts: library 83 | .selectable 84 | .sorts 85 | .iter() 86 | .map(|selectable_sort| model::SelectableSort { 87 | sort: &selectable_sort.sort, 88 | selected: &selectable_sort.selected, 89 | deep_links: LibraryDeepLinks::from((&root, &selectable_sort.request)) 90 | .into_web_deep_links(), 91 | }) 92 | .collect(), 93 | next_page: library.selectable.next_page.is_some(), 94 | }, 95 | catalog: library 96 | .catalog 97 | .iter() 98 | .map(|library_item| { 99 | // Try to get the stream from the StreamBucket 100 | // given that we have a video_id in the LibraryItemState! 101 | let streams_item = library_item.state.video_id.as_ref().and_then(|video_id| { 102 | ctx.streams.items.get(&StreamsItemKey { 103 | meta_id: library_item.id.to_owned(), 104 | video_id: video_id.to_owned(), 105 | }) 106 | }); 107 | 108 | model::LibraryItem { 109 | id: &library_item.id, 110 | name: &library_item.name, 111 | r#type: &library_item.r#type, 112 | poster: &library_item.poster, 113 | poster_shape: if library_item.poster_shape == PosterShape::Landscape { 114 | &PosterShape::Square 115 | } else { 116 | &library_item.poster_shape 117 | }, 118 | notifications: ctx 119 | .notifications 120 | .items 121 | .get(&library_item.id) 122 | .map_or(0, |item| item.len()), 123 | progress: library_item.progress(), 124 | watched: library_item.watched(), 125 | deep_links: LibraryItemDeepLinks::from(( 126 | library_item, 127 | streams_item, 128 | streaming_server_url, 129 | &ctx.profile.settings, 130 | )) 131 | .into_web_deep_links(), 132 | } 133 | }) 134 | .collect(), 135 | }) 136 | .expect("JsValue from model::LibraryWithFilters") 137 | } 138 | -------------------------------------------------------------------------------- /src/model/serialize_local_search.rs: -------------------------------------------------------------------------------- 1 | use gloo_utils::format::JsValueSerdeExt; 2 | use itertools::Itertools; 3 | use serde::Serialize; 4 | use wasm_bindgen::JsValue; 5 | 6 | use stremio_core::deep_links::LocalSearchItemDeepLinks; 7 | use stremio_core::models::local_search::{LocalSearch, Searchable}; 8 | 9 | use crate::model::deep_links_ext::DeepLinksExt; 10 | 11 | mod model { 12 | use super::*; 13 | 14 | #[derive(Serialize)] 15 | #[serde(rename_all = "camelCase")] 16 | pub struct LocalSearch<'a> { 17 | /// The results of the search autocompletion 18 | pub items: Vec>, 19 | } 20 | 21 | #[derive(Serialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct LocalSearchItem<'a> { 24 | pub query: &'a String, 25 | pub deep_links: LocalSearchItemDeepLinks, 26 | } 27 | } 28 | 29 | pub fn serialize_local_search(local_search: &LocalSearch) -> JsValue { 30 | ::from_serde(&model::LocalSearch { 31 | items: local_search 32 | .search_results 33 | .to_owned() 34 | .iter() 35 | .map(|Searchable { name, .. }| model::LocalSearchItem { 36 | query: name, 37 | deep_links: LocalSearchItemDeepLinks::from(name).into_web_deep_links(), 38 | }) 39 | .unique_by(|i| i.query) 40 | .collect(), 41 | }) 42 | .expect("JsValue from model::LocalSearch") 43 | } 44 | -------------------------------------------------------------------------------- /src/model/serialize_meta_details.rs: -------------------------------------------------------------------------------- 1 | use crate::{env::WebEnv, model::deep_links_ext::DeepLinksExt}; 2 | 3 | use either::Either; 4 | use gloo_utils::format::JsValueSerdeExt; 5 | use itertools::Itertools; 6 | use serde::Serialize; 7 | use std::iter; 8 | use url::Url; 9 | use wasm_bindgen::JsValue; 10 | 11 | use stremio_core::{ 12 | constants::META_RESOURCE_NAME, 13 | deep_links::{MetaItemDeepLinks, StreamDeepLinks, VideoDeepLinks}, 14 | models::{ 15 | common::{Loadable, ResourceError, ResourceLoadable}, 16 | ctx::Ctx, 17 | meta_details::{MetaDetails, Selected as MetaDetailsSelected}, 18 | streaming_server::StreamingServer, 19 | }, 20 | runtime::Env, 21 | types::library::LibraryItem, 22 | }; 23 | 24 | mod model { 25 | use super::*; 26 | #[derive(Serialize)] 27 | #[serde(rename_all = "camelCase")] 28 | pub struct ManifestPreview<'a> { 29 | pub id: &'a String, 30 | pub name: &'a String, 31 | pub logo: &'a Option, 32 | } 33 | #[derive(Serialize)] 34 | #[serde(rename_all = "camelCase")] 35 | pub struct DescriptorPreview<'a> { 36 | pub manifest: ManifestPreview<'a>, 37 | pub transport_url: &'a Url, 38 | } 39 | #[derive(Serialize)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct Stream<'a> { 42 | #[serde(flatten)] 43 | pub stream: &'a stremio_core::types::resource::Stream, 44 | // Watch progress percentage 45 | pub progress: Option, 46 | pub deep_links: StreamDeepLinks, 47 | } 48 | #[derive(Serialize)] 49 | #[serde(rename_all = "camelCase")] 50 | pub struct Video<'a> { 51 | #[serde(flatten)] 52 | pub video: &'a stremio_core::types::resource::Video, 53 | pub upcoming: bool, 54 | pub watched: bool, 55 | // Watch progress percentage 56 | pub progress: Option, 57 | pub scheduled: bool, 58 | pub deep_links: VideoDeepLinks, 59 | } 60 | #[derive(Serialize)] 61 | #[serde(rename_all = "camelCase")] 62 | pub struct MetaItem<'a> { 63 | #[serde(flatten)] 64 | pub meta_item: &'a stremio_core::types::resource::MetaItem, 65 | pub videos: Vec>, 66 | pub trailer_streams: Vec>, 67 | pub in_library: bool, 68 | pub watched: bool, 69 | pub deep_links: MetaItemDeepLinks, 70 | } 71 | #[derive(Serialize)] 72 | #[serde(rename_all = "camelCase")] 73 | pub struct ResourceLoadable<'a, T> { 74 | pub content: Loadable, 75 | pub addon: DescriptorPreview<'a>, 76 | } 77 | #[derive(Serialize)] 78 | #[serde(rename_all = "camelCase")] 79 | pub struct MetaExtension<'a> { 80 | pub url: &'a Url, 81 | pub name: &'a String, 82 | pub addon: DescriptorPreview<'a>, 83 | } 84 | #[derive(Serialize)] 85 | #[serde(rename_all = "camelCase")] 86 | pub struct MetaDetails<'a> { 87 | pub selected: &'a Option, 88 | pub meta_item: Option>>, 89 | pub library_item: &'a Option, 90 | pub streams: Vec>>>, 91 | pub meta_extensions: Vec>, 92 | pub title: Option, 93 | } 94 | } 95 | 96 | /// For MetaDetails: 97 | /// 98 | /// 1. If at least 1 item is ready we show the first ready item's data 99 | /// 2. If all loaded resources have returned an error we show the first item's error 100 | /// 3. We show a loading state 101 | pub fn serialize_meta_details( 102 | meta_details: &MetaDetails, 103 | ctx: &Ctx, 104 | streaming_server: &StreamingServer, 105 | ) -> JsValue { 106 | let meta_item = meta_details 107 | .meta_items 108 | .iter() 109 | .find(|meta_item| matches!(&meta_item.content, Some(Loadable::Ready(_)))) 110 | .or_else(|| { 111 | if meta_details 112 | .meta_items 113 | .iter() 114 | .all(|meta_item| matches!(&meta_item.content, Some(Loadable::Err(_)))) 115 | { 116 | meta_details.meta_items.first() 117 | } else { 118 | meta_details 119 | .meta_items 120 | .iter() 121 | .find(|meta_item| matches!(&meta_item.content, Some(Loadable::Loading))) 122 | } 123 | }); 124 | 125 | let streams = if meta_details.meta_streams.is_empty() { 126 | meta_details.streams.iter() 127 | } else { 128 | meta_details.meta_streams.iter() 129 | }; 130 | ::from_serde(&model::MetaDetails { 131 | selected: &meta_details.selected, 132 | meta_item: meta_item 133 | .and_then(|meta_item| { 134 | ctx.profile 135 | .addons 136 | .iter() 137 | .find(|addon| addon.transport_url == meta_item.request.base) 138 | .map(|addon| (meta_item, addon)) 139 | }) 140 | .map(|(meta_item, addon)| model::ResourceLoadable { 141 | content: match &meta_item { 142 | ResourceLoadable { 143 | request, 144 | content: Some(Loadable::Ready(meta_item)), 145 | } => Loadable::Ready(model::MetaItem { 146 | meta_item, 147 | videos: meta_item 148 | .videos 149 | .iter() 150 | .map(|video| model::Video { 151 | video, 152 | upcoming: meta_item.preview.behavior_hints.has_scheduled_videos 153 | && video.released > Some(WebEnv::now()), 154 | watched: meta_details 155 | .watched 156 | .as_ref() 157 | .map(|watched| watched.get_video(&video.id)) 158 | .unwrap_or_default(), 159 | progress: ctx 160 | .library 161 | .items 162 | .get(&meta_item.preview.id) 163 | .filter(|library_item| { 164 | Some(video.id.to_owned()) == library_item.state.video_id 165 | }) 166 | .map(|library_item| library_item.progress()), 167 | scheduled: meta_item.preview.behavior_hints.has_scheduled_videos, 168 | deep_links: VideoDeepLinks::from(( 169 | video, 170 | request, 171 | &streaming_server.base_url, 172 | &ctx.profile.settings, 173 | )) 174 | .into_web_deep_links(), 175 | }) 176 | .collect::>(), 177 | trailer_streams: meta_item 178 | .preview 179 | .trailer_streams 180 | .iter() 181 | .map(|stream| model::Stream { 182 | stream, 183 | progress: None, 184 | deep_links: StreamDeepLinks::from(( 185 | stream, 186 | &streaming_server.base_url, 187 | &ctx.profile.settings, 188 | )) 189 | .into_web_deep_links(), 190 | }) 191 | .collect::>(), 192 | in_library: ctx 193 | .library 194 | .items 195 | .get(&meta_item.preview.id) 196 | .map(|library_item| !library_item.removed) 197 | .unwrap_or_default(), 198 | watched: ctx 199 | .library 200 | .items 201 | .get(&meta_item.preview.id) 202 | .map(|library_item| library_item.watched()) 203 | .unwrap_or_default(), 204 | deep_links: MetaItemDeepLinks::from((meta_item, request)) 205 | .into_web_deep_links(), 206 | }), 207 | ResourceLoadable { 208 | content: Some(Loadable::Loading), 209 | .. 210 | } 211 | | ResourceLoadable { content: None, .. } => Loadable::Loading, 212 | ResourceLoadable { 213 | content: Some(Loadable::Err(error)), 214 | .. 215 | } => Loadable::Err(error), 216 | }, 217 | addon: model::DescriptorPreview { 218 | transport_url: &addon.transport_url, 219 | manifest: model::ManifestPreview { 220 | id: &addon.manifest.id, 221 | name: &addon.manifest.name, 222 | logo: &addon.manifest.logo, 223 | }, 224 | }, 225 | }), 226 | library_item: &meta_details.library_item, 227 | streams: streams 228 | .filter_map(|streams| { 229 | ctx.profile 230 | .addons 231 | .iter() 232 | .find(|addon| addon.transport_url == streams.request.base) 233 | .map(|addon| (streams, addon)) 234 | }) 235 | .map(|(streams, addon)| model::ResourceLoadable { 236 | content: match streams { 237 | ResourceLoadable { 238 | request, 239 | content: Some(Loadable::Ready(streams)), 240 | } => Loadable::Ready( 241 | streams 242 | .iter() 243 | .map(|stream| model::Stream { 244 | stream, 245 | progress: meta_details.library_item.as_ref().and_then( 246 | |library_item| { 247 | ctx.streams 248 | .items 249 | .values() 250 | .find(|item| item.stream == *stream) 251 | .map(|_| library_item.progress()) 252 | }, 253 | ), 254 | deep_links: meta_item 255 | .map_or_else( 256 | || { 257 | StreamDeepLinks::from(( 258 | stream, 259 | &streaming_server.base_url, 260 | &ctx.profile.settings, 261 | )) 262 | }, 263 | |meta_item| { 264 | StreamDeepLinks::from(( 265 | stream, 266 | request, 267 | &meta_item.request, 268 | &streaming_server.base_url, 269 | &ctx.profile.settings, 270 | )) 271 | }, 272 | ) 273 | .into_web_deep_links(), 274 | }) 275 | .collect::>(), 276 | ), 277 | ResourceLoadable { 278 | content: Some(Loadable::Loading), 279 | .. 280 | } 281 | | ResourceLoadable { content: None, .. } => Loadable::Loading, 282 | ResourceLoadable { 283 | content: Some(Loadable::Err(error)), 284 | .. 285 | } => Loadable::Err(error), 286 | }, 287 | addon: model::DescriptorPreview { 288 | transport_url: &addon.transport_url, 289 | manifest: model::ManifestPreview { 290 | id: &addon.manifest.id, 291 | name: &addon.manifest.name, 292 | logo: &addon.manifest.logo, 293 | }, 294 | }, 295 | }) 296 | .collect::>(), 297 | meta_extensions: meta_details 298 | .meta_items 299 | .iter() 300 | .filter_map(|meta_item| { 301 | ctx.profile 302 | .addons 303 | .iter() 304 | .find(|addon| addon.transport_url == meta_item.request.base) 305 | .map(|addon| (meta_item, addon)) 306 | }) 307 | .flat_map(|(meta_item, addon)| match meta_item { 308 | ResourceLoadable { 309 | content: Some(Loadable::Ready(meta_item)), 310 | .. 311 | } => Either::Left( 312 | meta_item 313 | .preview 314 | .links 315 | .iter() 316 | .filter(|link| link.category == META_RESOURCE_NAME) 317 | .map(move |link| (link, addon)), 318 | ), 319 | _ => Either::Right(iter::empty()), 320 | }) 321 | .unique_by(|(link, _)| &link.url) 322 | .map(|(link, addon)| model::MetaExtension { 323 | url: &link.url, 324 | name: &link.name, 325 | addon: model::DescriptorPreview { 326 | transport_url: &addon.transport_url, 327 | manifest: model::ManifestPreview { 328 | id: &addon.manifest.id, 329 | name: &addon.manifest.name, 330 | logo: &addon.manifest.logo, 331 | }, 332 | }, 333 | }) 334 | .collect::>(), 335 | title: meta_item 336 | .as_ref() 337 | .and_then(|meta_item| match meta_item { 338 | ResourceLoadable { 339 | content: Some(Loadable::Ready(meta_item)), 340 | .. 341 | } => Some(meta_item), 342 | _ => None, 343 | }) 344 | .map(|meta_item| { 345 | meta_details 346 | .selected 347 | .as_ref() 348 | .and_then(|selected| selected.stream_path.as_ref()) 349 | .and_then(|stream_path| { 350 | match meta_item 351 | .videos 352 | .iter() 353 | .find(|video| video.id == stream_path.id) 354 | { 355 | Some(video) 356 | if meta_item.preview.behavior_hints.default_video_id.is_none() => 357 | { 358 | match &video.series_info { 359 | Some(series_info) => Some(format!( 360 | "{} - {} ({}x{})", 361 | &meta_item.preview.name, 362 | &video.title, 363 | &series_info.season, 364 | &series_info.episode 365 | )), 366 | _ => Some(format!( 367 | "{} - {}", 368 | &meta_item.preview.name, &video.title 369 | )), 370 | } 371 | } 372 | _ => None, 373 | } 374 | }) 375 | .unwrap_or_else(|| meta_item.preview.name.to_owned()) 376 | }), 377 | }) 378 | .expect("JsValue from model::MetaDetails") 379 | } 380 | -------------------------------------------------------------------------------- /src/model/serialize_player.rs: -------------------------------------------------------------------------------- 1 | use crate::env::WebEnv; 2 | use crate::model::deep_links_ext::DeepLinksExt; 3 | use gloo_utils::format::JsValueSerdeExt; 4 | use semver::Version; 5 | use serde::Serialize; 6 | use stremio_core::deep_links::{StreamDeepLinks, VideoDeepLinks}; 7 | use stremio_core::models::common::{Loadable, ResourceError, ResourceLoadable}; 8 | use stremio_core::models::ctx::Ctx; 9 | use stremio_core::models::player::Player; 10 | use stremio_core::models::streaming_server::StreamingServer; 11 | use stremio_core::runtime::Env; 12 | use stremio_core::types::{ 13 | addon::{ResourcePath, ResourceRequest}, 14 | streams::StreamItemState, 15 | }; 16 | use url::Url; 17 | use wasm_bindgen::JsValue; 18 | 19 | mod model { 20 | use super::*; 21 | #[derive(Serialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct Stream<'a> { 24 | #[serde(flatten)] 25 | pub stream: &'a stremio_core::types::resource::Stream, 26 | pub deep_links: StreamDeepLinks, 27 | } 28 | #[derive(Serialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct ManifestPreview<'a> { 31 | pub id: &'a String, 32 | pub name: &'a String, 33 | pub version: &'a Version, 34 | pub description: &'a Option, 35 | pub logo: &'a Option, 36 | pub background: &'a Option, 37 | pub types: &'a Vec, 38 | } 39 | #[derive(Serialize)] 40 | #[serde(rename_all = "camelCase")] 41 | pub struct DescriptorPreview<'a> { 42 | pub manifest: ManifestPreview<'a>, 43 | pub transport_url: &'a Url, 44 | } 45 | #[derive(Serialize)] 46 | #[serde(rename_all = "camelCase")] 47 | pub struct Video<'a> { 48 | #[serde(flatten)] 49 | pub video: &'a stremio_core::types::resource::Video, 50 | pub upcoming: bool, 51 | pub watched: bool, 52 | pub progress: Option, 53 | pub scheduled: bool, 54 | pub deep_links: VideoDeepLinks, 55 | } 56 | #[derive(Serialize)] 57 | #[serde(rename_all = "camelCase")] 58 | pub struct MetaItem<'a> { 59 | #[serde(flatten)] 60 | pub meta_item: &'a stremio_core::types::resource::MetaItem, 61 | pub videos: Vec>, 62 | } 63 | #[derive(Serialize)] 64 | #[serde(rename_all = "camelCase")] 65 | pub struct Subtitles<'a> { 66 | #[serde(flatten)] 67 | pub subtitles: &'a stremio_core::types::resource::Subtitles, 68 | pub id: String, 69 | pub origin: &'a String, 70 | } 71 | #[derive(Serialize)] 72 | #[serde(rename_all = "camelCase")] 73 | pub struct LibraryItemState<'a> { 74 | pub time_offset: &'a u64, 75 | #[serde(rename = "video_id")] 76 | pub video_id: &'a Option, 77 | } 78 | #[derive(Serialize)] 79 | #[serde(rename_all = "camelCase")] 80 | pub struct LibraryItem<'a> { 81 | #[serde(rename = "_id")] 82 | pub id: &'a String, 83 | pub state: LibraryItemState<'a>, 84 | } 85 | #[derive(Serialize)] 86 | #[serde(rename_all = "camelCase")] 87 | pub struct Selected<'a> { 88 | pub stream: Stream<'a>, 89 | pub stream_request: &'a Option, 90 | pub meta_request: &'a Option, 91 | pub subtitles_path: &'a Option, 92 | } 93 | #[derive(Serialize)] 94 | #[serde(rename_all = "camelCase")] 95 | pub struct Player<'a> { 96 | pub selected: Option>, 97 | pub meta_item: Option, &'a ResourceError>>, 98 | pub subtitles: Vec>, 99 | pub next_video: Option>, 100 | pub series_info: Option<&'a stremio_core::types::resource::SeriesInfo>, 101 | pub library_item: Option>, 102 | pub stream_state: Option<&'a StreamItemState>, 103 | #[serde(skip_serializing_if = "Option::is_none")] 104 | pub intro_outro: Option<&'a stremio_core::types::player::IntroOutro>, 105 | pub title: Option, 106 | pub addon: Option>, 107 | } 108 | } 109 | 110 | pub fn serialize_player(player: &Player, ctx: &Ctx, streaming_server: &StreamingServer) -> JsValue { 111 | ::from_serde(&model::Player { 112 | selected: player.selected.as_ref().map(|selected| model::Selected { 113 | stream: model::Stream { 114 | stream: &selected.stream, 115 | deep_links: StreamDeepLinks::from(( 116 | &selected.stream, 117 | &streaming_server.base_url, 118 | &ctx.profile.settings, 119 | )) 120 | .into_web_deep_links(), 121 | }, 122 | stream_request: &selected.stream_request, 123 | meta_request: &selected.meta_request, 124 | subtitles_path: &selected.subtitles_path, 125 | }), 126 | meta_item: player 127 | .meta_item 128 | .as_ref() 129 | .map(|ResourceLoadable { request, content }| match &content { 130 | Some(Loadable::Loading) | None => Loadable::Loading, 131 | Some(Loadable::Err(error)) => Loadable::Err(error), 132 | Some(Loadable::Ready(meta_item)) => { 133 | Loadable::Ready(model::MetaItem { 134 | meta_item, 135 | videos: meta_item 136 | .videos 137 | .iter() 138 | .map(|video| model::Video { 139 | video, 140 | upcoming: meta_item.preview.behavior_hints.has_scheduled_videos 141 | && video.released > Some(WebEnv::now()), 142 | watched: false, // TODO use library 143 | progress: None, // TODO use library, 144 | scheduled: meta_item.preview.behavior_hints.has_scheduled_videos, 145 | deep_links: VideoDeepLinks::from(( 146 | video, 147 | request, 148 | &streaming_server.base_url, 149 | &ctx.profile.settings, 150 | )) 151 | .into_web_deep_links(), 152 | }) 153 | .collect(), 154 | }) 155 | } 156 | }), 157 | subtitles: player 158 | .subtitles 159 | .iter() 160 | .filter_map(|subtitles| { 161 | ctx.profile 162 | .addons 163 | .iter() 164 | .find(|addon| addon.transport_url == subtitles.request.base) 165 | .map(|addon| (addon, subtitles)) 166 | }) 167 | .filter_map(|(addon, subtitles)| match subtitles { 168 | ResourceLoadable { 169 | content: Some(Loadable::Ready(subtitles)), 170 | .. 171 | } => Some((addon, subtitles)), 172 | _ => None, 173 | }) 174 | .flat_map(|(addon, subtitles)| { 175 | subtitles 176 | .iter() 177 | .enumerate() 178 | .map(move |(position, subtitles)| model::Subtitles { 179 | subtitles, 180 | id: format!("{}_{}", addon.transport_url, position), 181 | origin: &addon.manifest.name, 182 | }) 183 | }) 184 | .collect(), 185 | next_video: player 186 | .selected 187 | .as_ref() 188 | .and_then(|selected| { 189 | selected 190 | .meta_request 191 | .as_ref() 192 | .zip(selected.stream_request.as_ref()) 193 | }) 194 | .zip(player.next_video.as_ref()) 195 | .map(|((meta_request, stream_request), video)| model::Video { 196 | video, 197 | upcoming: player 198 | .meta_item 199 | .as_ref() 200 | .and_then(|meta_item| match meta_item { 201 | ResourceLoadable { 202 | content: Some(Loadable::Ready(meta_item)), 203 | .. 204 | } => Some(meta_item), 205 | _ => None, 206 | }) 207 | .map(|meta_item| { 208 | meta_item.preview.behavior_hints.has_scheduled_videos 209 | && video.released > Some(WebEnv::now()) 210 | }) 211 | .unwrap_or_default(), 212 | watched: false, // TODO use library 213 | progress: None, // TODO use library, 214 | scheduled: player 215 | .meta_item 216 | .as_ref() 217 | .and_then(|meta_item| match meta_item { 218 | ResourceLoadable { 219 | content: Some(Loadable::Ready(meta_item)), 220 | .. 221 | } => Some(meta_item.preview.behavior_hints.has_scheduled_videos), 222 | _ => None, 223 | }) 224 | .unwrap_or_default(), 225 | deep_links: VideoDeepLinks::from(( 226 | video, 227 | stream_request, 228 | meta_request, 229 | &streaming_server.base_url, 230 | &ctx.profile.settings, 231 | )) 232 | .into_web_deep_links(), 233 | }), 234 | series_info: player.series_info.as_ref(), 235 | library_item: player 236 | .library_item 237 | .as_ref() 238 | .map(|library_item| model::LibraryItem { 239 | id: &library_item.id, 240 | state: model::LibraryItemState { 241 | time_offset: &library_item.state.time_offset, 242 | video_id: &library_item.state.video_id, 243 | }, 244 | }), 245 | stream_state: player.stream_state.as_ref(), 246 | intro_outro: player.intro_outro.as_ref(), 247 | title: player.selected.as_ref().and_then(|selected| { 248 | player 249 | .meta_item 250 | .as_ref() 251 | .and_then(|meta_item| match meta_item { 252 | ResourceLoadable { 253 | content: Some(Loadable::Ready(meta_item)), 254 | .. 255 | } => Some(meta_item), 256 | _ => None, 257 | }) 258 | .zip(selected.stream_request.as_ref()) 259 | .map(|(meta_item, stream_request)| { 260 | match meta_item 261 | .videos 262 | .iter() 263 | .find(|video| video.id == stream_request.path.id) 264 | { 265 | Some(video) 266 | if meta_item.preview.behavior_hints.default_video_id.is_none() => 267 | { 268 | match &video.series_info { 269 | Some(series_info) => format!( 270 | "{} - {} ({}x{})", 271 | &meta_item.preview.name, 272 | &video.title, 273 | &series_info.season, 274 | &series_info.episode 275 | ), 276 | _ => format!("{} - {}", &meta_item.preview.name, &video.title), 277 | } 278 | } 279 | _ => meta_item.preview.name.to_owned(), 280 | } 281 | }) 282 | .or_else(|| selected.stream.name.to_owned()) 283 | }), 284 | addon: player 285 | .selected 286 | .as_ref() 287 | .and_then(|selected| selected.stream_request.as_ref()) 288 | .and_then(|stream_request| { 289 | ctx.profile 290 | .addons 291 | .iter() 292 | .find(|addon| addon.transport_url == stream_request.base) 293 | }) 294 | .map(|addon| model::DescriptorPreview { 295 | transport_url: &addon.transport_url, 296 | manifest: model::ManifestPreview { 297 | id: &addon.manifest.id, 298 | name: &addon.manifest.name, 299 | version: &addon.manifest.version, 300 | description: &addon.manifest.description, 301 | logo: &addon.manifest.logo, 302 | background: &addon.manifest.background, 303 | types: &addon.manifest.types, 304 | }, 305 | }), 306 | }) 307 | .expect("JsValue from model::Player") 308 | } 309 | -------------------------------------------------------------------------------- /src/model/serialize_remote_addons.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use serde::Serialize; 4 | use stremio_core::deep_links::AddonsDeepLinks; 5 | use stremio_core::models::catalog_with_filters::{CatalogWithFilters, Selected}; 6 | use stremio_core::models::common::Loadable; 7 | use stremio_core::models::ctx::Ctx; 8 | use stremio_core::types::addon::DescriptorPreview; 9 | use wasm_bindgen::JsValue; 10 | 11 | mod model { 12 | use super::*; 13 | #[derive(Serialize)] 14 | #[serde(rename_all = "camelCase")] 15 | pub struct SelectableCatalog<'a> { 16 | pub id: &'a String, 17 | pub name: &'a String, 18 | pub selected: &'a bool, 19 | pub deep_links: AddonsDeepLinks, 20 | } 21 | #[derive(Serialize)] 22 | #[serde(rename_all = "camelCase")] 23 | pub struct SelectableType<'a> { 24 | pub r#type: &'a String, 25 | pub selected: &'a bool, 26 | pub deep_links: AddonsDeepLinks, 27 | } 28 | #[derive(Serialize)] 29 | #[serde(rename_all = "camelCase")] 30 | pub struct Selectable<'a> { 31 | pub catalogs: Vec>, 32 | pub types: Vec>, 33 | } 34 | #[derive(Serialize)] 35 | #[serde(rename_all = "camelCase")] 36 | pub struct DescriptorPreview<'a> { 37 | #[serde(flatten)] 38 | pub addon: &'a stremio_core::types::addon::DescriptorPreview, 39 | pub installed: bool, 40 | } 41 | #[derive(Serialize)] 42 | #[serde(rename_all = "camelCase")] 43 | pub struct ResourceLoadable<'a> { 44 | pub content: Loadable>, String>, 45 | } 46 | #[derive(Serialize)] 47 | #[serde(rename_all = "camelCase")] 48 | pub struct CatalogWithFilters<'a> { 49 | pub selected: &'a Option, 50 | pub selectable: Selectable<'a>, 51 | pub catalog: Option>, 52 | } 53 | } 54 | 55 | pub fn serialize_remote_addons( 56 | remote_addons: &CatalogWithFilters, 57 | ctx: &Ctx, 58 | ) -> JsValue { 59 | ::from_serde(&model::CatalogWithFilters { 60 | selected: &remote_addons.selected, 61 | selectable: model::Selectable { 62 | catalogs: remote_addons 63 | .selectable 64 | .catalogs 65 | .iter() 66 | .map(|selectable_catalog| model::SelectableCatalog { 67 | id: &selectable_catalog.request.path.id, 68 | name: &selectable_catalog.catalog, 69 | selected: &selectable_catalog.selected, 70 | deep_links: AddonsDeepLinks::from(&selectable_catalog.request) 71 | .into_web_deep_links(), 72 | }) 73 | .collect(), 74 | types: remote_addons 75 | .selectable 76 | .types 77 | .iter() 78 | .map(|selectable_type| model::SelectableType { 79 | r#type: &selectable_type.r#type, 80 | selected: &selectable_type.selected, 81 | deep_links: AddonsDeepLinks::from(&selectable_type.request) 82 | .into_web_deep_links(), 83 | }) 84 | .collect(), 85 | }, 86 | catalog: remote_addons 87 | .catalog 88 | .first() 89 | .map(|catalog| model::ResourceLoadable { 90 | content: match &catalog.content { 91 | Some(Loadable::Ready(addons)) => Loadable::Ready( 92 | addons 93 | .iter() 94 | .map(|addon| model::DescriptorPreview { 95 | addon, 96 | installed: ctx 97 | .profile 98 | .addons 99 | .iter() 100 | .map(|addon| &addon.transport_url) 101 | .any(|transport_url| *transport_url == addon.transport_url), 102 | }) 103 | .collect::>(), 104 | ), 105 | Some(Loadable::Loading) | None => Loadable::Loading, 106 | Some(Loadable::Err(error)) => Loadable::Err(error.to_string()), 107 | }, 108 | }), 109 | }) 110 | .expect("JsValue from model::CatalogWithFilters") 111 | } 112 | -------------------------------------------------------------------------------- /src/model/serialize_streaming_server.rs: -------------------------------------------------------------------------------- 1 | use crate::model::deep_links_ext::DeepLinksExt; 2 | use gloo_utils::format::JsValueSerdeExt; 3 | use serde::Serialize; 4 | use stremio_core::deep_links::MetaItemDeepLinks; 5 | use stremio_core::models::common::Loadable; 6 | use stremio_core::models::streaming_server::{PlaybackDevice, Selected, StreamingServer}; 7 | use stremio_core::runtime::EnvError; 8 | use stremio_core::types::addon::ResourcePath; 9 | use stremio_core::types::streaming_server::{DeviceInfo, NetworkInfo, Settings, Statistics}; 10 | use url::Url; 11 | use wasm_bindgen::JsValue; 12 | 13 | mod model { 14 | use super::*; 15 | type TorrentLoadable<'a> = Loadable<(&'a ResourcePath, MetaItemDeepLinks), &'a EnvError>; 16 | #[derive(Serialize)] 17 | #[serde(rename_all = "camelCase")] 18 | pub struct StreamingServer<'a> { 19 | pub selected: &'a Selected, 20 | pub settings: &'a Loadable, 21 | pub base_url: &'a Option, 22 | pub remote_url: &'a Option, 23 | pub playback_devices: &'a Loadable, EnvError>, 24 | pub network_info: &'a Loadable, 25 | pub device_info: &'a Loadable, 26 | pub torrent: Option<(&'a String, TorrentLoadable<'a>)>, 27 | pub statistics: Option<&'a Loadable>, 28 | } 29 | } 30 | 31 | pub fn serialize_streaming_server(streaming_server: &StreamingServer) -> JsValue { 32 | ::from_serde(&model::StreamingServer { 33 | selected: &streaming_server.selected, 34 | settings: &streaming_server.settings, 35 | base_url: &streaming_server.base_url, 36 | remote_url: &streaming_server.remote_url, 37 | playback_devices: &streaming_server.playback_devices, 38 | network_info: &streaming_server.network_info, 39 | device_info: &streaming_server.device_info, 40 | torrent: streaming_server 41 | .torrent 42 | .as_ref() 43 | .map(|(info_hash, loadable)| { 44 | let loadable = match loadable { 45 | Loadable::Ready(resource_path) => Loadable::Ready(( 46 | resource_path, 47 | MetaItemDeepLinks::from(resource_path).into_web_deep_links(), 48 | )), 49 | Loadable::Loading => Loadable::Loading, 50 | Loadable::Err(error) => Loadable::Err(error), 51 | }; 52 | (info_hash, loadable) 53 | }), 54 | statistics: streaming_server.statistics.as_ref(), 55 | }) 56 | .expect("JsValue from model::StreamingServer") 57 | } 58 | -------------------------------------------------------------------------------- /src/stremio_core_web.rs: -------------------------------------------------------------------------------- 1 | use std::sync::RwLock; 2 | 3 | use enclose::enclose; 4 | use futures::{future, try_join, FutureExt, StreamExt}; 5 | use gloo_utils::format::JsValueSerdeExt; 6 | use lazy_static::lazy_static; 7 | use tracing::{info, Level}; 8 | use tracing_wasm::WASMLayerConfigBuilder; 9 | use wasm_bindgen::{prelude::wasm_bindgen, JsValue}; 10 | 11 | use stremio_core::{ 12 | constants::{ 13 | DISMISSED_EVENTS_STORAGE_KEY, LIBRARY_RECENT_STORAGE_KEY, LIBRARY_STORAGE_KEY, 14 | NOTIFICATIONS_STORAGE_KEY, PROFILE_STORAGE_KEY, SEARCH_HISTORY_STORAGE_KEY, 15 | STREAMS_STORAGE_KEY, 16 | }, 17 | models::common::Loadable, 18 | runtime::{msg::Action, Env, EnvError, Runtime, RuntimeAction, RuntimeEvent}, 19 | types::{ 20 | events::DismissedEventsBucket, library::LibraryBucket, notifications::NotificationsBucket, 21 | profile::Profile, resource::Stream, search_history::SearchHistoryBucket, 22 | streams::StreamsBucket, 23 | }, 24 | }; 25 | 26 | use crate::{ 27 | env::WebEnv, 28 | event::WebEvent, 29 | model::{WebModel, WebModelField}, 30 | }; 31 | 32 | lazy_static! { 33 | static ref RUNTIME: RwLock, EnvError>>> = 34 | Default::default(); 35 | } 36 | 37 | #[wasm_bindgen(start)] 38 | pub fn start() { 39 | // print pretty errors in wasm https://github.com/rustwasm/console_error_panic_hook 40 | // This is not needed for tracing_wasm to work, but it is a common tool for getting proper error line numbers for panics. 41 | console_error_panic_hook::set_once(); 42 | 43 | #[cfg(any(debug_assertions, feature = "log-trace"))] 44 | let max_level = Level::TRACE; 45 | #[cfg(all(not(debug_assertions), not(feature = "log-trace")))] 46 | let max_level = Level::ERROR; 47 | 48 | let config = WASMLayerConfigBuilder::default() 49 | .set_max_level(max_level) 50 | .build(); 51 | // setup wasm tracing Subscriber on web console 52 | tracing_wasm::set_as_global_default_with_config(config); 53 | 54 | info!(?max_level, "Logging level"); 55 | } 56 | 57 | #[wasm_bindgen] 58 | pub async fn initialize_runtime(emit_to_ui: js_sys::Function) -> Result<(), JsValue> { 59 | if RUNTIME.read().expect("runtime read failed").is_some() { 60 | panic!("runtime initialization has already started"); 61 | }; 62 | 63 | *RUNTIME.write().expect("runtime write failed") = Some(Loadable::Loading); 64 | let env_init_result = WebEnv::init().await; 65 | match env_init_result { 66 | Ok(_) => { 67 | let storage_result = try_join!( 68 | WebEnv::get_storage::(PROFILE_STORAGE_KEY), 69 | WebEnv::get_storage::(LIBRARY_RECENT_STORAGE_KEY), 70 | WebEnv::get_storage::(LIBRARY_STORAGE_KEY), 71 | WebEnv::get_storage::(STREAMS_STORAGE_KEY), 72 | WebEnv::get_storage::(NOTIFICATIONS_STORAGE_KEY), 73 | WebEnv::get_storage::(SEARCH_HISTORY_STORAGE_KEY), 74 | WebEnv::get_storage::(DISMISSED_EVENTS_STORAGE_KEY), 75 | ); 76 | match storage_result { 77 | Ok(( 78 | profile, 79 | recent_bucket, 80 | other_bucket, 81 | streams_bucket, 82 | notifications_bucket, 83 | search_history_bucket, 84 | dismissed_events_bucket, 85 | )) => { 86 | let profile = profile.unwrap_or_default(); 87 | let mut library = LibraryBucket::new(profile.uid(), vec![]); 88 | if let Some(recent_bucket) = recent_bucket { 89 | library.merge_bucket(recent_bucket); 90 | }; 91 | if let Some(other_bucket) = other_bucket { 92 | library.merge_bucket(other_bucket); 93 | }; 94 | let streams_bucket = 95 | streams_bucket.unwrap_or_else(|| StreamsBucket::new(profile.uid())); 96 | let notifications_bucket = notifications_bucket 97 | .unwrap_or(NotificationsBucket::new::(profile.uid(), vec![])); 98 | let search_history_bucket = 99 | search_history_bucket.unwrap_or(SearchHistoryBucket::new(profile.uid())); 100 | let dismissed_events_bucket = dismissed_events_bucket 101 | .unwrap_or(DismissedEventsBucket::new(profile.uid())); 102 | let (model, effects) = WebModel::new( 103 | profile, 104 | library, 105 | streams_bucket, 106 | notifications_bucket, 107 | search_history_bucket, 108 | dismissed_events_bucket, 109 | ); 110 | let (runtime, rx) = Runtime::::new( 111 | model, 112 | effects.into_iter().collect::>(), 113 | 1000, 114 | ); 115 | WebEnv::exec_concurrent(rx.for_each(move |event| { 116 | if let RuntimeEvent::CoreEvent(event) = &event { 117 | WebEnv::exec_concurrent(WebEnv::get_location_hash().then( 118 | enclose!((event) move |location_hash| async move { 119 | let runtime = RUNTIME.read().expect("runtime read failed"); 120 | let runtime = runtime 121 | .as_ref() 122 | .expect("runtime is not ready") 123 | .as_ref() 124 | .expect("runtime is not ready"); 125 | let model = runtime.model().expect("model read failed"); 126 | let path = location_hash.split('#').last().map(|path| path.to_owned()).unwrap_or_default(); 127 | WebEnv::emit_to_analytics( 128 | &WebEvent::CoreEvent(Box::new(event.to_owned())), 129 | &model, 130 | &path 131 | ); 132 | }), 133 | )); 134 | }; 135 | emit_to_ui 136 | .call1(&JsValue::NULL, &::from_serde(&event).expect("Event handler: JsValue from Event")) 137 | .expect("emit event failed"); 138 | future::ready(()) 139 | })); 140 | *RUNTIME.write().expect("runtime write failed") = 141 | Some(Loadable::Ready(runtime)); 142 | Ok(()) 143 | } 144 | Err(error) => { 145 | *RUNTIME.write().expect("runtime write failed") = 146 | Some(Loadable::Err(error.to_owned())); 147 | Err(::from_serde(&error) 148 | .expect("Storage: JsValue from Event")) 149 | } 150 | } 151 | } 152 | Err(error) => { 153 | *RUNTIME.write().expect("runtime write failed") = Some(Loadable::Err(error.to_owned())); 154 | Err(::from_serde(&error).expect("JsValue from Event")) 155 | } 156 | } 157 | } 158 | 159 | #[wasm_bindgen] 160 | #[cfg(debug_assertions)] 161 | pub fn get_debug_state() -> JsValue { 162 | let runtime = RUNTIME.read().expect("runtime read failed"); 163 | let runtime = runtime 164 | .as_ref() 165 | .expect("runtime is not ready") 166 | .as_ref() 167 | .expect("runtime is not ready"); 168 | let model = runtime.model().expect("model read failed"); 169 | ::from_serde(&*model).expect("JsValue from WebModel") 170 | } 171 | 172 | #[wasm_bindgen] 173 | pub fn get_state(field: JsValue) -> JsValue { 174 | let field = JsValueSerdeExt::into_serde(&field).expect("get state failed"); 175 | let runtime = RUNTIME.read().expect("runtime read failed"); 176 | let runtime = runtime 177 | .as_ref() 178 | .expect("runtime is not ready") 179 | .as_ref() 180 | .expect("runtime is not ready"); 181 | let model = runtime.model().expect("model read failed"); 182 | model.get_state(&field) 183 | } 184 | 185 | #[wasm_bindgen] 186 | pub fn dispatch(action: JsValue, field: JsValue, location_hash: JsValue) { 187 | let action: Action = 188 | JsValueSerdeExt::into_serde(&action).expect("dispatch failed because of Action"); 189 | let field: Option = 190 | JsValueSerdeExt::into_serde(&field).expect("dispatch failed because of Field"); 191 | let runtime = RUNTIME.read().expect("runtime read failed"); 192 | let runtime = runtime 193 | .as_ref() 194 | .expect("runtime is not ready - None") 195 | .as_ref() 196 | .expect("runtime is not ready - Loading or Error"); 197 | { 198 | let model = runtime.model().expect("model read failed"); 199 | let path = location_hash 200 | .as_string() 201 | .and_then(|location_hash| location_hash.split('#').last().map(|path| path.to_owned())) 202 | .unwrap_or_default(); 203 | WebEnv::emit_to_analytics( 204 | &WebEvent::CoreAction(Box::new(action.to_owned())), 205 | &model, 206 | &path, 207 | ); 208 | } 209 | runtime.dispatch(RuntimeAction { action, field }); 210 | } 211 | 212 | #[wasm_bindgen] 213 | pub fn analytics(event: JsValue, location_hash: JsValue) { 214 | let event = 215 | JsValueSerdeExt::into_serde(&event).expect("UIEvent deserialization for analytics failed"); 216 | let runtime = RUNTIME.read().expect("runtime read failed"); 217 | let runtime = runtime 218 | .as_ref() 219 | .expect("runtime is not ready - None") 220 | .as_ref() 221 | .expect("runtime is not ready - Loading or Error"); 222 | let model = runtime.model().expect("model read failed"); 223 | let path = location_hash 224 | .as_string() 225 | .and_then(|location_hash| location_hash.split('#').last().map(|path| path.to_owned())) 226 | .unwrap_or_default(); 227 | WebEnv::emit_to_analytics(&WebEvent::UIEvent(event), &model, &path); 228 | } 229 | 230 | #[wasm_bindgen] 231 | pub fn decode_stream(stream: JsValue) -> JsValue { 232 | let stream = stream.as_string().map(Stream::decode); 233 | match stream { 234 | Some(Ok(stream)) => { 235 | ::from_serde(&stream).expect("JsValue from Stream") 236 | } 237 | _ => JsValue::NULL, 238 | } 239 | } 240 | -------------------------------------------------------------------------------- /src/worker.js: -------------------------------------------------------------------------------- 1 | const Bridge = require('./bridge'); 2 | 3 | const bridge = new Bridge(self, self); 4 | 5 | self.init = async ({ appVersion, shellVersion }) => { 6 | // TODO remove the document shim when this PR is merged 7 | // https://github.com/cfware/babel-plugin-bundled-import-meta/pull/26 8 | self.document = { 9 | baseURI: self.location.href 10 | }; 11 | self.app_version = appVersion; 12 | self.shell_version = shellVersion; 13 | self.get_location_hash = async () => bridge.call(['location', 'hash'], []); 14 | self.local_storage_get_item = async (key) => bridge.call(['localStorage', 'getItem'], [key]); 15 | self.local_storage_set_item = async (key, value) => bridge.call(['localStorage', 'setItem'], [key, value]); 16 | self.local_storage_remove_item = async (key) => bridge.call(['localStorage', 'removeItem'], [key]); 17 | const { default: initialize_api, initialize_runtime, get_state, get_debug_state, dispatch, analytics, decode_stream } = require('./stremio_core_web.js'); 18 | self.getState = get_state; 19 | self.getDebugState = get_debug_state; 20 | self.dispatch = dispatch; 21 | self.analytics = analytics; 22 | self.decodeStream = decode_stream; 23 | await initialize_api(require('./stremio_core_web_bg.wasm')); 24 | await initialize_runtime((event) => bridge.call(['onCoreEvent'], [event])); 25 | }; 26 | --------------------------------------------------------------------------------