From 4e1439f27e80aabcd4516aa6edc4b910f4eb4b75 Mon Sep 17 00:00:00 2001 From: Amaan Qureshi Date: Sun, 1 Mar 2026 02:43:36 -0500 Subject: [PATCH] all: init twt --- .editorconfig | 3 + .envrc | 1 + .gitignore | 35 + .rustfmt.toml | 26 + .taplo.toml | 13 + Cargo.lock | 1468 ++++++++++++++++++++++++++ Cargo.toml | 113 ++ README.md | 105 ++ config/teapot.example.toml | 56 + flake.lock | 66 ++ flake.nix | 373 +++++++ public/android-chrome-192x192.png | Bin 0 -> 2861 bytes public/android-chrome-384x384.png | Bin 0 -> 6294 bytes public/android-chrome-512x512.png | Bin 0 -> 2319 bytes public/apple-touch-icon.png | Bin 0 -> 1325 bytes public/browserconfig.xml | 8 + public/css/fontello.css | 138 +++ public/css/style.css | 1 + public/css/themes/auto.css | 2 + public/css/themes/auto_(twitter).css | 2 + public/css/themes/black.css | 37 + public/css/themes/dracula.css | 41 + public/css/themes/mastodon.css | 37 + public/css/themes/pleroma.css | 37 + public/css/themes/teapot.css | 3 + public/css/themes/twitter.css | 37 + public/css/themes/twitter_dark.css | 37 + public/favicon-16x16.png | Bin 0 -> 446 bytes public/favicon-32x32.png | Bin 0 -> 601 bytes public/favicon.ico | Bin 0 -> 15086 bytes public/fonts/LICENSE.txt | 48 + public/fonts/fontello.eot | Bin 0 -> 9544 bytes public/fonts/fontello.svg | 50 + public/fonts/fontello.ttf | Bin 0 -> 9376 bytes public/fonts/fontello.woff | Bin 0 -> 5924 bytes public/fonts/fontello.woff2 | Bin 0 -> 4896 bytes public/js/hls.min.js | 5 + public/js/hlsPlayback.js | 25 + public/js/infiniteScroll.js | 77 ++ public/logo.png | Bin 0 -> 1865 bytes public/lp.svg | 6 + public/opensearch.xml | 9 + public/robots.txt | 5 + public/safari-pinned-tab.svg | 1 + public/site.webmanifest | 24 + sass/general.scss | 40 + sass/include/_mixins.scss | 86 ++ sass/include/_variables.scss | 43 + sass/index.scss | 202 ++++ sass/inputs.scss | 203 ++++ sass/navbar.scss | 87 ++ sass/profile/_base.scss | 83 ++ sass/profile/card.scss | 130 +++ sass/profile/photo-rail.scss | 96 ++ sass/search.scss | 120 +++ sass/timeline.scss | 162 +++ sass/tweet/_base.scss | 244 +++++ sass/tweet/card.scss | 119 +++ sass/tweet/embed.scss | 17 + sass/tweet/media.scss | 119 +++ sass/tweet/poll.scss | 42 + sass/tweet/quote.scss | 95 ++ sass/tweet/thread.scss | 139 +++ sass/tweet/video.scss | 77 ++ sessions.example.jsonl | 1 + src/api/auth.rs | 343 ++++++ src/api/client.rs | 620 +++++++++++ src/api/endpoints.rs | 142 +++ src/api/http.rs | 178 ++++ src/api/mod.rs | 13 + src/api/parser/card.rs | 464 ++++++++ src/api/parser/conversation.rs | 144 +++ src/api/parser/edit_history.rs | 62 ++ src/api/parser/entity.rs | 64 ++ src/api/parser/media.rs | 197 ++++ src/api/parser/mod.rs | 23 + src/api/parser/search.rs | 124 +++ src/api/parser/timeline.rs | 184 ++++ src/api/parser/tweet.rs | 437 ++++++++ src/api/parser/user.rs | 224 ++++ src/api/schema.rs | 1190 +++++++++++++++++++++ src/api/tid.rs | 117 ++ src/cache/mod.rs | 152 +++ src/config.rs | 160 +++ src/error.rs | 149 +++ src/main.rs | 122 +++ src/routes/debug.rs | 30 + src/routes/embed.rs | 162 +++ src/routes/helpers.rs | 77 ++ src/routes/intent.rs | 104 ++ src/routes/list.rs | 231 ++++ src/routes/media.rs | 256 +++++ src/routes/mod.rs | 234 ++++ src/routes/preferences.rs | 368 +++++++ src/routes/redirect.rs | 170 +++ src/routes/rss.rs | 266 +++++ src/routes/search.rs | 395 +++++++ src/routes/status.rs | 399 +++++++ src/routes/timeline.rs | 568 ++++++++++ src/routes/unsupported.rs | 78 ++ src/types/mod.rs | 13 + src/types/prefs.rs | 120 +++ src/types/query.rs | 283 +++++ src/types/session.rs | 111 ++ src/types/timeline.rs | 81 ++ src/types/tweet.rs | 313 ++++++ src/types/user.rs | 48 + src/utils/entity_expander.rs | 291 +++++ src/utils/formatters.rs | 458 ++++++++ src/utils/hmac.rs | 48 + src/utils/mod.rs | 8 + src/views/embed.rs | 522 +++++++++ src/views/layout.rs | 292 +++++ src/views/mod.rs | 10 + src/views/preferences.rs | 111 ++ src/views/profile.rs | 317 ++++++ src/views/renderutils.rs | 272 +++++ src/views/rss.rs | 362 +++++++ src/views/search.rs | 294 ++++++ src/views/timeline.rs | 293 +++++ src/views/tweet.rs | 916 ++++++++++++++++ src/views/user_list.rs | 96 ++ 122 files changed, 18700 insertions(+) create mode 100644 .editorconfig create mode 100644 .envrc create mode 100644 .gitignore create mode 100644 .rustfmt.toml create mode 100644 .taplo.toml create mode 100644 Cargo.lock create mode 100644 Cargo.toml create mode 100644 README.md create mode 100644 config/teapot.example.toml create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 public/android-chrome-192x192.png create mode 100644 public/android-chrome-384x384.png create mode 100644 public/android-chrome-512x512.png create mode 100644 public/apple-touch-icon.png create mode 100644 public/browserconfig.xml create mode 100644 public/css/fontello.css create mode 100644 public/css/style.css create mode 100644 public/css/themes/auto.css create mode 100644 public/css/themes/auto_(twitter).css create mode 100644 public/css/themes/black.css create mode 100644 public/css/themes/dracula.css create mode 100644 public/css/themes/mastodon.css create mode 100644 public/css/themes/pleroma.css create mode 100644 public/css/themes/teapot.css create mode 100644 public/css/themes/twitter.css create mode 100644 public/css/themes/twitter_dark.css create mode 100644 public/favicon-16x16.png create mode 100644 public/favicon-32x32.png create mode 100644 public/favicon.ico create mode 100644 public/fonts/LICENSE.txt create mode 100644 public/fonts/fontello.eot create mode 100644 public/fonts/fontello.svg create mode 100644 public/fonts/fontello.ttf create mode 100644 public/fonts/fontello.woff create mode 100644 public/fonts/fontello.woff2 create mode 100644 public/js/hls.min.js create mode 100644 public/js/hlsPlayback.js create mode 100644 public/js/infiniteScroll.js create mode 100644 public/logo.png create mode 100644 public/lp.svg create mode 100644 public/opensearch.xml create mode 100644 public/robots.txt create mode 100644 public/safari-pinned-tab.svg create mode 100644 public/site.webmanifest create mode 100644 sass/general.scss create mode 100644 sass/include/_mixins.scss create mode 100644 sass/include/_variables.scss create mode 100644 sass/index.scss create mode 100644 sass/inputs.scss create mode 100644 sass/navbar.scss create mode 100644 sass/profile/_base.scss create mode 100644 sass/profile/card.scss create mode 100644 sass/profile/photo-rail.scss create mode 100644 sass/search.scss create mode 100644 sass/timeline.scss create mode 100644 sass/tweet/_base.scss create mode 100644 sass/tweet/card.scss create mode 100644 sass/tweet/embed.scss create mode 100644 sass/tweet/media.scss create mode 100644 sass/tweet/poll.scss create mode 100644 sass/tweet/quote.scss create mode 100644 sass/tweet/thread.scss create mode 100644 sass/tweet/video.scss create mode 100644 sessions.example.jsonl create mode 100644 src/api/auth.rs create mode 100644 src/api/client.rs create mode 100644 src/api/endpoints.rs create mode 100644 src/api/http.rs create mode 100644 src/api/mod.rs create mode 100644 src/api/parser/card.rs create mode 100644 src/api/parser/conversation.rs create mode 100644 src/api/parser/edit_history.rs create mode 100644 src/api/parser/entity.rs create mode 100644 src/api/parser/media.rs create mode 100644 src/api/parser/mod.rs create mode 100644 src/api/parser/search.rs create mode 100644 src/api/parser/timeline.rs create mode 100644 src/api/parser/tweet.rs create mode 100644 src/api/parser/user.rs create mode 100644 src/api/schema.rs create mode 100644 src/api/tid.rs create mode 100644 src/cache/mod.rs create mode 100644 src/config.rs create mode 100644 src/error.rs create mode 100644 src/main.rs create mode 100644 src/routes/debug.rs create mode 100644 src/routes/embed.rs create mode 100644 src/routes/helpers.rs create mode 100644 src/routes/intent.rs create mode 100644 src/routes/list.rs create mode 100644 src/routes/media.rs create mode 100644 src/routes/mod.rs create mode 100644 src/routes/preferences.rs create mode 100644 src/routes/redirect.rs create mode 100644 src/routes/rss.rs create mode 100644 src/routes/search.rs create mode 100644 src/routes/status.rs create mode 100644 src/routes/timeline.rs create mode 100644 src/routes/unsupported.rs create mode 100644 src/types/mod.rs create mode 100644 src/types/prefs.rs create mode 100644 src/types/query.rs create mode 100644 src/types/session.rs create mode 100644 src/types/timeline.rs create mode 100644 src/types/tweet.rs create mode 100644 src/types/user.rs create mode 100644 src/utils/entity_expander.rs create mode 100644 src/utils/formatters.rs create mode 100644 src/utils/hmac.rs create mode 100644 src/utils/mod.rs create mode 100644 src/views/embed.rs create mode 100644 src/views/layout.rs create mode 100644 src/views/mod.rs create mode 100644 src/views/preferences.rs create mode 100644 src/views/profile.rs create mode 100644 src/views/renderutils.rs create mode 100644 src/views/rss.rs create mode 100644 src/views/search.rs create mode 100644 src/views/timeline.rs create mode 100644 src/views/tweet.rs create mode 100644 src/views/user_list.rs diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..40a7516 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,3 @@ +[*.rs] +indent_style = space +indent_size = 3 diff --git a/.envrc b/.envrc new file mode 100644 index 0000000..8392d15 --- /dev/null +++ b/.envrc @@ -0,0 +1 @@ +use flake \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c78b717 --- /dev/null +++ b/.gitignore @@ -0,0 +1,35 @@ +# Ignore everything +* + +# Track project files +!.editorconfig +!.envrc +!.gitignore +!.rustfmt.toml +!.taplo.toml +!Cargo.lock +!Cargo.toml +!README.md + +# Config (example only, not real config) +!config/ +!config/teapot.example.toml + +# Session example (never real sessions) +!sessions.example.jsonl + +# Source +!src/ +!src/** + +# Static assets +!public/ +!public/** + +# Sass sources +!sass/ +!sass/** + +# Nix +!flake.lock +!flake.nix diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 0000000..ba48561 --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,26 @@ +condense_wildcard_suffixes = true +doc_comment_code_block_width = 100 +edition = "2024" # Keep in sync with Cargo.toml. +enum_discrim_align_threshold = 60 +float_literal_trailing_zero = "Always" +force_multiline_blocks = true +format_code_in_doc_comments = true +format_macro_matchers = true +format_strings = true +group_imports = "StdExternalCrate" +hex_literal_case = "Upper" +imports_granularity = "Crate" +imports_layout = "Vertical" +inline_attribute_width = 60 +match_block_trailing_comma = true +max_width = 100 +newline_style = "Unix" +normalize_comments = true +normalize_doc_attributes = true +overflow_delimited_expr = true +struct_field_align_threshold = 60 +tab_spaces = 3 +unstable_features = true +use_field_init_shorthand = true +use_try_shorthand = true +wrap_comments = true diff --git a/.taplo.toml b/.taplo.toml new file mode 100644 index 0000000..5a6456b --- /dev/null +++ b/.taplo.toml @@ -0,0 +1,13 @@ +[formatting] +align_entries = true +column_width = 100 +compact_arrays = false +reorder_inline_tables = true +reorder_keys = true + +[[rule]] +include = [ "**/Cargo.toml" ] +keys = [ "package" ] + +[rule.formatting] +reorder_keys = false diff --git a/Cargo.lock b/Cargo.lock new file mode 100644 index 0000000..f2aa93a --- /dev/null +++ b/Cargo.lock @@ -0,0 +1,1468 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" + +[[package]] +name = "aho-corasick" +version = "1.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301" +dependencies = [ + "memchr", +] + +[[package]] +name = "async-compression" +version = "0.4.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "98ec5f6c2f8bc326c994cb9e241cc257ddaba9afa8555a43cffbb5dd86efaa37" +dependencies = [ + "compression-codecs", + "compression-core", + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + +[[package]] +name = "axum" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b52af3cb4058c895d37317bb27508dccc8e5f2d39454016b297bf4a400597b8" +dependencies = [ + "axum-core", + "axum-macros", + "bytes", + "form_urlencoded", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "serde_core", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "sync_wrapper", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fef252edff26ddba56bbcdf2ee3307b8129acb86f5749b68990c168a6fcc9c76" +dependencies = [ + "axum", + "axum-core", + "bytes", + "cookie", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "bitflags" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" + +[[package]] +name = "bytes" +version = "1.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" + +[[package]] +name = "cc" +version = "1.2.51" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203" +dependencies = [ + "find-msvc-tools", + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801" + +[[package]] +name = "compression-codecs" +version = "0.4.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0f7ac3e5b97fdce45e8922fb05cae2c37f7bbd63d30dd94821dacfd8f3f2bf2" +dependencies = [ + "compression-core", + "flate2", + "memchr", +] + +[[package]] +name = "compression-core" +version = "0.4.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75984efb6ed102a0d42db99afb6c1948f0380d1d91808d5529916e6c08b49d8d" + +[[package]] +name = "cookie" +version = "0.18.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747" +dependencies = [ + "percent-encoding", + "time", + "version_check", +] + +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "core-foundation-sys" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" + +[[package]] +name = "crc32fast" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "data-encoding" +version = "2.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" + +[[package]] +name = "deranged" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" +dependencies = [ + "powerfmt", + "serde_core", +] + +[[package]] +name = "equivalent" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" + +[[package]] +name = "eyre" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cd915d99f24784cdc19fd37ef22b97e3ff0ae756c7e492e9fbfe897d61e2aec" +dependencies = [ + "indenter", + "once_cell", +] + +[[package]] +name = "find-msvc-tools" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff" + +[[package]] +name = "flate2" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "form_urlencoded" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "futures-channel" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d" +dependencies = [ + "futures-core", +] + +[[package]] +name = "futures-core" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" + +[[package]] +name = "futures-sink" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c39754e157331b013978ec91992bde1ac089843443c49cbc7f46150b0fad0893" + +[[package]] +name = "futures-task" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" + +[[package]] +name = "futures-util" +version = "0.3.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" +dependencies = [ + "futures-core", + "futures-task", + "pin-project-lite", + "slab", +] + +[[package]] +name = "getrandom" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "335ff9f135e4384c8150d6f27c6daed433577f86b4750418338c01a1a2528592" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" + +[[package]] +name = "hmac-sha256" +version = "1.1.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec9d92d097f4749b64e8cc33d924d9f40a2d4eb91402b458014b781f5733d60f" + +[[package]] +name = "http" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" +dependencies = [ + "bytes", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" +dependencies = [ + "bytes", + "futures-core", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "http-range-header" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9171a2ea8a68358193d15dd5d70c1c10a2afc3e7e4c5bc92bc9f025cebd7359c" + +[[package]] +name = "httparse" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" +dependencies = [ + "atomic-waker", + "bytes", + "futures-channel", + "futures-core", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "pin-utils", + "smallvec", + "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" +dependencies = [ + "http", + "hyper", + "hyper-util", + "log", + "rustls", + "rustls-native-certs", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-util" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "hyper", + "libc", + "pin-project-lite", + "socket2", + "tokio", + "tower-service", + "tracing", +] + +[[package]] +name = "indenter" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "964de6e86d545b246d84badc0fef527924ace5134f30641c203ef52ba83f58d5" + +[[package]] +name = "indexmap" +version = "2.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ad4bb2b565bca0645f4d68c5c9af97fba094e9791da685bf83cb5f3ce74acf2" +dependencies = [ + "equivalent", + "hashbrown", +] + +[[package]] +name = "itoa" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.178" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" + +[[package]] +name = "log" +version = "0.4.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" + +[[package]] +name = "matchers" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9" +dependencies = [ + "regex-automata", +] + +[[package]] +name = "matchit" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3" + +[[package]] +name = "maud" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8156733e27020ea5c684db5beac5d1d611e1272ab17901a49466294b84fc217e" +dependencies = [ + "axum-core", + "http", + "itoa", + "maud_macros", +] + +[[package]] +name = "maud_macros" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7261b00f3952f617899bc012e3dbd56e4f0110a038175929fa5d18e5a19913ca" +dependencies = [ + "proc-macro2", + "proc-macro2-diagnostics", + "quote", + "syn", +] + +[[package]] +name = "memchr" +version = "2.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" + +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + +[[package]] +name = "miniz_oxide" +version = "0.8.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316" +dependencies = [ + "adler2", + "simd-adler32", +] + +[[package]] +name = "mio" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc" +dependencies = [ + "libc", + "wasi", + "windows-sys 0.61.2", +] + +[[package]] +name = "nu-ansi-term" +version = "0.50.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "num-conv" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" + +[[package]] +name = "once_cell" +version = "1.21.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" + +[[package]] +name = "openssl-probe" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" + +[[package]] +name = "percent-encoding" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" + +[[package]] +name = "pin-project-lite" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" + +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + +[[package]] +name = "proc-macro2" +version = "1.0.104" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proc-macro2-diagnostics" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8" +dependencies = [ + "proc-macro2", + "quote", + "syn", + "version_check", +] + +[[package]] +name = "quote" +version = "1.0.42" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "regex" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" + +[[package]] +name = "ring" +version = "0.17.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "untrusted", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.23.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "758025cb5fccfd3bc2fd74708fd4682be41d99e5dff73c377c0646c6012c73a4" +dependencies = [ + "log", + "once_cell", + "ring", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-native-certs" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework", +] + +[[package]] +name = "rustls-pki-types" +version = "1.13.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21e6f2ab2928ca4291b86736a8bd920a277a399bba1589409d72154ff87c1282" +dependencies = [ + "zeroize", +] + +[[package]] +name = "rustls-webpki" +version = "0.103.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2ffdfa2f5286e2247234e03f680868ac2815974dc39e00ea15adc445d0aafe52" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + +[[package]] +name = "ryu" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" + +[[package]] +name = "schannel" +version = "0.1.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +dependencies = [ + "windows-sys 0.61.2", +] + +[[package]] +name = "security-framework" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d" +dependencies = [ + "bitflags", + "core-foundation", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework-sys" +version = "2.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +dependencies = [ + "core-foundation-sys", + "libc", +] + +[[package]] +name = "serde" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" +dependencies = [ + "serde_core", + "serde_derive", +] + +[[package]] +name = "serde_core" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.228" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "serde_json" +version = "1.0.149" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" +dependencies = [ + "itoa", + "memchr", + "serde", + "serde_core", + "zmij", +] + +[[package]] +name = "serde_path_to_error" +version = "0.1.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457" +dependencies = [ + "itoa", + "serde", + "serde_core", +] + +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "simd-adler32" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" + +[[package]] +name = "slab" +version = "0.4.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" + +[[package]] +name = "smallvec" +version = "1.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03" + +[[package]] +name = "socket2" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881" +dependencies = [ + "libc", + "windows-sys 0.60.2", +] + +[[package]] +name = "subtle" +version = "2.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" + +[[package]] +name = "syn" +version = "2.0.112" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21f182278bf2d2bcb3c88b1b08a37df029d71ce3d3ae26168e3c653b213b99d4" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "sync_wrapper" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" + +[[package]] +name = "teapot" +version = "0.1.0" +dependencies = [ + "axum", + "axum-extra", + "bytes", + "data-encoding", + "eyre", + "flate2", + "form_urlencoded", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-util", + "maud", + "percent-encoding", + "regex", + "ring", + "rustls", + "serde", + "serde_json", + "thiserror", + "time", + "tokio", + "tokio-test", + "toml", + "tower", + "tower-http", + "tracing", + "tracing-subscriber", + "xitter-txid", +] + +[[package]] +name = "thiserror" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "thread_local" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "time" +version = "0.3.47" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde_core", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" + +[[package]] +name = "time-macros" +version = "0.2.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215" +dependencies = [ + "num-conv", + "time-core", +] + +[[package]] +name = "tokio" +version = "1.49.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" +dependencies = [ + "bytes", + "libc", + "mio", + "pin-project-lite", + "socket2", + "tokio-macros", + "windows-sys 0.61.2", +] + +[[package]] +name = "tokio-macros" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" +dependencies = [ + "rustls", + "tokio", +] + +[[package]] +name = "tokio-stream" +version = "0.1.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-test" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f6d24790a10a7af737693a3e8f1d03faef7e6ca0cc99aae5066f533766de545" +dependencies = [ + "futures-core", + "tokio", + "tokio-stream", +] + +[[package]] +name = "tokio-util" +version = "0.7.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2efa149fe76073d6e8fd97ef4f4eca7b67f599660115591483572e406e165594" +dependencies = [ + "bytes", + "futures-core", + "futures-sink", + "pin-project-lite", + "tokio", +] + +[[package]] +name = "toml" +version = "1.0.3+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7614eaf19ad818347db24addfa201729cf2a9b6fdfd9eb0ab870fcacc606c0c" +dependencies = [ + "indexmap", + "serde_core", + "serde_spanned", + "toml_datetime", + "toml_parser", + "toml_writer", + "winnow", +] + +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + +[[package]] +name = "toml_parser" +version = "1.0.9+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" +dependencies = [ + "winnow", +] + +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + +[[package]] +name = "tower" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4e6559d53cc268e5031cd8429d05415bc4cb4aefc4aa5d6cc35fbf5b924a1f8" +dependencies = [ + "async-compression", + "bitflags", + "bytes", + "futures-core", + "futures-util", + "http", + "http-body", + "http-body-util", + "http-range-header", + "httpdate", + "mime", + "mime_guess", + "percent-encoding", + "pin-project-lite", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + +[[package]] +name = "tracing" +version = "0.1.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100" +dependencies = [ + "log", + "pin-project-lite", + "tracing-attributes", + "tracing-core", +] + +[[package]] +name = "tracing-attributes" +version = "0.1.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "tracing-core" +version = "0.1.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a" +dependencies = [ + "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f30143827ddab0d256fd843b7a66d164e9f271cfa0dde49142c5ca0ca291f1e" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex-automata", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + +[[package]] +name = "unicode-ident" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "valuable" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" + +[[package]] +name = "version_check" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", +] + +[[package]] +name = "wasi" +version = "0.11.1+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b" + +[[package]] +name = "windows-link" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-sys" +version = "0.60.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" +dependencies = [ + "windows-targets 0.53.5", +] + +[[package]] +name = "windows-sys" +version = "0.61.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" +dependencies = [ + "windows-link", +] + +[[package]] +name = "windows-targets" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" +dependencies = [ + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", + "windows_i686_gnullvm 0.52.6", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.53.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" +dependencies = [ + "windows-link", + "windows_aarch64_gnullvm 0.53.1", + "windows_aarch64_msvc 0.53.1", + "windows_i686_gnu 0.53.1", + "windows_i686_gnullvm 0.53.1", + "windows_i686_msvc 0.53.1", + "windows_x86_64_gnu 0.53.1", + "windows_x86_64_gnullvm 0.53.1", + "windows_x86_64_msvc 0.53.1", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" + +[[package]] +name = "windows_i686_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" + +[[package]] +name = "windows_i686_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" + +[[package]] +name = "windows_i686_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.53.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" + +[[package]] +name = "winnow" +version = "0.7.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" + +[[package]] +name = "xitter-txid" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48d81e07015ad7af9c564af96e91f377562d026d573be6d0d34615e0cabde372" +dependencies = [ + "data-encoding", + "hmac-sha256", +] + +[[package]] +name = "zeroize" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0" + +[[package]] +name = "zmij" +version = "1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "317f17ff091ac4515f17cc7a190d2769a8c9a96d227de5d64b500b01cda8f2cd" diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..5ffa1c2 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,113 @@ +[package] +name = "teapot" +version = "0.1.0" +edition = "2024" +authors = [ "amaanq" ] +description = "A privacy-focused Twitter/X frontend written in Rust" +license = "AGPL-3.0" +repository = "https://github.com/amaanq/teapot" +keywords = [ "twitter", "privacy", "frontend", "teapot" ] +categories = [ "web-programming::http-server" ] + +[dependencies] +axum = { features = [ "macros" ], version = "0.8.8" } +axum-extra = { features = [ "cookie" ], version = "0.12.5" } +bytes = "1.11.1" +data-encoding = "2.10.0" +eyre = "0.6.12" +flate2 = "1.1.9" +form_urlencoded = "1.2.2" +http-body-util = "0.1.3" +hyper = { features = [ "http1", "client" ], version = "1.8.1" } +hyper-rustls = { default-features = false, features = [ + "ring", + "http1", + "tls12", + "logging", + "native-tokio", +], version = "0.27.7" } +hyper-util = { features = [ "client-legacy", "http1", "tokio" ], version = "0.1.20" } +maud = { features = [ "axum" ], version = "0.27.0" } +percent-encoding = "2.3.2" +regex = "1.12.3" +ring = "0.17.14" +rustls = { default-features = false, features = [ + "ring", + "logging", + "std", + "tls12", +], version = "0.23.37" } +serde = { features = [ "derive" ], version = "1.0.228" } +serde_json = "1.0.149" +thiserror = "2.0.18" +time = { features = [ "formatting", "parsing", "macros", "serde" ], version = "0.3.47" } +tokio = { features = [ + "rt-multi-thread", + "net", + "macros", + "sync", + "fs", + "io-util", +], version = "1.49.0" } +toml = "1.0.3" +tower = "0.5.3" +tower-http = { features = [ "fs", "cors", "compression-gzip" ], version = "0.6.8" } +tracing = "0.1.44" +tracing-subscriber = { features = [ "env-filter" ], version = "0.3.22" } +xitter-txid = { default-features = false, version = "0.1.0" } + +[dev-dependencies] +tokio-test = "0.4.5" + +[profile.release] +codegen-units = 1 +lto = true +panic = "abort" +strip = true + +[lints.clippy] +cargo = { level = "warn", priority = -1 } +nursery = { level = "warn", priority = -1 } +pedantic = { level = "warn", priority = -1 } + +blanket_clippy_restriction_lints = "allow" +restriction = { level = "warn", priority = -1 } + +arbitrary_source_item_ordering = "allow" +arithmetic_side_effects = "allow" +as_conversions = "allow" +default_numeric_fallback = "allow" +else_if_without_else = "allow" +error_impl_error = "allow" +exhaustive_structs = "allow" +expect_used = "allow" +float_arithmetic = "allow" +implicit_return = "allow" +indexing_slicing = "allow" +integer_division = "allow" +integer_division_remainder_used = "allow" +let_underscore_must_use = "allow" +let_underscore_untyped = "allow" +map_err_ignore = "allow" +missing_docs_in_private_items = "allow" +mod_module_files = "allow" +multiple_crate_versions = "allow" +needless_borrowed_reference = "allow" +non_ascii_literal = "allow" +pub_use = "allow" +question_mark_used = "allow" +ref_patterns = "allow" +semicolon_outside_block = "allow" +separated_literal_suffix = "allow" +shadow_reuse = "allow" +single_call_fn = "allow" +single_char_lifetime_names = "allow" +std_instead_of_alloc = "allow" +std_instead_of_core = "allow" +string_slice = "allow" +too_many_lines = "allow" +unimplemented = "allow" +unreachable = "allow" +unwrap_in_result = "allow" +unwrap_used = "allow" +wildcard_enum_match_arm = "allow" diff --git a/README.md b/README.md new file mode 100644 index 0000000..aeabd91 --- /dev/null +++ b/README.md @@ -0,0 +1,105 @@ +# teapot + +A privacy-focused Twitter/X frontend written in Rust. + +## Features + +- **Privacy-focused**: No JavaScript required, no tracking, no ads +- **RSS feeds**: Subscribe to any user's tweets +- **FxEmbed-style Discord embeds**: + - Multiple images displayed in carousel on mobile + - Videos play directly in the embed + - Proper image dimensions for better layout + - ActivityPub JSON endpoint for rich embeds + +## Requirements + +- Rust 1.70+ +- Twitter/X session tokens for API access + +## Building + +```bash +cargo build --release +``` + +## Configuration + +1. Copy the example config: + +```bash +cp config/teapot.example.toml config/teapot.toml +``` + +1. Edit `config/teapot.toml` with your settings. + +2. Create a sessions file with your Twitter/X credentials: + +```bash +cp sessions.example.jsonl sessions.jsonl +# Edit sessions.jsonl with your auth_token and ct0 from browser cookies +``` + +## Running + +```bash +# Development +cargo run + +# Production +./target/release/teapot +``` + +The server will start on `http://localhost:8080` by default. + +## Getting Twitter Sessions + +To use the Twitter API, you need session tokens from a logged-in Twitter account: + +1. Log into Twitter/X in your browser +2. Open Developer Tools (F12) → Application → Cookies +3. Copy the values of `auth_token` and `ct0` +4. Add them to `sessions.jsonl` + +## Project Structure + +``` +teapot/ +├── src/ +│ ├── main.rs # Entry point +│ ├── config.rs # Configuration +│ ├── error.rs # Error types +│ ├── types/ # Data structures (User, Tweet, Timeline, etc.) +│ ├── api/ # Twitter API client, OAuth, parsing +│ ├── cache/ # In-process caching +│ ├── routes/ # HTTP route handlers +│ ├── views/ # Maud HTML templates +│ └── utils/ # Utilities (HMAC, formatters) +├── public/ # Static assets (CSS, JS, fonts) +├── config/ # Configuration files +└── Cargo.toml # Dependencies +``` + +## Embed Improvements + +This rewrite includes FxEmbed-style improvements for Discord embeds: + +### Multiple Images + +Discord can display all images from a tweet in a carousel. This works via an ActivityPub JSON endpoint at `/users/{username}/statuses/{id}` that returns media attachments. + +### Video Playback + +Videos can be played directly in Discord embeds using `twitter:player` meta tags: + +- `twitter:player` - Embed URL +- `twitter:player:stream` - Direct MP4 URL +- `twitter:player:width/height` - Dimensions + +### Image Dimensions + +All images include `og:image:width` and `og:image:height` meta tags for proper layout. + +## License + +AGPL-3.0 diff --git a/config/teapot.example.toml b/config/teapot.example.toml new file mode 100644 index 0000000..5ff18da --- /dev/null +++ b/config/teapot.example.toml @@ -0,0 +1,56 @@ +# teapot Configuration + +[server] +# Domain for generating links - change this to your own domain/ip +hostname = "localhost" +title = "teapot" +address = "0.0.0.0" +port = 8080 +https = false # Set to true when using HTTPS +httpMaxConnections = 100 +staticDir = "./public" + +[cache] +# Cache TTL settings (in-process cache, no external dependencies) +listMinutes = 240 # How long to cache list info +rssMinutes = 10 # How long to cache RSS queries + +[config] +# Secret key for cryptographic signing of video URLs +hmacKey = "secretkey" + +# Use base64 encoding for proxied media URLs +base64Media = false + +# Enable RSS feeds +enableRSS = true + +# Enable debug endpoints (/.sessions) +enableDebug = false + +# HTTP/HTTPS proxy (SOCKS proxies not supported) +proxy = "" +proxyAuth = "" + +# API proxy host, e.g. localhost:7000 +apiProxy = "" + +# Enable this if cookie-based auth is failing +disableTid = false + +# Max requests per session to avoid race conditions +maxConcurrentReqs = 2 + +[preferences] +# Default theme +theme = "teapot" + +# URL replacements for privacy +replaceTwitter = "" +replaceYouTube = "" +replaceReddit = "" + +# Media handling +proxyVideos = true +hlsPlayback = false +infiniteScroll = false diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..a11c598 --- /dev/null +++ b/flake.lock @@ -0,0 +1,66 @@ +{ + "nodes": { + "fenix": { + "inputs": { + "nixpkgs": [ + "nixpkgs" + ], + "rust-analyzer-src": "rust-analyzer-src" + }, + "locked": { + "lastModified": 1772261909, + "narHash": "sha256-8XbJXrhMFhLgoBrjFIJx5XJi+SD+7/gbvaIXCuqy9Z0=", + "owner": "nix-community", + "repo": "fenix", + "rev": "e4c413b9546d6c9e6426b33b4d6de1a49a375024", + "type": "github" + }, + "original": { + "owner": "nix-community", + "repo": "fenix", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1772173633, + "narHash": "sha256-MOH58F4AIbCkh6qlQcwMycyk5SWvsqnS/TCfnqDlpj4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "c0f3d81a7ddbc2b1332be0d8481a672b4f6004d6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "fenix": "fenix", + "nixpkgs": "nixpkgs" + } + }, + "rust-analyzer-src": { + "flake": false, + "locked": { + "lastModified": 1772178959, + "narHash": "sha256-DkjUvrEnnhHjOcjMx6aXfYGIZ0PWmcYzvVayhRj1r4M=", + "owner": "rust-lang", + "repo": "rust-analyzer", + "rev": "8494a8b3b769c17e8594d811012cc1b0fab090c7", + "type": "github" + }, + "original": { + "owner": "rust-lang", + "ref": "nightly", + "repo": "rust-analyzer", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..b1e6a40 --- /dev/null +++ b/flake.nix @@ -0,0 +1,373 @@ +{ + description = "teapot - A privacy-focused Twitter/X frontend written in Rust"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + fenix = { + url = "github:nix-community/fenix"; + inputs.nixpkgs.follows = "nixpkgs"; + }; + }; + + outputs = + { + self, + nixpkgs, + fenix, + }: + let + systems = [ + "x86_64-linux" + "aarch64-linux" + "x86_64-darwin" + "aarch64-darwin" + ]; + forAllSystems = nixpkgs.lib.genAttrs systems; + in + { + packages = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + fenixPkgs = fenix.packages.${system}.latest; + rustPlatform = pkgs.makeRustPlatform { + cargo = fenixPkgs.cargo; + rustc = fenixPkgs.rustc; + }; + in + { + default = rustPlatform.buildRustPackage { + pname = "teapot"; + version = "0.1.0"; + + src = pkgs.lib.fileset.toSource { + root = ./.; + fileset = pkgs.lib.fileset.unions [ + ./Cargo.toml + ./Cargo.lock + ./src + ./sass + ./public + ./config + ]; + }; + + cargoLock.lockFile = ./Cargo.lock; + + nativeBuildInputs = [ + pkgs.pkg-config + pkgs.dart-sass + ]; + + doCheck = false; + + postInstall = '' + mkdir -p $out/share/teapot + cp -r public $out/share/teapot/ + cp -r config $out/share/teapot/ + # Compile SCSS + sass sass/index.scss:$out/share/teapot/public/css/style.css \ + --load-path=sass/include --style=compressed --no-source-map + ''; + + meta = with pkgs.lib; { + description = "A privacy-focused Twitter/X frontend written in Rust"; + homepage = "https://github.com/amaanq/teapot"; + license = licenses.agpl3Only; + mainProgram = "teapot"; + }; + }; + } + ); + + devShells = forAllSystems ( + system: + let + pkgs = nixpkgs.legacyPackages.${system}; + toolchain = fenix.packages.${system}.latest.toolchain; + in + { + default = pkgs.mkShell { + buildInputs = [ + toolchain + pkgs.rust-analyzer + pkgs.pkg-config + ]; + + shellHook = '' + echo "teapot development shell" + echo "" + echo "Quick start:" + echo " 1. cp config/teapot.example.toml config/teapot.toml" + echo " 2. Create sessions.jsonl with your tokens" + echo " 3. cargo run" + ''; + }; + } + ); + + nixosModules.default = + { + config, + lib, + pkgs, + ... + }: + let + cfg = config.services.teapot; + configFile = pkgs.writeText "teapot.toml" '' + ${lib.generators.toINI + { + mkKeyValue = lib.generators.mkKeyValueDefault { + mkValueString = + v: + if lib.isString v then + "\"" + (lib.escape [ "\"" ] (toString v)) + "\"" + else + lib.generators.mkValueStringDefault { } v; + } " = "; + } + ( + lib.recursiveUpdate { + inherit (cfg) cache preferences; + config = cfg.config // { + hmacKey = "@hmac@"; + }; + server = cfg.server // { + staticDir = "${cfg.package}/share/teapot/public"; + }; + } cfg.settings + ) + } + ''; + preStart = pkgs.writers.writePython3 "teapot-prestart" { } '' + import os + import secrets + + state_dir = os.environ.get("STATE_DIRECTORY") + if not os.path.isfile(f"{state_dir}/hmac"): + hmac = secrets.token_hex(32) + with open(f"{state_dir}/hmac", "w") as f: + f.write(hmac) + else: + with open(f"{state_dir}/hmac", "r") as f: + hmac = f.read() + + configFile = "${configFile}" + with open(configFile, "r") as f_in: + with open(f"{state_dir}/teapot.toml", "w") as f_out: + f_out.write(f_in.read().replace("@hmac@", hmac)) + ''; + in + { + options.services.teapot = { + enable = lib.mkEnableOption "teapot, a privacy-focused Twitter/X frontend"; + + package = lib.mkPackageOption self.packages.${pkgs.system} "default" { }; + + server = { + hostname = lib.mkOption { + type = lib.types.str; + default = "localhost"; + example = "teapot.example.com"; + description = "Hostname of the instance."; + }; + title = lib.mkOption { + type = lib.types.str; + default = "teapot"; + description = "Title of the instance."; + }; + address = lib.mkOption { + type = lib.types.str; + default = "0.0.0.0"; + example = "127.0.0.1"; + description = "The address to listen on."; + }; + port = lib.mkOption { + type = lib.types.port; + default = 8080; + description = "The port to listen on."; + }; + https = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Set secure attribute on cookies. Enable when using HTTPS."; + }; + httpMaxConnections = lib.mkOption { + type = lib.types.int; + default = 100; + description = "Maximum number of HTTP connections."; + }; + }; + + cache = { + listMinutes = lib.mkOption { + type = lib.types.int; + default = 240; + description = "How long to cache list info (minutes)."; + }; + rssMinutes = lib.mkOption { + type = lib.types.int; + default = 10; + description = "How long to cache RSS queries (minutes)."; + }; + }; + + config = { + base64Media = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Use base64 encoding for proxied media URLs."; + }; + enableRSS = lib.mkEnableOption "RSS feeds" // { + default = true; + }; + enableDebug = lib.mkEnableOption "request logs and debug endpoints"; + proxy = lib.mkOption { + type = lib.types.str; + default = ""; + description = "URL to a HTTP/HTTPS proxy."; + }; + proxyAuth = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Credentials for proxy."; + }; + apiProxy = lib.mkOption { + type = lib.types.str; + default = ""; + description = "API proxy host for requests."; + }; + disableTid = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Disable TID for cookie-based auth."; + }; + maxConcurrentReqs = lib.mkOption { + type = lib.types.int; + default = 2; + description = "Max concurrent requests per session."; + }; + }; + + preferences = { + theme = lib.mkOption { + type = lib.types.str; + default = "teapot"; + description = "Instance theme."; + }; + replaceTwitter = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Replace Twitter links with links to this instance."; + }; + replaceYouTube = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Replace YouTube links with this instance."; + }; + replaceReddit = lib.mkOption { + type = lib.types.str; + default = ""; + description = "Replace Reddit links with this instance."; + }; + proxyVideos = lib.mkOption { + type = lib.types.bool; + default = true; + description = "Proxy video streaming through the server."; + }; + hlsPlayback = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Enable HLS video streaming (requires JavaScript)."; + }; + infiniteScroll = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Infinite scrolling (requires JavaScript)."; + }; + }; + + settings = lib.mkOption { + type = lib.types.attrs; + default = { }; + description = "Additional settings to override module-generated config."; + }; + + sessionsFile = lib.mkOption { + type = lib.types.path; + default = "/var/lib/teapot/sessions.jsonl"; + description = '' + Path to the session tokens file (JSONL format). + + Each line: {"oauth_token":"...","oauth_token_secret":"..."} + ''; + }; + + openFirewall = lib.mkOption { + type = lib.types.bool; + default = false; + description = "Open ports in the firewall for the web interface."; + }; + }; + + config = lib.mkIf cfg.enable { + systemd.services.teapot = { + description = "teapot (privacy-focused Twitter/X frontend)"; + wantedBy = [ "multi-user.target" ]; + wants = [ "network-online.target" ]; + after = [ "network-online.target" ]; + serviceConfig = { + DynamicUser = true; + LoadCredential = "sessionsFile:${cfg.sessionsFile}"; + StateDirectory = "teapot"; + Environment = [ + "TEAPOT_CONF_FILE=/var/lib/teapot/teapot.toml" + "TEAPOT_SESSIONS_FILE=%d/sessionsFile" + ]; + WorkingDirectory = "${cfg.package}/share/teapot"; + ExecStart = "${cfg.package}/bin/teapot"; + ExecStartPre = "${preStart}"; + AmbientCapabilities = lib.mkIf (cfg.server.port < 1024) [ "CAP_NET_BIND_SERVICE" ]; + Restart = "on-failure"; + RestartSec = "5s"; + # Hardening + CapabilityBoundingSet = if (cfg.server.port < 1024) then [ "CAP_NET_BIND_SERVICE" ] else [ "" ]; + DeviceAllow = [ "" ]; + LockPersonality = true; + MemoryDenyWriteExecute = true; + PrivateDevices = true; + PrivateUsers = cfg.server.port >= 1024; + ProcSubset = "pid"; + ProtectClock = true; + ProtectControlGroups = true; + ProtectHome = true; + ProtectHostname = true; + ProtectKernelLogs = true; + ProtectKernelModules = true; + ProtectKernelTunables = true; + ProtectProc = "invisible"; + RestrictAddressFamilies = [ + "AF_INET" + "AF_INET6" + ]; + RestrictNamespaces = true; + RestrictRealtime = true; + RestrictSUIDSGID = true; + SystemCallArchitectures = "native"; + SystemCallFilter = [ + "@system-service" + "~@privileged" + "~@resources" + ]; + UMask = "0077"; + }; + }; + + networking.firewall = lib.mkIf cfg.openFirewall { + allowedTCPPorts = [ cfg.server.port ]; + }; + }; + }; + }; +} diff --git a/public/android-chrome-192x192.png b/public/android-chrome-192x192.png new file mode 100644 index 0000000000000000000000000000000000000000..e0097fec5d5a1a693f34ea228ef051494f91d317 GIT binary patch literal 2861 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 literal 0 HcmV?d00001 diff --git a/public/android-chrome-384x384.png b/public/android-chrome-384x384.png new file mode 100644 index 0000000000000000000000000000000000000000..beb4965ebf795dda36a7c342808d1b3e8cf703da GIT binary patch literal 6294 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 literal 0 HcmV?d00001 diff --git a/public/android-chrome-512x512.png b/public/android-chrome-512x512.png new file mode 100644 index 0000000000000000000000000000000000000000..fb7411f131857d6cf0c7c779b9acbba85e13561a GIT binary patch literal 2319 zcma)7dsGwY6(8#ZHDX-Iqbm}ySV4s(Vk-(0ke56n1mgn`CcyfjE@+?|LmVWis2r1B z(nTv$mjn_*+Oj5E)(i!%j?H}FK@0{PcbMKt{ z-8=XDeZM)MlbzTVBW7L+DtB?7>85oV@jA>xPb zPiJNU0K66ePM!e(4-z_Q2Y_Q#0C-gl0I`<=fLQT`dMAjy2*}y7I~B3M^5WU@; z*32ZI&&Tb25QjwPahTb=fNd`a524OS^t6~!l$aDxSw4^$NgTuXHY8}1CM`K#w5kwxt(!ZAN-Z##ls6JA?4|!_{s+H zg|X~_*~YP3g9CwoW(3r}G1mSHnoJ3;c{Q|zGZ5C4EydzRt)1c?PC!iuZxJ+^6I%0P zNGqA0K39}ld-$opVGl#zuYY|bp*g+~JgbBw&&C(b$Q)g|{r10(GVL;5!j*W~iB(AC z44kjRbHK$=Ux<_ z;$I?@#oI#GlM*HC#M_AkQL7%OZx!K0vN1tvf104aKU^7{B;nP}H)=f#tXUxu;v@v= z#Nc_!W}>KVOx2Q{KySk9+a3x132(T*_hx zFG|nFW7|6g^`<~LGwO}ix-iF|Y|Weu7Z29()7_tTen?`PNsoqG&NeswSMQ+_qv!TTkY4}Qk4 zo-u~e4|7UfTRhHbINTZe%!am)_uy_FdIBL?Q+|06!ec*%Vz$2RT$LByR8OQ)XG8F=GP$jmiA#WlJXU#N}hH#H!oI_qzSafk)YIMBQ|AU4`)b zp*u*{$F5?y8>L*|5)Bh<9*HDnjpN6YC^%8aJ3*qDB7|ace z)$9aYMs9LJZzQb<6tgI(46#%7cvKo8tLHe zP>a3PamUtTe_%t21UXL4S&jWKwj+*sU*DdJXx$aVCiBtl`o3Eqn%F_ThCm&PIMbZ6 zi-<@QMZF>3NbkPB5L)3}Vf5;cCW{E;=~z~87;UQLY0GTACCYK#hQjHPu*Wm7coYd~ zq9?!cj%B}D2EW3w-2c3R)ViOi#vor3M5}C&5BCS^h7HgCuFLMLY8>0$Z!!*qe`{2{ za9udzC00)v?{O?k#K~B33h(sAnj8{%-o4;MPdmtMd=~in=hn>aX(Ik2ymu|(+BxL6 z|7%Wa9_K&_=V0vq-ycK_5KWEV7)6bV+PGyml^VN|8oO!Z2IPTC-I1}}_FoA{*ar?D XI{v>CXt=LgNCJ?Nnw_FdDn9)mGzEdh literal 0 HcmV?d00001 diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6909edc68a607068dc849f08dd9c48ff5c4fcdd4 GIT binary patch literal 1325 zcmV+|1=9M7P)Px#El^BUMMFYD|7>9WYhXn}LQX_P@o8R%QB71uM1D_9 z_iA6-WLl$FQNUhRXG%xqW?V@*gUL73NFnEdX`+~s|5-+PS3h!G=3j2JOu)C^JS6!*TkkZ63}&tl|1_WXB# zZ2wQM5_&eFEC&8#$7z0y(xXb~dpSB;@SbXJS?Gt=&_tbaq?Eq*eWuM7N;;Dus*KL# z2KTBqk5xy18S3~zwAk~b+rzW?SRI`{8DB$dOMX7Whfe0s{vv)Use!)RRna?0X^#8R z4SlM6yB2yZmCNtp)jn9Y$i4=?Z|t;au}R5w9t6#=B$SMTpjl%lrC(vt?8J8?Q=`?5 z&- z7W-k)treFcK+s|}O|J1EXl)3iAZXV7N$GAFw0P-Ae;D-G5JU*H(-1&tWeCOe2STgg zE&dFpT9gd|g=QPF9uA}0VF)nQD*X+@Tu9jvU}&!DVe3Y4s@08H=?{l4)x$V+sU8NR z#b#=yzva?V5&=*Td64%XB)qQknivfsaYD zD$A7qkcI$7XG(2DK%jG_$`BqyRp&~jA+(XI^Hz>!2oQ8`T`K*(=1A3fqQ0DD4H5)e z{(onU3!^~-K+iwRm74W10)5}8+zmrOp!;U!o*M!LEtSrXSg}C@L95Dr+c1Rj`#{i^ zb@53(3_?g|D(>&o4HLIA3^ z(%%MDH8U!AWe7<0hWUhW(COgNe8%i#FA*Ivm}=7ySYz!_;Lxg-K74o@ + + + + #2b5797 + + + diff --git a/public/css/fontello.css b/public/css/fontello.css new file mode 100644 index 0000000..52362d8 --- /dev/null +++ b/public/css/fontello.css @@ -0,0 +1,138 @@ +@font-face { + font-family: "fontello"; + src: url("/fonts/fontello.eot?77185648"); + src: + url("/fonts/fontello.eot?77185648#iefix") format("embedded-opentype"), + url("/fonts/fontello.woff2?77185648") format("woff2"), + url("/fonts/fontello.woff?77185648") format("woff"), + url("/fonts/fontello.ttf?77185648") format("truetype"), + url("/fonts/fontello.svg?77185648#fontello") format("svg"); + font-weight: normal; + font-style: normal; +} + +[class^="icon-"]:before, +[class*=" icon-"]:before { + font-family: "fontello"; + font-style: normal; + font-weight: normal; + speak: never; + + display: inline-block; + text-decoration: inherit; + width: 1em; + margin-right: 0.2em; + text-align: center; + + /* For safety - reset parent styles, that can break glyph codes*/ + font-variant: normal; + text-transform: none; + + /* fix buttons height, for twitter bootstrap */ + line-height: 1em; + + /* Font smoothing. That was taken from TWBS */ + -webkit-font-smoothing: antialiased; + -moz-osx-font-smoothing: grayscale; +} + +.icon-views:before { + content: "\e800"; +} + +/* '' */ +.icon-heart:before { + content: "\e801"; +} + +/* '' */ +.icon-quote:before { + content: "\e802"; +} + +/* '' */ +.icon-comment:before { + content: "\e803"; +} + +/* '' */ +.icon-play:before { + content: "\e805"; +} + +/* '' */ +.icon-link:before { + content: "\e806"; +} + +/* '' */ +.icon-calendar:before { + content: "\e807"; +} + +/* '' */ +.icon-location:before { + content: "\e808"; +} + +/* '' */ +.icon-picture:before { + content: "\e809"; +} + +/* '' */ +.icon-lock:before { + content: "\e80a"; +} + +/* '' */ +.icon-down:before { + content: "\e80b"; +} + +/* '' */ +.icon-retweet:before { + content: "\e80c"; +} + +/* '' */ +.icon-search:before { + content: "\e80d"; +} + +/* '' */ +.icon-pin:before { + content: "\e80e"; +} + +/* '' */ +.icon-cog:before { + content: "\e80f"; +} + +/* '' */ +.icon-rss:before { + content: "\e810"; +} + +/* '' */ +.icon-ok:before { + content: "\e811"; +} + +/* '' */ +.icon-circle:before { + content: "\f111"; +} + +/* '' */ +.icon-info:before { + content: "\f128"; +} + +/* '' */ +.icon-bird:before { + content: "\f309"; +} + +/* '' */ diff --git a/public/css/style.css b/public/css/style.css new file mode 100644 index 0000000..97cd31b --- /dev/null +++ b/public/css/style.css @@ -0,0 +1 @@ +.conversation,.edit-history{max-width:600px;margin:0 auto;float:none;border-radius:0;position:relative;width:100%}.conversation .show-more,.edit-history .show-more{margin-bottom:10px}.main-thread,.latest-edit{margin-bottom:20px}.reply{margin-bottom:10px}body.fixed-nav .main-tweet,body.fixed-nav .replies,body.fixed-nav .edit-history>div{padding-top:50px;margin-top:-50px}.edit-history-header{padding:10px;margin-bottom:5px;font-size:16px;font-weight:bold;background-color:var(--bg_panel)}.tweet-edit{margin-bottom:5px}.main-tweet .tweet-content{font-size:18px}@media(max-width: 600px){.main-tweet .tweet-content{font-size:16px}}.thread-line .timeline-item::before,.thread-line.timeline-item::before{background:var(--accent_dark);content:"";position:relative;min-width:3px;width:3px;left:26px;border-radius:2px;margin-left:-3px;margin-bottom:37px;top:56px;z-index:1;pointer-events:none}.thread-line .with-header:not(:first-child)::after{background:var(--accent_dark);content:"";position:relative;float:left;min-width:3px;width:3px;right:calc(100% - 26px);border-radius:2px;margin-left:-3px;margin-bottom:37px;bottom:10px;height:30px;z-index:1;pointer-events:none}.thread-line .unavailable::before{top:48px;margin-bottom:28px}.thread-line .more-replies::before{content:"...";background:unset;color:var(--more_replies_dots);font-weight:bold;font-size:20px;line-height:.25em;left:1.2em;width:5px;top:2px;margin-bottom:0;margin-left:-2.5px}.thread-line .earlier-replies{padding-bottom:0;margin-bottom:-5px}.timeline-item.thread-last::before{background:unset;min-width:unset;width:0;margin:0}.more-replies{padding-top:.3em !important}.more-replies-text{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;margin-left:58px;padding:7px 0}.timeline-item.thread.more-replies-thread{padding:0 .75em}.timeline-item.thread.more-replies-thread::before{top:40px;margin-bottom:31px}.timeline-item.thread.more-replies-thread .more-replies{display:flex;padding-top:unset !important;margin-top:8px}.timeline-item.thread.more-replies-thread .more-replies::before{display:inline-block;position:relative;top:-1px;line-height:.4em}.timeline-item.thread.more-replies-thread .more-replies .more-replies-text{display:inline}.gallery-row{display:flex;flex-direction:row;flex-wrap:nowrap;overflow:hidden;flex-grow:1;max-height:379.5px;max-width:533px;pointer-events:all}.gallery-row .still-image{width:100%;align-self:center}.attachments{margin-top:.35em;display:flex;flex-direction:row;width:100%;max-height:600px;border-radius:7px;overflow:hidden;flex-flow:column;background-color:var(--bg_color);align-items:center;pointer-events:all}.attachments .image-attachment{width:100%}.attachment{position:relative;line-height:0;overflow:hidden;margin:0 .25em 0 0;flex-grow:1;box-sizing:border-box;min-width:2em}.attachment:last-child{margin:0;max-height:530px}.gallery-gif video{max-height:530px;background-color:#101010}.still-image{max-height:379.5px;max-width:533px}.still-image img{object-fit:cover;max-width:100%;max-height:379.5px;flex-basis:300px;flex-grow:1}.alt-text{margin:0px;padding:11px 7px;box-sizing:border-box;position:absolute;bottom:10px;left:10px;width:2.98em;max-height:25px;white-space:pre;overflow:hidden;border-radius:10px;color:var(--fg_color);font-size:12px;font-weight:bold;background:rgba(0,0,0,.5);backdrop-filter:blur(12px)}.alt-text:hover{padding:7px;width:min(230px,100% - 20px);max-height:calc(100% - 10px);line-height:1.2em;white-space:pre-wrap;transition-duration:.4s;transition-property:max-height}.image{display:flex}.overlay-circle{border-radius:50%;background-color:var(--dark_grey);width:40px;height:40px;align-items:center;display:flex;border-width:5px;border-color:var(--play_button);border-style:solid}.overlay-triangle{width:0;height:0;border-style:solid;border-width:12px 0 12px 17px;border-color:rgba(0,0,0,0) rgba(0,0,0,0) rgba(0,0,0,0) var(--play_button);margin-left:14px}.media-gif{display:table;background-color:unset;width:unset}.media-body{flex:1;padding:0;white-space:pre-wrap}video{height:100%;width:100%}.gallery-video{display:flex;overflow:hidden}.gallery-video.card-container{flex-direction:column;width:100%}.video-container{min-height:80px;min-width:200px;max-height:530px;margin:0}.video-container img{max-height:100%;max-width:100%}.video-overlay{position:absolute;width:100%;height:100%;top:0;left:0;z-index:1}.video-overlay:hover .overlay-circle{border-color:var(--play_button_hover)}.video-overlay:hover .overlay-triangle{border-color:rgba(0,0,0,0) rgba(0,0,0,0) rgba(0,0,0,0) var(--play_button_hover)}.video-overlay{background-color:rgba(0,0,0,.6)}.video-overlay p{position:relative;z-index:0;text-align:center;top:calc(50% - 20px);font-size:20px;line-height:1.3;margin:0 20px}.video-overlay .overlay-circle{position:relative;z-index:0;top:calc(50% - 20px);margin:0 auto;width:40px;height:40px}.video-overlay .overlay-duration{position:absolute;bottom:8px;left:8px;background-color:rgba(0,0,0,.4784313725);line-height:1em;padding:4px 6px 4px 6px;border-radius:5px;font-weight:bold}.video-overlay form{width:100%;height:100%;align-items:center;justify-content:center;display:flex}.video-overlay button{padding:5px 8px;font-size:16px}.embed-video .gallery-video{width:100%;height:100%;position:absolute;background-color:#000;top:0%;left:0%}.embed-video .video-container{max-height:unset}.card{margin:5px 0;pointer-events:all;max-height:unset}.card-container{border:solid 1px var(--dark_grey);border-radius:10px;background-color:var(--bg_elements);overflow:hidden;color:inherit;display:flex;flex-direction:row;text-decoration:none !important}.card-container:hover{border-color:var(--grey)}.card-container .attachments{margin:0;border-radius:0}.card-content{padding:.5em}.card-title{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;white-space:unset;font-weight:bold;font-size:1.1em}.card-description{margin:.3em 0;white-space:pre-wrap}.card-destination{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--grey);display:block}.card-content-container{color:unset;overflow:auto}.card-content-container:hover{text-decoration:none}.card-image-container{width:98px;flex-shrink:0;position:relative;overflow:hidden}.card-image-container:before{content:"";display:block;padding-top:100%}.card-image{position:absolute;top:0;left:0;bottom:0;right:0;background-color:var(--bg_overlays)}.card-image img{width:100%;height:100%;max-height:400px;display:block;object-fit:cover}.card-overlay{position:absolute;width:100%;height:100%;top:0;left:0;z-index:1}.card-overlay:hover .overlay-circle{border-color:var(--play_button_hover)}.card-overlay:hover .overlay-triangle{border-color:rgba(0,0,0,0) rgba(0,0,0,0) rgba(0,0,0,0) var(--play_button_hover)}.card-overlay{opacity:.8;display:flex;justify-content:center;align-items:center}.large .card-container{display:block}.large .card-image-container{width:unset}.large .card-image-container:before{display:none}.large .card-image{position:unset;border-style:solid;border-color:var(--dark_grey);border-width:0;border-bottom-width:1px}.poll-meter{overflow:hidden;position:relative;margin:6px 0;height:26px;background:var(--bg_color);border-radius:5px;display:flex;align-items:center}.poll-choice-bar{height:100%;position:absolute;background:var(--dark_grey)}.poll-choice-value{position:relative;font-weight:bold;margin-left:5px;margin-right:6px;min-width:30px;text-align:right;pointer-events:all}.poll-choice-option{position:relative;pointer-events:all}.poll-info{color:var(--grey);pointer-events:all}.leader .poll-choice-bar{background:var(--accent_dark)}.quote{margin-top:10px;border:solid 1px var(--dark_grey);border-radius:10px;background-color:var(--bg_elements);overflow:hidden;pointer-events:all;position:relative;width:100%}.quote:hover{border-color:var(--grey)}.quote.unavailable:hover{border-color:var(--dark_grey)}.quote .tweet-name-row{padding:8px 10px 6px 10px}.quote .quote-text{overflow:hidden;white-space:pre-wrap;word-wrap:break-word;padding:10px;padding-top:0}.quote .show-thread{padding:0px 10px 6px 10px;margin-top:-6px}.quote .quote-latest{padding:0px 10px 6px 10px;color:var(--grey)}.quote .replying-to{padding:0px 10px;padding-bottom:4px;margin:unset}.quote .community-note{background-color:var(--bg_panel);border:unset;border-top:solid 1px var(--dark_grey);border-radius:unset;margin-top:0}.quote .community-note:hover{border-top-color:var(--grey)}.quote .community-note .community-note-header{background-color:var(--bg_panel);padding-bottom:0}.unavailable-quote{padding:12px;display:block}.quote-link{width:100%;height:100%;left:0;top:0;position:absolute}.quote-media-container{max-height:300px;display:flex}.quote-media-container .card{margin:unset}.quote-media-container .attachments{border-radius:0}.quote-media-container .media-gif{width:100%;display:flex;justify-content:center}.quote-media-container .gallery-gif .attachment{display:flex;justify-content:center;background-color:var(--bg_color)}.quote-media-container .gallery-gif .attachment video{height:unset;width:unset;max-height:100%;max-width:100%}.quote-media-container .gallery-video,.quote-media-container .gallery-gif{max-height:300px}.quote-media-container .still-image img{max-height:250px}.tweet-body{flex:1;min-width:0;margin-left:58px;pointer-events:none;z-index:1}.tweet-content{line-height:1.3em;pointer-events:all;display:inline}.tweet-bidi{display:block !important}.tweet-header{padding:0;vertical-align:bottom;flex-basis:100%;margin-bottom:.2em}.tweet-header a{display:inline-block;word-break:break-all;max-width:100%;pointer-events:all}.tweet-name-row{padding:0;display:flex;justify-content:space-between}.fullname-and-username{display:flex;min-width:0}.fullname{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;flex-shrink:2;max-width:80%;font-size:14px;font-weight:700;color:var(--fg_color)}.username{overflow:hidden;text-overflow:ellipsis;white-space:nowrap;min-width:1.6em;margin-left:.4em;word-wrap:normal}.tweet-date{display:flex;flex-shrink:0;margin-left:4px}.tweet-date a,.username,.show-more a{color:var(--fg_dark)}.tweet-published{margin-top:10px;margin-bottom:3px;color:var(--grey);pointer-events:all}.tweet-avatar{display:contents !important}.tweet-avatar img{float:left;margin-top:3px;margin-left:-58px;width:48px;height:48px}.avatar.round{border-radius:50%;-webkit-user-select:none}.avatar.mini{position:unset;margin-right:5px;margin-top:-1px;width:20px;height:20px}.tweet-embed{display:flex;flex-direction:column;justify-content:center;height:100%;background-color:var(--bg_panel)}.tweet-embed .tweet-content{font-size:18px}.tweet-embed .tweet-body{display:flex;flex-direction:column;max-height:calc(100vh - 1.5em)}.tweet-embed .card-image img{height:auto}.tweet-embed .avatar{position:absolute}.attribution{display:flex;pointer-events:all;margin:5px 0}.attribution strong{color:var(--fg_color)}.media-tag-block{padding-top:5px;pointer-events:all;color:var(--fg_faded)}.media-tag-block .icon-container{padding-right:2px}.media-tag-block .media-tag,.media-tag-block .icon-container{color:var(--fg_faded)}.timeline-container .media-tag-block{font-size:13px}.tweet-geo{color:var(--fg_faded)}.replying-to{color:var(--fg_faded);margin:-2px 0 4px}.replying-to a{pointer-events:all}.retweet-header,.pinned,.tweet-stats{align-content:center;color:var(--grey);display:flex;flex-shrink:0;flex-wrap:wrap;font-size:14px;font-weight:600;line-height:22px}.retweet-header span,.pinned span,.tweet-stats span{overflow:hidden;text-overflow:ellipsis;white-space:nowrap}.retweet-header{margin-top:-5px !important}.tweet-stats{margin-bottom:-3px;-webkit-user-select:none}.tweet-stat{padding-top:5px;min-width:1em;margin-right:.8em}.show-thread{display:block;pointer-events:all;padding-top:2px}.unavailable-box{width:100%;height:100%;padding:12px;border:solid 1px var(--dark_grey);box-sizing:border-box;border-radius:10px;background-color:var(--bg_color);z-index:2}.tweet-link{height:100%;width:100%;left:0;top:0;position:absolute;-webkit-user-select:none}.tweet-link:hover{background-color:var(--bg_hover)}.latest-post-version{border-bottom:1px solid var(--dark_grey);border-top:1px solid var(--dark_grey);padding:1ch 0px;margin:1ch 0px;color:var(--grey)}.latest-post-version a{pointer-events:all}.community-note{background-color:var(--bg_elements);margin-top:10px;border:solid 1px var(--dark_grey);border-radius:10px;overflow:hidden;pointer-events:all}.community-note:hover{background-color:var(--bg_panel);border-color:var(--grey)}.community-note-header{background-color:var(--bg_hover);font-weight:700;padding:8px 10px;padding-top:6px;display:flex;align-items:center;gap:2px}.community-note-header .icon-container{flex-shrink:0;color:var(--accent)}.community-note-text{white-space:pre-line;padding:10px 10px;padding-top:6px}.profile-card{flex-wrap:wrap;background:var(--bg_panel);padding:12px;display:flex}.profile-card-info{overflow:hidden;overflow-wrap:break-word;width:100%}.profile-card-tabs-name{overflow:hidden;overflow-wrap:break-word;max-width:100%}.profile-card-username{overflow:hidden;overflow-wrap:break-word;color:var(--fg_color);font-size:14px;display:block}.profile-card-fullname{overflow:hidden;overflow-wrap:break-word;color:var(--fg_color);font-size:16px;font-weight:bold;text-shadow:none;max-width:100%}.profile-card-avatar{display:inline-block;position:relative;width:100%;margin-right:4px;margin-bottom:6px}.profile-card-avatar:after{content:"";display:block;margin-top:100%}.profile-card-avatar img{box-sizing:border-box;position:absolute;width:100%;height:100%;border:4px solid var(--darker_grey);background:var(--bg_panel)}.profile-card-extra{display:contents;flex:100%;margin-top:7px}.profile-card-extra .profile-bio{overflow:hidden;overflow-wrap:break-word;width:100%;margin:4px -6px 6px 0;white-space:pre-wrap}.profile-card-extra .profile-bio p{margin:0}.profile-card-extra .profile-joindate,.profile-card-extra .profile-location,.profile-card-extra .profile-website{color:var(--fg_faded);margin:1px 0;width:100%}.profile-card-extra-links{margin-top:8px;font-size:14px;width:100%}.profile-statlist{display:flex;flex-wrap:wrap;padding:0;width:100%;justify-content:space-between}.profile-statlist li{display:table-cell;text-align:center}.profile-stat-header{font-weight:bold;color:var(--profile_stat)}.profile-stat-num{display:block;color:var(--profile_stat)}@media(max-width: 700px){.profile-card-info{display:flex}.profile-card-tabs-name{flex-shrink:100}.profile-card-avatar{width:80px;height:80px}.profile-card-avatar img{border-width:2px;width:unset}}.photo-rail-card{float:left;background:var(--bg_panel);border-radius:0 0 4px 4px;width:100%;margin:5px 0}.photo-rail-header{padding:5px 12px 0}.photo-rail-header-mobile{display:none;box-sizing:border-box;padding:5px 12px 0;width:100%;float:unset;color:var(--accent);justify-content:space-between}.photo-rail-grid{display:grid;grid-template-columns:repeat(4, 1fr);grid-gap:3px 3px;padding:5px 12px 12px}.photo-rail-grid a{position:relative;border-radius:5px;background-color:var(--darker_grey)}.photo-rail-grid a:before{content:"";display:block;padding-top:100%}.photo-rail-grid img{height:100%;width:100%;border-radius:4px;object-fit:cover;position:absolute;top:0;left:0;bottom:0;right:0}#photo-rail-grid-toggle{display:none}#photo-rail-grid-toggle:checked~.photo-rail-grid{max-height:640px}#photo-rail-grid-toggle:checked~label .icon-down:before{transform:rotate(180deg) translateY(-1px)}#photo-rail-grid-toggle:checked~.photo-rail-grid{padding-bottom:12px}@media(max-width: 700px){.photo-rail-header{display:none}.photo-rail-header-mobile{display:flex}.photo-rail-grid{max-height:0;padding-bottom:0;overflow:hidden;transition:max-height .4s}.photo-rail-grid{grid-template-columns:repeat(6, 1fr)}#photo-rail-grid-toggle:checked~.photo-rail-grid{max-height:300px}}@media(max-width: 450px){.photo-rail-grid{grid-template-columns:repeat(4, 1fr)}#photo-rail-grid-toggle:checked~.photo-rail-grid{max-height:450px}}.profile-tabs{max-width:900px;margin:0 auto;float:none;border-radius:0;position:relative;width:auto}.profile-tabs .timeline-container{float:right;width:68% !important;max-width:unset}.profile-banner{margin-bottom:4px;background-color:var(--bg_panel)}.profile-banner a{display:block;position:relative;padding:33.34% 0 0 0}.profile-banner img{max-width:100%;position:absolute;top:0}.profile-tab{padding:0 4px 0 0;box-sizing:border-box;display:inline-block;font-size:14px;text-align:left;vertical-align:top;max-width:32%;top:0}body.fixed-nav .profile-tab{top:50px}.profile-result{min-height:54px}.profile-result .username{margin:0 !important}.profile-result .tweet-header{margin-bottom:unset}@media(max-width: 700px){.profile-tabs{width:100vw;max-width:600px}.profile-tabs .timeline-container{width:100% !important}.profile-tabs .timeline-container .tab-item wide{flex-grow:1.4}.profile-tab{width:100%;max-width:unset;position:initial !important;padding:0}}@media(min-height: 900px){.profile-tab.sticky{position:sticky}}.panel-container{margin:auto;font-size:130%}.error-panel{padding:12px;border-radius:4px;display:flex;background:var(--error_red);box-shadow:0 0 15px rgba(0,0,0,.2);margin:auto;margin-top:-50px;text-align:center}.search-bar>form{padding:12px;border-radius:4px;display:flex;background:var(--darkest_grey);box-shadow:0 0 15px rgba(0,0,0,.2);margin:auto;margin-top:-50px}.search-bar>form button{background:var(--bg_elements);color:var(--fg_color);border:0;border-radius:3px;cursor:pointer;font-weight:bold;width:30px;height:30px;padding:0px 5px 1px 8px}.search-bar>form input{font-size:16px;width:100%;background:var(--bg_elements);color:var(--fg_color);border:0;border-radius:4px;padding:4px;margin-right:8px;height:unset}nav{display:flex;align-items:center;background-color:var(--bg_overlays);box-shadow:0 0 4px rgba(0,0,0,.6);padding:0;width:100%;height:50px;z-index:1000;font-size:16px}nav a,nav .icon-button button{color:var(--fg_nav)}body.fixed-nav nav{position:fixed}.inner-nav{margin:auto;box-sizing:border-box;padding:0 10px;display:flex;align-items:center;flex-basis:920px;height:50px}.site-name{font-size:15px;font-weight:600;line-height:1}.site-name:hover{color:var(--accent_light);text-decoration:unset}.site-logo{display:block;width:35px;height:35px}.nav-item{display:flex;flex:1;line-height:50px;height:50px;overflow:hidden;flex-wrap:wrap;align-items:center}.nav-item.right{text-align:right;justify-content:flex-end}.nav-item.right a:hover{color:var(--accent_light);text-decoration:unset}.lp{height:14px;display:inline-block;position:relative;top:2px;fill:var(--fg_nav)}.lp:hover{fill:var(--accent_light)}.icon-info{margin:0 -3px}.icon-cog{font-size:15px;padding-left:0 !important}button:hover{border-color:var(--accent)}button:active{border-color:var(--accent_light)}button{background-color:var(--bg_elements);color:var(--fg_color);border:1px solid var(--accent_border);padding:3px 6px;font-size:14px;cursor:pointer;float:right}input[type=text]:hover,input[type=date]:hover,input[type=number]:hover,select:hover{border-color:var(--accent)}input[type=text]:active,input[type=date]:active,input[type=number]:active,select:active{border-color:var(--accent_light)}input[type=text],input[type=date],input[type=number],select{background-color:var(--bg_elements);padding:1px 4px;color:var(--fg_color);border:1px solid var(--accent_border);border-radius:0;font-size:14px}input[type=number]{-moz-appearance:textfield}input[type=text],input[type=number]{height:16px}select{height:20px;padding:0 2px;line-height:1}input[type=date]::-webkit-inner-spin-button{display:none}input[type=number]{-moz-appearance:textfield}input[type=number]::-webkit-inner-spin-button,input[type=number]::-webkit-outer-spin-button{display:none;-webkit-appearance:none;margin:0}input[type=date]::-webkit-clear-button{margin-left:17px;filter:grayscale(100%);filter:hue-rotate(120deg)}input::-webkit-calendar-picker-indicator{opacity:0}input::-webkit-datetime-edit-day-field:focus,input::-webkit-datetime-edit-month-field:focus,input::-webkit-datetime-edit-year-field:focus{background-color:var(--accent);color:var(--fg_color);outline:none}.date-range .date-input{display:inline-block;position:relative}.date-range .icon-container{pointer-events:none;position:absolute;top:2px;right:5px}.date-range .search-title{margin:0 2px}.icon-button button{color:var(--accent);text-decoration:none;background:none;border:none;float:none;padding:unset;padding-left:4px}.icon-button button:hover{color:var(--accent_light)}.checkbox{position:absolute;top:1px;right:0;height:17px;width:17px;background-color:var(--bg_elements);border:1px solid var(--accent_border)}.checkbox:after{content:"";position:absolute;display:none}.checkbox-container{display:block;position:relative;margin-bottom:5px;cursor:pointer;user-select:none;padding-right:22px}.checkbox-container input{position:absolute;opacity:0;cursor:pointer;height:0;width:0}.checkbox-container input:checked~.checkbox:after{display:block}.checkbox-container:hover input~.checkbox{border-color:var(--accent)}.checkbox-container:active input~.checkbox{border-color:var(--accent_light)}.checkbox-container .checkbox:after{left:2px;bottom:0;font-size:13px;font-family:fontello;content:""}.pref-group{display:inline}.preferences button{margin:6px 0 3px 0}.preferences label{padding-right:150px}.preferences select{position:absolute;top:0;right:0;display:block;-moz-appearance:none;-webkit-appearance:none;appearance:none}.preferences input[type=text],.preferences input[type=number]{position:absolute;right:0;max-width:140px}.preferences .pref-group{display:block}.preferences .pref-input{position:relative;margin-bottom:6px}.preferences .pref-reset{float:left}.preferences .prefs-code{background-color:var(--bg_elements);border:1px solid var(--accent_border);color:var(--fg_color);font-size:13px;padding:6px 8px;margin:4px 0;word-break:break-all;white-space:pre-wrap;user-select:all}.timeline-container{max-width:600px;margin:0 auto;float:none;border-radius:0;position:relative;width:100%}.timeline>div:not(:first-child){border-top:1px solid var(--border_grey)}.timeline-header{width:100%;background-color:var(--bg_panel);text-align:center;padding:8px;display:block;font-weight:bold;margin-bottom:5px;box-sizing:border-box}.timeline-header button{float:unset}.timeline-banner img{width:100%}.timeline-description{font-weight:normal}.tab{align-items:center;display:flex;flex-wrap:wrap;list-style:none;margin:0 0 5px 0;background-color:var(--bg_panel);padding:0}.tab-item{flex:1 1 0;text-align:center;margin-top:0}.tab-item a{border-bottom:.1rem solid rgba(0,0,0,0);color:var(--tab);display:block;padding:8px 0;text-decoration:none;font-weight:bold}.tab-item a:hover{text-decoration:none}.tab-item a.active{border-bottom-color:var(--tab_selected);color:var(--tab_selected)}.tab-item.active a{border-bottom-color:var(--tab_selected);color:var(--tab_selected)}.tab-item.wide{flex-grow:1.2;flex-basis:50px}.timeline-footer{background-color:var(--bg_panel);padding:6px 0}.timeline-protected{text-align:center}.timeline-protected p{margin:8px 0}.timeline-protected h2{color:var(--accent);font-size:20px;font-weight:600}.timeline-none{color:var(--accent);font-size:20px;font-weight:600;text-align:center}.timeline-end{background-color:var(--bg_panel);color:var(--accent);font-size:16px;font-weight:600;text-align:center}.show-more{background-color:var(--bg_panel);text-align:center;padding:.75em 0;display:block !important}.show-more a{background-color:var(--darkest_grey);display:inline-block;height:2em;padding:0 2em;line-height:2em}.show-more a:hover{background-color:var(--darker_grey)}.top-ref{background-color:var(--bg_color);border-top:none !important}.top-ref .icon-down{font-size:20px;display:flex;justify-content:center;text-decoration:none}.top-ref .icon-down:hover{color:var(--accent_light)}.top-ref .icon-down::before{transform:rotate(180deg) translateY(-1px)}.timeline-item{overflow-wrap:break-word;border-left-width:0;min-width:0;padding:.75em;display:flex;position:relative;background-color:var(--bg_panel)}.search-title{font-weight:bold;display:inline-block;margin-top:4px}.search-field{display:flex;flex-wrap:wrap}.search-field button{margin:0 2px 0 0;padding:0px 1px 1px 4px;height:23px;display:flex;align-items:center}.search-field .pref-input{margin:0 4px 0 0;flex-grow:1;height:23px}.search-field input[type=text],.search-field input[type=number]{height:calc(100% - 4px);width:calc(100% - 8px)}.search-field>label{display:inline;background-color:var(--bg_elements);color:var(--fg_color);border:1px solid var(--accent_border);padding:1px 1px 2px 4px;font-size:14px;cursor:pointer;margin-bottom:2px}.search-field>label:hover{border-color:var(--accent)}.search-field>label:active{border-color:var(--accent_light)}.search-field #search-panel-toggle{display:none}.search-field #search-panel-toggle:checked~.search-panel{max-height:380px}.search-field #search-panel-toggle:checked~label .icon-down:before{transform:rotate(180deg) translateY(-1px)}.search-panel{width:100%;max-height:0;overflow:hidden;transition:max-height .4s;flex-grow:1;font-weight:initial;text-align:left}.search-panel .checkbox-container{display:inline;padding-right:unset;margin-bottom:5px;margin-left:23px}.search-panel .checkbox{right:unset;left:-22px;line-height:1.6em}.search-panel .checkbox-container .checkbox:after{top:-4px}.search-row{display:flex;flex-wrap:wrap;line-height:unset}.search-row>div{flex-grow:1;flex-shrink:1}.search-row input{height:21px}.search-row .pref-input{display:block;padding-bottom:5px}.search-row .pref-input input{height:21px;margin-top:1px}.search-toggles{flex-grow:1;display:grid;grid-template-columns:repeat(5, auto);grid-column-gap:10px}@media(max-width: 820px){.profile-tabs .search-toggles{grid-template-columns:repeat(5, auto)}.profile-tabs #search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 715px){.profile-tabs .search-toggles{grid-template-columns:repeat(4, auto)}.profile-tabs #search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 700px){.profile-tabs .search-toggles{grid-template-columns:repeat(5, auto)}.profile-tabs #search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 485px){.profile-tabs .search-toggles{grid-template-columns:repeat(4, auto)}.profile-tabs #search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 410px){.profile-tabs .search-toggles{grid-template-columns:repeat(3, auto)}.profile-tabs #search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 700px){.search-toggles{grid-template-columns:repeat(5, auto)}#search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 485px){.search-toggles{grid-template-columns:repeat(4, auto)}#search-panel-toggle:checked~.search-panel{max-height:380px !important}}@media(max-width: 410px){.search-toggles{grid-template-columns:repeat(3, auto)}#search-panel-toggle:checked~.search-panel{max-height:380px !important}}body{--bg_color: #0f0f0f;--fg_color: #f8f8f2;--fg_faded: rgba(248, 248, 242, 0.8117647059);--fg_dark: #ff6c60;--fg_nav: #ff6c60;--bg_panel: #161616;--bg_elements: #121212;--bg_overlays: #1f1f1f;--bg_hover: #1a1a1a;--grey: #888889;--dark_grey: #404040;--darker_grey: #282828;--darkest_grey: #222222;--border_grey: #3e3e35;--accent: #ff6c60;--accent_light: #ffaca0;--accent_dark: #8a3731;--accent_border: rgba(255, 108, 96, 0.568627451);--play_button: #d8574d;--play_button_hover: #ff6c60;--more_replies_dots: #ad433b;--error_red: #420a05;--verified_blue: #1da1f2;--verified_business: #fac82b;--verified_government: #c1b6a4;--icon_text: #f8f8f2;--tab: #f8f8f2;--tab_selected: #ff6c60;--profile_stat: #f8f8f2;background-color:var(--bg_color);color:var(--fg_color);font-family:sans-serif,fontello;font-size:15px;line-height:1.3;margin:0}*{outline:unset;margin:0;text-decoration:none}img{dynamic-range-limit:standard}h1{display:inline}h2,h3{font-weight:normal}p{margin:14px 0}a{color:var(--accent)}a:hover{text-decoration:underline}fieldset{border:0;padding:0;margin-top:-0.6em}legend{width:100%;padding:.6em 0 .3em 0;border:0;font-size:16px;font-weight:600;border-bottom:1px solid var(--border_grey);margin-bottom:8px}.preferences .note{border-top:1px solid var(--border_grey);border-bottom:1px solid var(--border_grey);padding:6px 0 8px 0;margin-bottom:8px;margin-top:16px}.preferences .bookmark-note{margin:0;margin-bottom:10px}ul{padding-left:1.3em}.container{display:flex;flex-wrap:wrap;box-sizing:border-box;margin:auto;min-height:100vh}body.fixed-nav .container{padding-top:50px}.icon-container{display:inline}.overlay-panel{max-width:600px;width:100%;margin:0 auto;margin-top:10px;background-color:var(--bg_overlays);padding:10px 15px;align-self:start}.overlay-panel ul{margin-bottom:14px}.overlay-panel p{word-break:break-word}.verified-icon{display:inline-block;width:14px;height:14px;margin-left:2px}.verified-icon .verified-icon-circle{position:absolute;font-size:15px}.verified-icon .verified-icon-check{position:absolute;font-size:9px;margin:5px 3px}.verified-icon.blue .verified-icon-circle{color:var(--verified_blue)}.verified-icon.blue .verified-icon-check{color:var(--icon_text)}.verified-icon.business .verified-icon-circle{color:var(--verified_business)}.verified-icon.business .verified-icon-check{color:var(--bg_panel)}.verified-icon.government .verified-icon-circle{color:var(--verified_government)}.verified-icon.government .verified-icon-check{color:var(--bg_panel)}@media(max-width: 600px){.preferences-container{max-width:95vw}.nav-item,.nav-item .icon-container{font-size:16px}} diff --git a/public/css/themes/auto.css b/public/css/themes/auto.css new file mode 100644 index 0000000..e73aea2 --- /dev/null +++ b/public/css/themes/auto.css @@ -0,0 +1,2 @@ +@import "teapot.css" (prefers-color-scheme: dark); +@import "twitter.css" (prefers-color-scheme: light); diff --git a/public/css/themes/auto_(twitter).css b/public/css/themes/auto_(twitter).css new file mode 100644 index 0000000..eb94156 --- /dev/null +++ b/public/css/themes/auto_(twitter).css @@ -0,0 +1,2 @@ +@import "twitter_dark.css" (prefers-color-scheme: dark); +@import "twitter.css" (prefers-color-scheme: light); diff --git a/public/css/themes/black.css b/public/css/themes/black.css new file mode 100644 index 0000000..ba3dc45 --- /dev/null +++ b/public/css/themes/black.css @@ -0,0 +1,37 @@ +body { + --bg_color: #000000; + --fg_color: #FFFFFF; + --fg_faded: #FFFFFFD4; + --fg_dark: var(--accent); + --fg_nav: var(--accent); + + --bg_panel: #0C0C0C; + --bg_elements: #000000; + --bg_overlays: var(--bg_panel); + --bg_hover: #131313; + + --grey: #E2E2E2; + --dark_grey: #4E4E4E; + --darker_grey: #272727; + --darkest_grey: #212121; + --border_grey: #7D7D7D; + + --accent: #FF6C60; + --accent_light: #FFACA0; + --accent_dark: #909090; + --accent_border: #FF6C6091; + + --play_button: var(--accent); + --play_button_hover: var(--accent_light); + + --more_replies_dots: #A7A7A7; + --error_red: #420A05; + + --verified_blue: #1DA1F2; + --icon_text: var(--fg_color); + + --tab: var(--fg_color); + --tab_selected: var(--accent); + + --profile_stat: var(--fg_color); +} diff --git a/public/css/themes/dracula.css b/public/css/themes/dracula.css new file mode 100644 index 0000000..6042f7d --- /dev/null +++ b/public/css/themes/dracula.css @@ -0,0 +1,41 @@ +body { + --bg_color: #282a36; + --fg_color: #f8f8f2; + --fg_faded: #818eb6; + --fg_dark: var(--fg_faded); + --fg_nav: var(--accent); + + --bg_panel: #343746; + --bg_elements: #292b36; + --bg_overlays: #44475a; + --bg_hover: #2f323f; + + --grey: var(--fg_faded); + --dark_grey: #44475a; + --darker_grey: #3d4051; + --darkest_grey: #363948; + --border_grey: #44475a; + + --accent: #bd93f9; + --accent_light: #caa9fa; + --accent_dark: var(--accent); + --accent_border: #ff79c696; + + --play_button: #ffb86c; + --play_button_hover: #ffc689; + + --more_replies_dots: #bd93f9; + --error_red: #ff5555; + + --verified_blue: var(--accent); + --icon_text: ##F8F8F2; + + --tab: #6272a4; + --tab_selected: var(--accent); + + --profile_stat: #919cbf; +} + +.search-bar > form input::placeholder{ + color: var(--fg_faded); +} \ No newline at end of file diff --git a/public/css/themes/mastodon.css b/public/css/themes/mastodon.css new file mode 100644 index 0000000..4879051 --- /dev/null +++ b/public/css/themes/mastodon.css @@ -0,0 +1,37 @@ +body { + --bg_color: #191B22; + --fg_color: #FFFFFF; + --fg_faded: #F8F8F2CF; + --fg_dark: var(--accent); + --fg_nav: var(--accent); + + --bg_panel: #282C37; + --bg_elements: #22252F; + --bg_overlays: var(--bg_panel); + --bg_hover: var(--bg_elements); + + --grey: #8595AB; + --dark_grey: #393F4F; + --darker_grey: #1C1E23; + --darkest_grey: #1F2125; + --border_grey: #393F4F; + + --accent: #9BAEC8; + --accent_light: #CDDEF5; + --accent_dark: #748294; + --accent_border: var(--accent_dark); + + --play_button: var(--accent); + --play_button_hover: var(--accent_light); + + --more_replies_dots: #7F8DA0; + --error_red: #420A05; + + --verified_blue: #1DA1F2; + --icon_text: var(--fg_color); + + --tab: var(--accent); + --tab_selected: #D9E1E8; + + --profile_stat: var(--fg_color); +} diff --git a/public/css/themes/pleroma.css b/public/css/themes/pleroma.css new file mode 100644 index 0000000..bac68a7 --- /dev/null +++ b/public/css/themes/pleroma.css @@ -0,0 +1,37 @@ +body { + --bg_color: #0A1117; + --fg_color: #B9B9BA; + --fg_faded: #B9B9BAFA; + --fg_dark: var(--accent); + --fg_nav: var(--accent); + + --bg_panel: #121A24; + --bg_elements: #182230; + --bg_overlays: var(--bg_elements); + --bg_hover: #1B2735; + + --grey: #666A6F; + --dark_grey: #42413D; + --darker_grey: #293442; + --darkest_grey: #202935; + --border_grey: #1C2737; + + --accent: #D8A070; + --accent_light: #DEB897; + --accent_dark: #6D533C; + --accent_border: #947050; + + --play_button: var(--accent); + --play_button_hover: var(--accent_light); + + --more_replies_dots: #886446; + --error_red: #420A05; + + --verified_blue: #1DA1F2; + --icon_text: #F8F8F8; + + --tab: var(--fg_color); + --tab_selected: var(--accent); + + --profile_stat: var(--fg_color); +} diff --git a/public/css/themes/teapot.css b/public/css/themes/teapot.css new file mode 100644 index 0000000..a44b29c --- /dev/null +++ b/public/css/themes/teapot.css @@ -0,0 +1,3 @@ +body { + /* uses default values */ +} diff --git a/public/css/themes/twitter.css b/public/css/themes/twitter.css new file mode 100644 index 0000000..e5161cd --- /dev/null +++ b/public/css/themes/twitter.css @@ -0,0 +1,37 @@ +body { + --bg_color: #E6ECF0; + --fg_color: #0F0F0F; + --fg_faded: #657786; + --fg_dark: var(--fg_faded); + --fg_nav: var(--accent); + + --bg_panel: #FFFFFF; + --bg_elements: #FDFDFD; + --bg_overlays: #FFFFFF; + --bg_hover: #F5F8FA; + + --grey: var(--fg_faded); + --dark_grey: #D6D6D6; + --darker_grey: #CECECE; + --darkest_grey: #ECECEC; + --border_grey: #E6ECF0; + + --accent: #1DA1F2; + --accent_light: #A0EDFF; + --accent_dark: var(--accent); + --accent_border: #1DA1F296; + + --play_button: #D84D4D; + --play_button_hover: #FF6C60; + + --more_replies_dots: #0199F7; + --error_red: #FF7266; + + --verified_blue: var(--accent); + --icon_text: #F8F8F2; + + --tab: var(--accent); + --tab_selected: #000000; + + --profile_stat: var(--fg_dark); +} diff --git a/public/css/themes/twitter_dark.css b/public/css/themes/twitter_dark.css new file mode 100644 index 0000000..3895262 --- /dev/null +++ b/public/css/themes/twitter_dark.css @@ -0,0 +1,37 @@ +body { + --bg_color: #101821; + --fg_color: #FFFFFF; + --fg_faded: #8899A6; + --fg_dark: var(--fg_faded); + --fg_nav: var(--accent); + + --bg_panel: #15202B; + --bg_elements: var(--bg_panel); + --bg_overlays: var(--bg_panel); + --bg_hover: #192734; + + --grey: var(--fg_faded); + --dark_grey: #38444D; + --darker_grey: #2A343C; + --darkest_grey:#1B2835; + --border_grey: #38444D; + + --accent: #1B95E0; + --accent_light: #80CEFF; + --accent_dark: #2B608A; + --accent_border: #1B95E096; + + --play_button: var(--accent); + --play_button_hover: var(--accent_light); + + --more_replies_dots: #39719C; + --error_red: #FF7266; + + --verified_blue: var(--accent); + --icon_text: var(--fg_color); + + --tab: var(--grey); + --tab_selected: var(--accent); + + --profile_stat: var(--fg_color); +} diff --git a/public/favicon-16x16.png b/public/favicon-16x16.png new file mode 100644 index 0000000000000000000000000000000000000000..399ce731e8877a0d6c0ee718506ef0643aecadd3 GIT binary patch literal 446 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbKJOS+@4BLl<6e(pbstUx|vage(c z!@6@aFM%AV0G|-o|2YYm$S1Q9QxKPk(`jpSplM%l&S6h{KuY#Yb8i8Z4G499hS&vw?Tnp*5K& zgah=nA2K_3e1D<;rO;hvig=KU$Zop{&1Rc9C8TxuGx{Zp9tAI~C=eD%z5BXRYh~Vl z%kwuGmZrALpNVBx?cANU9q5+us*s41pu}>8f};Gi%$!t(lFEWqh0KDIWCn(cIgdZ_ za1@4VXq@stea7=?5CgL^w_Y;0u(GiCWD#az1(ybs!zs+ln?n>%-?(z($eANDN7zp{ dcr5VJV|XPlSn|oqbSlsa22WQ%mvv4FO#rOFs(Jtb literal 0 HcmV?d00001 diff --git a/public/favicon-32x32.png b/public/favicon-32x32.png new file mode 100644 index 0000000000000000000000000000000000000000..456aa2ea6a091fac57a8f430f8a568b65865011e GIT binary patch literal 601 zcmeAS@N?(olHy`uVBq!ia0vp^3LwnE3?yBabR7dyEa{HEjtmSN`?>!lvI6;x#X;^) z4C~IxyaaMQ0(?STeKHIG=Oj>$W((Rp0GcUL666=m!1zz(@a-g)7WR;LUt}+7o9(PR zA~`E>>7ULSzCbmc1s;*b3=G^tAk28_ZrvZC!-PFu978NlmtMI!sYOA+c(Ba z|JPgb-%8%D__b}Ghw=dyyJViTg3sp!=Pf*E`TWV}S@(`*oZfHXW40`#`U#Ws$*sR0 zME!e3ib$l)b*~BHI&Djf3Yjic3r!r39yOR9eVVg!a+cvVP5Nl;?BLP1e}T4qkFLP=#oszPQ#NiqXN#hk~VcsL5f zG&D~6pFZRHG>Cy&nOiTJTUc4xd$I_#u!2j2$>9`c<;@`qr*B+2apcSqnIr6{8$1?x a=`p+#7cBYYWI7dS1%s!npUXO@geCyVq4+!i literal 0 HcmV?d00001 diff --git a/public/favicon.ico b/public/favicon.ico new file mode 100644 index 0000000000000000000000000000000000000000..03b6d903a3acecb222a201b77dcf10d68a446f58 GIT binary patch literal 15086 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 cr$h%mW#(wR$~TrZ>8sd|2dLjV8( literal 0 HcmV?d00001 diff --git a/public/fonts/fontello.svg b/public/fonts/fontello.svg new file mode 100644 index 0000000..31bd38c --- /dev/null +++ b/public/fonts/fontello.svg @@ -0,0 +1,50 @@ + + + +Copyright (C) 2025 by original authors @ fontello.com + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/public/fonts/fontello.ttf b/public/fonts/fontello.ttf new file mode 100644 index 0000000000000000000000000000000000000000..0c04c6c934c632486fc0fc56ce07b794a1fcba07 GIT binary patch literal 9376 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

OQwWw&ekz4^7h30Gor3gwD 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 literal 0 HcmV?d00001 diff --git a/public/fonts/fontello.woff2 b/public/fonts/fontello.woff2 new file mode 100644 index 0000000000000000000000000000000000000000..d2c246d1aa701c85f3438d4d3baa7e3a245aff90 GIT binary patch literal 4896 zcmV+*6W{E2Pew8T0RR91023ep4*&oF03@IQ020Um0RR9100000000000000000000 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 S*`!;CpE=t5KRzi04o(09?GiZv literal 0 HcmV?d00001 diff --git a/public/js/hls.min.js b/public/js/hls.min.js new file mode 100644 index 0000000..b9098c5 --- /dev/null +++ b/public/js/hls.min.js @@ -0,0 +1,5 @@ +// @license http://www.apache.org/licenses/LICENSE-2.0 Apache-2.0 +// @source https://github.com/video-dev/hls.js +// @version v1.5.1 +!function t(e){var r,i;r=this,i=function(){"use strict";function r(t,e){var r=Object.keys(t);if(Object.getOwnPropertySymbols){var i=Object.getOwnPropertySymbols(t);e&&(i=i.filter((function(e){return Object.getOwnPropertyDescriptor(t,e).enumerable}))),r.push.apply(r,i)}return r}function i(t){for(var e=1;et.length)&&(e=t.length);for(var r=0,i=new Array(e);r=t.length?{done:!0}:{done:!1,value:t[i++]}}}throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.")}function v(t){return t&&t.__esModule&&Object.prototype.hasOwnProperty.call(t,"default")?t.default:t}var m={exports:{}};!function(t,e){var r,i,n,a,s;r=/^(?=((?:[a-zA-Z0-9+\-.]+:)?))\1(?=((?:\/\/[^\/?#]*)?))\2(?=((?:(?:[^?#\/]*\/)*[^;?#\/]*)?))\3((?:;[^?#]*)?)(\?[^#]*)?(#[^]*)?$/,i=/^(?=([^\/?#]*))\1([^]*)$/,n=/(?:\/|^)\.(?=\/)/g,a=/(?:\/|^)\.\.\/(?!\.\.\/)[^\/]*(?=\/)/g,s={buildAbsoluteURL:function(t,e,r){if(r=r||{},t=t.trim(),!(e=e.trim())){if(!r.alwaysNormalize)return t;var n=s.parseURL(t);if(!n)throw new Error("Error trying to parse base URL.");return n.path=s.normalizePath(n.path),s.buildURLFromParts(n)}var a=s.parseURL(e);if(!a)throw new Error("Error trying to parse relative URL.");if(a.scheme)return r.alwaysNormalize?(a.path=s.normalizePath(a.path),s.buildURLFromParts(a)):e;var o=s.parseURL(t);if(!o)throw new Error("Error trying to parse base URL.");if(!o.netLoc&&o.path&&"/"!==o.path[0]){var l=i.exec(o.path);o.netLoc=l[1],o.path=l[2]}o.netLoc&&!o.path&&(o.path="/");var u={scheme:o.scheme,netLoc:a.netLoc,path:null,params:a.params,query:a.query,fragment:a.fragment};if(!a.netLoc&&(u.netLoc=o.netLoc,"/"!==a.path[0]))if(a.path){var h=o.path,d=h.substring(0,h.lastIndexOf("/")+1)+a.path;u.path=s.normalizePath(d)}else u.path=o.path,a.params||(u.params=o.params,a.query||(u.query=o.query));return null===u.path&&(u.path=r.alwaysNormalize?s.normalizePath(a.path):a.path),s.buildURLFromParts(u)},parseURL:function(t){var e=r.exec(t);return e?{scheme:e[1]||"",netLoc:e[2]||"",path:e[3]||"",params:e[4]||"",query:e[5]||"",fragment:e[6]||""}:null},normalizePath:function(t){for(t=t.split("").reverse().join("").replace(n,"");t.length!==(t=t.replace(a,"")).length;);return t.split("").reverse().join("")},buildURLFromParts:function(t){return t.scheme+t.netLoc+t.path+t.params+t.query+t.fragment}},t.exports=s}(m);var p=m.exports,y=Number.isFinite||function(t){return"number"==typeof t&&isFinite(t)},E=Number.isSafeInteger||function(t){return"number"==typeof t&&Math.abs(t)<=T},T=Number.MAX_SAFE_INTEGER||9007199254740991,S=function(t){return t.MEDIA_ATTACHING="hlsMediaAttaching",t.MEDIA_ATTACHED="hlsMediaAttached",t.MEDIA_DETACHING="hlsMediaDetaching",t.MEDIA_DETACHED="hlsMediaDetached",t.BUFFER_RESET="hlsBufferReset",t.BUFFER_CODECS="hlsBufferCodecs",t.BUFFER_CREATED="hlsBufferCreated",t.BUFFER_APPENDING="hlsBufferAppending",t.BUFFER_APPENDED="hlsBufferAppended",t.BUFFER_EOS="hlsBufferEos",t.BUFFER_FLUSHING="hlsBufferFlushing",t.BUFFER_FLUSHED="hlsBufferFlushed",t.MANIFEST_LOADING="hlsManifestLoading",t.MANIFEST_LOADED="hlsManifestLoaded",t.MANIFEST_PARSED="hlsManifestParsed",t.LEVEL_SWITCHING="hlsLevelSwitching",t.LEVEL_SWITCHED="hlsLevelSwitched",t.LEVEL_LOADING="hlsLevelLoading",t.LEVEL_LOADED="hlsLevelLoaded",t.LEVEL_UPDATED="hlsLevelUpdated",t.LEVEL_PTS_UPDATED="hlsLevelPtsUpdated",t.LEVELS_UPDATED="hlsLevelsUpdated",t.AUDIO_TRACKS_UPDATED="hlsAudioTracksUpdated",t.AUDIO_TRACK_SWITCHING="hlsAudioTrackSwitching",t.AUDIO_TRACK_SWITCHED="hlsAudioTrackSwitched",t.AUDIO_TRACK_LOADING="hlsAudioTrackLoading",t.AUDIO_TRACK_LOADED="hlsAudioTrackLoaded",t.SUBTITLE_TRACKS_UPDATED="hlsSubtitleTracksUpdated",t.SUBTITLE_TRACKS_CLEARED="hlsSubtitleTracksCleared",t.SUBTITLE_TRACK_SWITCH="hlsSubtitleTrackSwitch",t.SUBTITLE_TRACK_LOADING="hlsSubtitleTrackLoading",t.SUBTITLE_TRACK_LOADED="hlsSubtitleTrackLoaded",t.SUBTITLE_FRAG_PROCESSED="hlsSubtitleFragProcessed",t.CUES_PARSED="hlsCuesParsed",t.NON_NATIVE_TEXT_TRACKS_FOUND="hlsNonNativeTextTracksFound",t.INIT_PTS_FOUND="hlsInitPtsFound",t.FRAG_LOADING="hlsFragLoading",t.FRAG_LOAD_EMERGENCY_ABORTED="hlsFragLoadEmergencyAborted",t.FRAG_LOADED="hlsFragLoaded",t.FRAG_DECRYPTED="hlsFragDecrypted",t.FRAG_PARSING_INIT_SEGMENT="hlsFragParsingInitSegment",t.FRAG_PARSING_USERDATA="hlsFragParsingUserdata",t.FRAG_PARSING_METADATA="hlsFragParsingMetadata",t.FRAG_PARSED="hlsFragParsed",t.FRAG_BUFFERED="hlsFragBuffered",t.FRAG_CHANGED="hlsFragChanged",t.FPS_DROP="hlsFpsDrop",t.FPS_DROP_LEVEL_CAPPING="hlsFpsDropLevelCapping",t.MAX_AUTO_LEVEL_UPDATED="hlsMaxAutoLevelUpdated",t.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached",t.STEERING_MANIFEST_LOADED="hlsSteeringManifestLoaded",t}({}),L=function(t){return t.NETWORK_ERROR="networkError",t.MEDIA_ERROR="mediaError",t.KEY_SYSTEM_ERROR="keySystemError",t.MUX_ERROR="muxError",t.OTHER_ERROR="otherError",t}({}),A=function(t){return t.KEY_SYSTEM_NO_KEYS="keySystemNoKeys",t.KEY_SYSTEM_NO_ACCESS="keySystemNoAccess",t.KEY_SYSTEM_NO_SESSION="keySystemNoSession",t.KEY_SYSTEM_NO_CONFIGURED_LICENSE="keySystemNoConfiguredLicense",t.KEY_SYSTEM_LICENSE_REQUEST_FAILED="keySystemLicenseRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_REQUEST_FAILED="keySystemServerCertificateRequestFailed",t.KEY_SYSTEM_SERVER_CERTIFICATE_UPDATE_FAILED="keySystemServerCertificateUpdateFailed",t.KEY_SYSTEM_SESSION_UPDATE_FAILED="keySystemSessionUpdateFailed",t.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED="keySystemStatusOutputRestricted",t.KEY_SYSTEM_STATUS_INTERNAL_ERROR="keySystemStatusInternalError",t.MANIFEST_LOAD_ERROR="manifestLoadError",t.MANIFEST_LOAD_TIMEOUT="manifestLoadTimeOut",t.MANIFEST_PARSING_ERROR="manifestParsingError",t.MANIFEST_INCOMPATIBLE_CODECS_ERROR="manifestIncompatibleCodecsError",t.LEVEL_EMPTY_ERROR="levelEmptyError",t.LEVEL_LOAD_ERROR="levelLoadError",t.LEVEL_LOAD_TIMEOUT="levelLoadTimeOut",t.LEVEL_PARSING_ERROR="levelParsingError",t.LEVEL_SWITCH_ERROR="levelSwitchError",t.AUDIO_TRACK_LOAD_ERROR="audioTrackLoadError",t.AUDIO_TRACK_LOAD_TIMEOUT="audioTrackLoadTimeOut",t.SUBTITLE_LOAD_ERROR="subtitleTrackLoadError",t.SUBTITLE_TRACK_LOAD_TIMEOUT="subtitleTrackLoadTimeOut",t.FRAG_LOAD_ERROR="fragLoadError",t.FRAG_LOAD_TIMEOUT="fragLoadTimeOut",t.FRAG_DECRYPT_ERROR="fragDecryptError",t.FRAG_PARSING_ERROR="fragParsingError",t.FRAG_GAP="fragGap",t.REMUX_ALLOC_ERROR="remuxAllocError",t.KEY_LOAD_ERROR="keyLoadError",t.KEY_LOAD_TIMEOUT="keyLoadTimeOut",t.BUFFER_ADD_CODEC_ERROR="bufferAddCodecError",t.BUFFER_INCOMPATIBLE_CODECS_ERROR="bufferIncompatibleCodecsError",t.BUFFER_APPEND_ERROR="bufferAppendError",t.BUFFER_APPENDING_ERROR="bufferAppendingError",t.BUFFER_STALLED_ERROR="bufferStalledError",t.BUFFER_FULL_ERROR="bufferFullError",t.BUFFER_SEEK_OVER_HOLE="bufferSeekOverHole",t.BUFFER_NUDGE_ON_STALL="bufferNudgeOnStall",t.INTERNAL_EXCEPTION="internalException",t.INTERNAL_ABORTED="aborted",t.UNKNOWN="unknown",t}({}),R=function(){},k={trace:R,debug:R,log:R,warn:R,info:R,error:R},b=k;function D(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i"):R}(e)}))}function I(t,e){if("object"==typeof console&&!0===t||"object"==typeof t){D(t,"debug","log","info","warn","error");try{b.log('Debug logs enabled for "'+e+'" in hls.js version 1.5.1')}catch(t){b=k}}else b=k}var w=b,C=/^(\d+)x(\d+)$/,_=/(.+?)=(".*?"|.*?)(?:,|$)/g,x=function(){function t(e){"string"==typeof e&&(e=t.parseAttrList(e)),o(this,e)}var e=t.prototype;return e.decimalInteger=function(t){var e=parseInt(this[t],10);return e>Number.MAX_SAFE_INTEGER?1/0:e},e.hexadecimalInteger=function(t){if(this[t]){var e=(this[t]||"0x").slice(2);e=(1&e.length?"0":"")+e;for(var r=new Uint8Array(e.length/2),i=0;iNumber.MAX_SAFE_INTEGER?1/0:e},e.decimalFloatingPoint=function(t){return parseFloat(this[t])},e.optionalFloat=function(t,e){var r=this[t];return r?parseFloat(r):e},e.enumeratedString=function(t){return this[t]},e.bool=function(t){return"YES"===this[t]},e.decimalResolution=function(t){var e=C.exec(this[t]);if(null!==e)return{width:parseInt(e[1],10),height:parseInt(e[2],10)}},t.parseAttrList=function(t){var e,r={};for(_.lastIndex=0;null!==(e=_.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1].trim()]=i}return r},s(t,[{key:"clientAttrs",get:function(){return Object.keys(this).filter((function(t){return"X-"===t.substring(0,2)}))}}]),t}();function P(t){return"SCTE35-OUT"===t||"SCTE35-IN"===t}var F=function(){function t(t,e){if(this.attr=void 0,this._startDate=void 0,this._endDate=void 0,this._badValueForSameId=void 0,e){var r=e.attr;for(var i in r)if(Object.prototype.hasOwnProperty.call(t,i)&&t[i]!==r[i]){w.warn('DATERANGE tag attribute: "'+i+'" does not match for tags with ID: "'+t.ID+'"'),this._badValueForSameId=i;break}t=o(new x({}),r,t)}if(this.attr=t,this._startDate=new Date(t["START-DATE"]),"END-DATE"in this.attr){var n=new Date(this.attr["END-DATE"]);y(n.getTime())&&(this._endDate=n)}}return s(t,[{key:"id",get:function(){return this.attr.ID}},{key:"class",get:function(){return this.attr.CLASS}},{key:"startDate",get:function(){return this._startDate}},{key:"endDate",get:function(){if(this._endDate)return this._endDate;var t=this.duration;return null!==t?new Date(this._startDate.getTime()+1e3*t):null}},{key:"duration",get:function(){if("DURATION"in this.attr){var t=this.attr.decimalFloatingPoint("DURATION");if(y(t))return t}else if(this._endDate)return(this._endDate.getTime()-this._startDate.getTime())/1e3;return null}},{key:"plannedDuration",get:function(){return"PLANNED-DURATION"in this.attr?this.attr.decimalFloatingPoint("PLANNED-DURATION"):null}},{key:"endOnNext",get:function(){return this.attr.bool("END-ON-NEXT")}},{key:"isValid",get:function(){return!!this.id&&!this._badValueForSameId&&y(this.startDate.getTime())&&(null===this.duration||this.duration>=0)&&(!this.endOnNext||!!this.class)}}]),t}(),M=function(){this.aborted=!1,this.loaded=0,this.retry=0,this.total=0,this.chunkCount=0,this.bwEstimate=0,this.loading={start:0,first:0,end:0},this.parsing={start:0,end:0},this.buffering={start:0,first:0,end:0}},O="audio",N="video",U="audiovideo",B=function(){function t(t){var e;this._byteRange=null,this._url=null,this.baseurl=void 0,this.relurl=void 0,this.elementaryStreams=((e={})[O]=null,e[N]=null,e[U]=null,e),this.baseurl=t}return t.prototype.setByteRange=function(t,e){var r,i=t.split("@",2);r=1===i.length?(null==e?void 0:e.byteRangeEndOffset)||0:parseInt(i[1]),this._byteRange=[r,parseInt(i[0])+r]},s(t,[{key:"byteRange",get:function(){return this._byteRange?this._byteRange:[]}},{key:"byteRangeStartOffset",get:function(){return this.byteRange[0]}},{key:"byteRangeEndOffset",get:function(){return this.byteRange[1]}},{key:"url",get:function(){return!this._url&&this.baseurl&&this.relurl&&(this._url=p.buildAbsoluteURL(this.baseurl,this.relurl,{alwaysNormalize:!0})),this._url||""},set:function(t){this._url=t}}]),t}(),G=function(t){function e(e,r){var i;return(i=t.call(this,r)||this)._decryptdata=null,i.rawProgramDateTime=null,i.programDateTime=null,i.tagList=[],i.duration=0,i.sn=0,i.levelkeys=void 0,i.type=void 0,i.loader=null,i.keyLoader=null,i.level=-1,i.cc=0,i.startPTS=void 0,i.endPTS=void 0,i.startDTS=void 0,i.endDTS=void 0,i.start=0,i.deltaPTS=void 0,i.maxStartPTS=void 0,i.minEndPTS=void 0,i.stats=new M,i.data=void 0,i.bitrateTest=!1,i.title=null,i.initSegment=null,i.endList=void 0,i.gap=void 0,i.urlId=0,i.type=e,i}l(e,t);var r=e.prototype;return r.setKeyFormat=function(t){if(this.levelkeys){var e=this.levelkeys[t];e&&!this._decryptdata&&(this._decryptdata=e.getDecryptData(this.sn))}},r.abortRequests=function(){var t,e;null==(t=this.loader)||t.abort(),null==(e=this.keyLoader)||e.abort()},r.setElementaryStreamInfo=function(t,e,r,i,n,a){void 0===a&&(a=!1);var s=this.elementaryStreams,o=s[t];o?(o.startPTS=Math.min(o.startPTS,e),o.endPTS=Math.max(o.endPTS,r),o.startDTS=Math.min(o.startDTS,i),o.endDTS=Math.max(o.endDTS,n)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:n,partial:a}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[O]=null,t[N]=null,t[U]=null},s(e,[{key:"decryptdata",get:function(){if(!this.levelkeys&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkeys&&!this.levelkeys.NONE){var t=this.levelkeys.identity;if(t)this._decryptdata=t.getDecryptData(this.sn);else{var e=Object.keys(this.levelkeys);if(1===e.length)return this._decryptdata=this.levelkeys[e[0]].getDecryptData(this.sn)}}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!y(this.programDateTime))return null;var t=y(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;if(null!=(t=this._decryptdata)&&t.encrypted)return!0;if(this.levelkeys){var e=Object.keys(this.levelkeys),r=e.length;if(r>1||1===r&&this.levelkeys[e[0]].encrypted)return!0}return!1}}]),e}(B),K=function(t){function e(e,r,i,n,a){var s;(s=t.call(this,i)||this).fragOffset=0,s.duration=0,s.gap=!1,s.independent=!1,s.relurl=void 0,s.fragment=void 0,s.index=void 0,s.stats=new M,s.duration=e.decimalFloatingPoint("DURATION"),s.gap=e.bool("GAP"),s.independent=e.bool("INDEPENDENT"),s.relurl=e.enumeratedString("URI"),s.fragment=r,s.index=n;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,a),a&&(s.fragOffset=a.fragOffset+a.duration),s}return l(e,t),s(e,[{key:"start",get:function(){return this.fragment.start+this.fragOffset}},{key:"end",get:function(){return this.start+this.duration}},{key:"loaded",get:function(){var t=this.elementaryStreams;return!!(t.audio||t.video||t.audiovideo)}}]),e}(B),H=function(){function t(t){this.PTSKnown=!1,this.alignedSliding=!1,this.averagetargetduration=void 0,this.endCC=0,this.endSN=0,this.fragments=void 0,this.fragmentHint=void 0,this.partList=null,this.dateRanges=void 0,this.live=!0,this.ageHeader=0,this.advancedDateTime=void 0,this.updated=!0,this.advanced=!0,this.availabilityDelay=void 0,this.misses=0,this.startCC=0,this.startSN=0,this.startTimeOffset=null,this.targetduration=0,this.totalduration=0,this.type=null,this.url=void 0,this.m3u8="",this.version=null,this.canBlockReload=!1,this.canSkipUntil=0,this.canSkipDateRanges=!1,this.skippedSegments=0,this.recentlyRemovedDateranges=void 0,this.partHoldBack=0,this.holdBack=0,this.partTarget=0,this.preloadHint=void 0,this.renditionReports=void 0,this.tuneInGoal=0,this.deltaUpdateFailed=void 0,this.driftStartTime=0,this.driftEndTime=0,this.driftStart=0,this.driftEnd=0,this.encryptedFragments=void 0,this.playlistParsingError=null,this.variableList=null,this.hasVariableRefs=!1,this.fragments=[],this.encryptedFragments=[],this.dateRanges={},this.url=t}return t.prototype.reloaded=function(t){if(!t)return this.advanced=!0,void(this.updated=!0);var e=this.lastPartSn-t.lastPartSn,r=this.lastPartIndex-t.lastPartIndex;this.updated=this.endSN!==t.endSN||!!r||!!e||!this.live,this.advanced=this.endSN>t.endSN||e>0||0===e&&r>0,this.updated||this.advanced?this.misses=Math.floor(.6*t.misses):this.misses=t.misses+1,this.availabilityDelay=t.availabilityDelay},s(t,[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&y(this.fragments[this.fragments.length-1].programDateTime)}},{key:"levelTargetDuration",get:function(){return this.averagetargetduration||this.targetduration||10}},{key:"drift",get:function(){var t=this.driftEndTime-this.driftStartTime;return t>0?1e3*(this.driftEnd-this.driftStart)/t:1}},{key:"edge",get:function(){return this.partEnd||this.fragmentEnd}},{key:"partEnd",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!=(t=this.fragments)&&t.length?this.fragments[this.fragments.length-1].end:0}},{key:"age",get:function(){return this.advancedDateTime?Math.max(Date.now()-this.advancedDateTime,0)/1e3:0}},{key:"lastPartIndex",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!=(t=this.partList)&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}]),t}();function V(t){return Uint8Array.from(atob(t),(function(t){return t.charCodeAt(0)}))}function Y(t){var e,r,i=t.split(":"),n=null;if("data"===i[0]&&2===i.length){var a=i[1].split(";"),s=a[a.length-1].split(",");if(2===s.length){var o="base64"===s[0],l=s[1];o?(a.splice(-1,1),n=V(l)):(e=W(l).subarray(0,16),(r=new Uint8Array(16)).set(e,16-e.length),n=r)}}return n}function W(t){return Uint8Array.from(unescape(encodeURIComponent(t)),(function(t){return t.charCodeAt(0)}))}var j="undefined"!=typeof self?self:void 0,q={CLEARKEY:"org.w3.clearkey",FAIRPLAY:"com.apple.fps",PLAYREADY:"com.microsoft.playready",WIDEVINE:"com.widevine.alpha"},X="org.w3.clearkey",z="com.apple.streamingkeydelivery",Q="com.microsoft.playready",J="urn:uuid:edef8ba9-79d6-4ace-a3c8-27dcd51d21ed";function $(t){switch(t){case z:return q.FAIRPLAY;case Q:return q.PLAYREADY;case J:return q.WIDEVINE;case X:return q.CLEARKEY}}var Z="edef8ba979d64acea3c827dcd51d21ed";function tt(t){switch(t){case q.FAIRPLAY:return z;case q.PLAYREADY:return Q;case q.WIDEVINE:return J;case q.CLEARKEY:return X}}function et(t){var e=t.drmSystems,r=t.widevineLicenseUrl,i=e?[q.FAIRPLAY,q.WIDEVINE,q.PLAYREADY,q.CLEARKEY].filter((function(t){return!!e[t]})):[];return!i[q.WIDEVINE]&&r&&i.push(q.WIDEVINE),i}var rt,it=null!=j&&null!=(rt=j.navigator)&&rt.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function nt(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}var at,st=function(t,e){return e+10<=t.length&&73===t[e]&&68===t[e+1]&&51===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},ot=function(t,e){return e+10<=t.length&&51===t[e]&&68===t[e+1]&&73===t[e+2]&&t[e+3]<255&&t[e+4]<255&&t[e+6]<128&&t[e+7]<128&&t[e+8]<128&&t[e+9]<128},lt=function(t,e){for(var r=e,i=0;st(t,e);)i+=10,i+=ut(t,e+6),ot(t,e+10)&&(i+=10),e+=i;if(i>0)return t.subarray(r,r+i)},ut=function(t,e){var r=0;return r=(127&t[e])<<21,r|=(127&t[e+1])<<14,r|=(127&t[e+2])<<7,r|=127&t[e+3]},ht=function(t,e){return st(t,e)&&ut(t,e+6)+10<=t.length-e},dt=function(t){for(var e=gt(t),r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(a);break;case 12:case 13:s=t[h++],u+=String.fromCharCode((31&a)<<6|63&s);break;case 14:s=t[h++],o=t[h++],u+=String.fromCharCode((15&a)<<12|(63&s)<<6|(63&o)<<0)}}return u};function St(){if(!navigator.userAgent.includes("PlayStation 4"))return at||void 0===self.TextDecoder||(at=new self.TextDecoder("utf-8")),at}var Lt=function(t){for(var e="",r=0;r>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function _t(t,e){var r=[];if(!e.length)return r;for(var i=t.byteLength,n=0;n1?n+a:i;if(bt(t.subarray(n+4,n+8))===e[0])if(1===e.length)r.push(t.subarray(n+8,s));else{var o=_t(t.subarray(n+8,s),e.slice(1));o.length&&Rt.apply(r,o)}n=s}return r}function xt(t){var e=[],r=t[0],i=8,n=It(t,i);i+=4,i+=0===r?8:16,i+=2;var a=t.length+0,s=Dt(t,i);i+=2;for(var o=0;o>>31)return w.warn("SIDX has hierarchical references (not supported)"),null;var d=It(t,l);l+=4,e.push({referenceSize:h,subsegmentDuration:d,info:{duration:d/n,start:a,end:a+h-1}}),a+=h,i=l+=4}return{earliestPresentationTime:0,timescale:n,version:r,referencesCount:s,references:e}}function Pt(t){for(var e=[],r=_t(t,["moov","trak"]),n=0;n12){var h=4;if(3!==u[h++])break;h=Mt(u,h),h+=2;var d=u[h++];if(128&d&&(h+=2),64&d&&(h+=u[h++]),4!==u[h++])break;h=Mt(u,h);var c=u[h++];if(64!==c)break;if(n+="."+Ot(c),h+=12,5!==u[h++])break;h=Mt(u,h);var f=u[h++],g=(248&f)>>3;31===g&&(g+=1+((7&f)<<3)+((224&u[h])>>5)),n+="."+g}break;case"hvc1":case"hev1":var v=_t(r,["hvcC"])[0],m=v[1],p=["","A","B","C"][m>>6],y=31&m,E=It(v,2),T=(32&m)>>5?"H":"L",S=v[12],L=v.subarray(6,12);n+="."+p+y,n+="."+E.toString(16).toUpperCase(),n+="."+T+S;for(var A="",R=L.length;R--;){var k=L[R];(k||A)&&(A="."+k.toString(16).toUpperCase()+A)}n+=A;break;case"dvh1":case"dvhe":var b=_t(r,["dvcC"])[0],D=b[2]>>1&127,I=b[2]<<5&32|b[3]>>3&31;n+="."+Nt(D)+"."+Nt(I);break;case"vp09":var w=_t(r,["vpcC"])[0],C=w[4],_=w[5],x=w[6]>>4&15;n+="."+Nt(C)+"."+Nt(_)+"."+Nt(x);break;case"av01":var P=_t(r,["av1C"])[0],F=P[1]>>>5,M=31&P[1],O=P[2]>>>7?"H":"M",N=(64&P[2])>>6,U=(32&P[2])>>5,B=2===F&&N?U?12:10:N?10:8,G=(16&P[2])>>4,K=(8&P[2])>>3,H=(4&P[2])>>2,V=3&P[2];n+="."+F+"."+Nt(M)+O+"."+Nt(B)+"."+G+"."+K+H+V+"."+Nt(1)+"."+Nt(1)+"."+Nt(1)+".0"}return{codec:n,encrypted:a}}function Mt(t,e){for(var r=e+5;128&t[e++]&&e>1&63;return 39===r||40===r}return 6==(31&e)}function Vt(t,e,r,i){var n=Yt(t),a=0;a+=e;for(var s=0,o=0,l=0;a=n.length)break;s+=l=n[a++]}while(255===l);o=0;do{if(a>=n.length)break;o+=l=n[a++]}while(255===l);var u=n.length-a,h=a;if(ou){w.error("Malformed SEI payload. "+o+" is too small, only "+u+" bytes left to parse.");break}if(4===s){if(181===n[h++]){var d=Dt(n,h);if(h+=2,49===d){var c=It(n,h);if(h+=4,1195456820===c){var f=n[h++];if(3===f){var g=n[h++],v=64&g,m=v?2+3*(31&g):0,p=new Uint8Array(m);if(v){p[0]=g;for(var y=1;y16){for(var E=[],T=0;T<16;T++){var S=n[h++].toString(16);E.push(1==S.length?"0"+S:S),3!==T&&5!==T&&7!==T&&9!==T||E.push("-")}for(var L=o-16,A=new Uint8Array(L),R=0;R0?(a=new Uint8Array(4),e.length>0&&new DataView(a.buffer).setUint32(0,e.length,!1)):a=new Uint8Array;var l=new Uint8Array(4);return r&&r.byteLength>0&&new DataView(l.buffer).setUint32(0,r.byteLength,!1),function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>24&255,o[1]=a>>16&255,o[2]=a>>8&255,o[3]=255&a,o.set(t,4),s=0,a=8;s>8*(15-r)&255;return e}(e);return new t(this.method,this.uri,"identity",this.keyFormatVersions,r)}var i=Y(this.uri);if(i)switch(this.keyFormat){case J:this.pssh=i,i.length>=22&&(this.keyId=i.subarray(i.length-22,i.length-6));break;case Q:var n=new Uint8Array([154,4,240,121,152,64,66,134,171,146,230,91,224,136,95,149]);this.pssh=Wt(n,null,i);var a=new Uint16Array(i.buffer,i.byteOffset,i.byteLength/2),s=String.fromCharCode.apply(null,Array.from(a)),o=s.substring(s.indexOf("<"),s.length),l=(new DOMParser).parseFromString(o,"text/xml").getElementsByTagName("KID")[0];if(l){var u=l.childNodes[0]?l.childNodes[0].nodeValue:l.getAttribute("VALUE");if(u){var h=V(u).subarray(0,16);!function(t){var e=function(t,e,r){var i=t[e];t[e]=t[r],t[r]=i};e(t,0,3),e(t,1,2),e(t,4,5),e(t,6,7)}(h),this.keyId=h}}break;default:var d=i.subarray(0,16);if(16!==d.length){var c=new Uint8Array(16);c.set(d,16-d.length),d=c}this.keyId=d}if(!this.keyId||16!==this.keyId.byteLength){var f=jt[this.uri];if(!f){var g=Object.keys(jt).length%Number.MAX_SAFE_INTEGER;f=new Uint8Array(16),new DataView(f.buffer,12,4).setUint32(0,g),jt[this.uri]=f}this.keyId=f}return this},t}(),Xt=/\{\$([a-zA-Z0-9-_]+)\}/g;function zt(t){return Xt.test(t)}function Qt(t,e,r){if(null!==t.variableList||t.hasVariableRefs)for(var i=r.length;i--;){var n=r[i],a=e[n];a&&(e[n]=Jt(t,a))}}function Jt(t,e){if(null!==t.variableList||t.hasVariableRefs){var r=t.variableList;return e.replace(Xt,(function(e){var i=e.substring(2,e.length-1),n=null==r?void 0:r[i];return void 0===n?(t.playlistParsingError||(t.playlistParsingError=new Error('Missing preceding EXT-X-DEFINE tag for Variable Reference: "'+i+'"')),e):n}))}return e}function $t(t,e,r){var i,n,a=t.variableList;if(a||(t.variableList=a={}),"QUERYPARAM"in e){i=e.QUERYPARAM;try{var s=new self.URL(r).searchParams;if(!s.has(i))throw new Error('"'+i+'" does not match any query parameter in URI: "'+r+'"');n=s.get(i)}catch(e){t.playlistParsingError||(t.playlistParsingError=new Error("EXT-X-DEFINE QUERYPARAM: "+e.message))}}else i=e.NAME,n=e.VALUE;i in a?t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE duplicate Variable Name declarations: "'+i+'"')):a[i]=n||""}function Zt(t,e,r){var i=e.IMPORT;if(r&&i in r){var n=t.variableList;n||(t.variableList=n={}),n[i]=r[i]}else t.playlistParsingError||(t.playlistParsingError=new Error('EXT-X-DEFINE IMPORT attribute not found in Multivariant Playlist: "'+i+'"'))}function te(t){if(void 0===t&&(t=!0),"undefined"!=typeof self)return(t||!self.MediaSource)&&self.ManagedMediaSource||self.MediaSource||self.WebKitMediaSource}var ee={audio:{a3ds:1,"ac-3":.95,"ac-4":1,alac:.9,alaw:1,dra1:1,"dts+":1,"dts-":1,dtsc:1,dtse:1,dtsh:1,"ec-3":.9,enca:1,fLaC:.9,flac:.9,FLAC:.9,g719:1,g726:1,m4ae:1,mha1:1,mha2:1,mhm1:1,mhm2:1,mlpa:1,mp4a:1,"raw ":1,Opus:1,opus:1,samr:1,sawb:1,sawp:1,sevc:1,sqcp:1,ssmv:1,twos:1,ulaw:1},video:{avc1:1,avc2:1,avc3:1,avc4:1,avcp:1,av01:.8,drac:1,dva1:1,dvav:1,dvh1:.7,dvhe:.7,encv:1,hev1:.75,hvc1:.75,mjp2:1,mp4v:1,mvc1:1,mvc2:1,mvc3:1,mvc4:1,resv:1,rv60:1,s263:1,svc1:1,svc2:1,"vc-1":1,vp08:1,vp09:.9},text:{stpp:1,wvtt:1}};function re(t,e,r){return void 0===r&&(r=!0),!t.split(",").some((function(t){return!ie(t,e,r)}))}function ie(t,e,r){var i;void 0===r&&(r=!0);var n=te(r);return null!=(i=null==n?void 0:n.isTypeSupported(ne(t,e)))&&i}function ne(t,e){return e+'/mp4;codecs="'+t+'"'}function ae(t){if(t){var e=t.substring(0,4);return ee.video[e]}return 2}function se(t){return t.split(",").reduce((function(t,e){var r=ee.video[e];return r?(2*r+t)/(t?3:2):(ee.audio[e]+t)/(t?2:1)}),0)}var oe={},le=/flac|opus/i;function ue(t,e){return void 0===e&&(e=!0),t.replace(le,(function(t){return function(t,e){if(void 0===e&&(e=!0),oe[t])return oe[t];for(var r={flac:["flac","fLaC","FLAC"],opus:["opus","Opus"]}[t],i=0;i0&&a.length0&&X.bool("CAN-SKIP-DATERANGES"),h.partHoldBack=X.optionalFloat("PART-HOLD-BACK",0),h.holdBack=X.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var z=new x(I);h.partTarget=z.decimalFloatingPoint("PART-TARGET");break;case"PART":var Q=h.partList;Q||(Q=h.partList=[]);var J=g>0?Q[Q.length-1]:void 0,$=g++,Z=new x(I);Qt(h,Z,["BYTERANGE","URI"]);var tt=new K(Z,E,e,$,J);Q.push(tt),E.duration+=tt.duration;break;case"PRELOAD-HINT":var et=new x(I);Qt(h,et,["URI"]),h.preloadHint=et;break;case"RENDITION-REPORT":var rt=new x(I);Qt(h,rt,["URI"]),h.renditionReports=h.renditionReports||[],h.renditionReports.push(rt);break;default:w.warn("line parsed but not handled: "+s)}}}p&&!p.relurl?(d.pop(),v-=p.duration,h.partList&&(h.fragmentHint=p)):h.partList&&(Se(E,p),E.cc=m,h.fragmentHint=E,u&&Ae(E,u,h));var it=d.length,nt=d[0],at=d[it-1];if((v+=h.skippedSegments*h.targetduration)>0&&it&&at){h.averagetargetduration=v/it;var st=at.sn;h.endSN="initSegment"!==st?st:0,h.live||(at.endList=!0),nt&&(h.startCC=nt.cc)}else h.endSN=0,h.startCC=0;return h.fragmentHint&&(v+=h.fragmentHint.duration),h.totalduration=v,h.endCC=m,T>0&&function(t,e){for(var r=t[e],i=e;i--;){var n=t[i];if(!n)return;n.programDateTime=r.programDateTime-1e3*n.duration,r=n}}(d,T),h},t}();function pe(t,e,r){var i,n,a=new x(t);Qt(r,a,["KEYFORMAT","KEYFORMATVERSIONS","URI","IV","URI"]);var s=null!=(i=a.METHOD)?i:"",o=a.URI,l=a.hexadecimalInteger("IV"),u=a.KEYFORMATVERSIONS,h=null!=(n=a.KEYFORMAT)?n:"identity";o&&a.IV&&!l&&w.error("Invalid IV: "+a.IV);var d=o?me.resolve(o,e):"",c=(u||"1").split("/").map(Number).filter(Number.isFinite);return new qt(s,d,h,c,l)}function ye(t){var e=new x(t).decimalFloatingPoint("TIME-OFFSET");return y(e)?e:null}function Ee(t,e){var r=(t||"").split(/[ ,]+/).filter((function(t){return t}));["video","audio","text"].forEach((function(t){var i=r.filter((function(e){return function(t,e){var r=ee[e];return!!r&&!!r[t.slice(0,4)]}(e,t)}));i.length&&(e[t+"Codec"]=i.join(","),r=r.filter((function(t){return-1===i.indexOf(t)})))})),e.unknownCodecs=r}function Te(t,e,r){var i=e[r];i&&(t[r]=i)}function Se(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),y(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}function Le(t,e,r,i){t.relurl=e.URI,e.BYTERANGE&&t.setByteRange(e.BYTERANGE),t.level=r,t.sn="initSegment",i&&(t.levelkeys=i),t.initSegment=null}function Ae(t,e,r){t.levelkeys=e;var i=r.encryptedFragments;i.length&&i[i.length-1].levelkeys===e||!Object.keys(e).some((function(t){return e[t].isCommonEncryption}))||i.push(t)}var Re="manifest",ke="level",be="audioTrack",De="subtitleTrack",Ie="main",we="audio",Ce="subtitle";function _e(t){switch(t.type){case be:return we;case De:return Ce;default:return Ie}}function xe(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var Pe=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.variableList=null,this.hls=t,this.registerListeners()}var e=t.prototype;return e.startLoad=function(t){},e.stopLoad=function(){this.destroyInternalLoaders()},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_LOADING,this.onLevelLoading,this),t.on(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_LOADING,this.onLevelLoading,this),t.off(S.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(S.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,n=new(r||i)(e);return this.loaders[t.type]=n,n},e.getInternalLoader=function(t){return this.loaders[t.type]},e.resetInternalLoader=function(t){this.loaders[t]&&delete this.loaders[t]},e.destroyInternalLoaders=function(){for(var t in this.loaders){var e=this.loaders[t];e&&e.destroy(),this.resetInternalLoader(t)}},e.destroy=function(){this.variableList=null,this.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.variableList=null,this.load({id:null,level:0,responseType:"text",type:Re,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,n=e.pathwayId,a=e.url,s=e.deliveryDirectives;this.load({id:r,level:i,pathwayId:n,responseType:"text",type:ke,url:a,deliveryDirectives:s})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:be,url:n,deliveryDirectives:a})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,n=e.url,a=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:De,url:n,deliveryDirectives:a})},e.load=function(t){var e,r,i,n=this,a=this.hls.config,s=this.getInternalLoader(t);if(s){var l=s.context;if(l&&l.url===t.url&&l.level===t.level)return void w.trace("[playlist-loader]: playlist request ongoing");w.log("[playlist-loader]: aborting previous loader for type: "+t.type),s.abort()}if(r=t.type===Re?a.manifestLoadPolicy.default:o({},a.playlistLoadPolicy.default,{timeoutRetry:null,errorRetry:null}),s=this.createInternalLoader(t),y(null==(e=t.deliveryDirectives)?void 0:e.part)&&(t.type===ke&&null!==t.level?i=this.hls.levels[t.level].details:t.type===be&&null!==t.id?i=this.hls.audioTracks[t.id].details:t.type===De&&null!==t.id&&(i=this.hls.subtitleTracks[t.id].details),i)){var u=i.partTarget,h=i.targetduration;if(u&&h){var d=1e3*Math.max(3*u,.8*h);r=o({},r,{maxTimeToFirstByteMs:Math.min(d,r.maxTimeToFirstByteMs),maxLoadTimeMs:Math.min(d,r.maxTimeToFirstByteMs)})}}var c=r.errorRetry||r.timeoutRetry||{},f={loadPolicy:r,timeout:r.maxLoadTimeMs,maxRetry:c.maxNumRetry||0,retryDelay:c.retryDelayMs||0,maxRetryDelay:c.maxRetryDelayMs||0},g={onSuccess:function(t,e,r,i){var a=n.getInternalLoader(r);n.resetInternalLoader(r.type);var s=t.data;0===s.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),me.isMediaPlaylist(s)?n.handleTrackOrLevelPlaylist(t,e,r,i||null,a):n.handleMasterPlaylist(t,e,r,i)):n.handleManifestParsingError(t,r,new Error("no EXTM3U delimiter"),i||null,e)},onError:function(t,e,r,i){n.handleNetworkError(e,r,!1,t,i)},onTimeout:function(t,e,r){n.handleNetworkError(e,r,!0,void 0,t)}};s.load(t,f,g)},e.handleMasterPlaylist=function(t,e,r,i){var n=this.hls,a=t.data,s=xe(t,r),o=me.parseMasterPlaylist(a,s);if(o.playlistParsingError)this.handleManifestParsingError(t,r,o.playlistParsingError,i,e);else{var l=o.contentSteering,u=o.levels,h=o.sessionData,d=o.sessionKeys,c=o.startTimeOffset,f=o.variableList;this.variableList=f;var g=me.parseMasterPlaylistMedia(a,s,o),v=g.AUDIO,m=void 0===v?[]:v,p=g.SUBTITLES,y=g["CLOSED-CAPTIONS"];m.length&&(m.some((function(t){return!t.url}))||!u[0].audioCodec||u[0].attrs.AUDIO||(w.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),m.unshift({type:"main",name:"main",groupId:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new x({}),bitrate:0,url:""}))),n.trigger(S.MANIFEST_LOADED,{levels:u,audioTracks:m,subtitles:p,captions:y,contentSteering:l,url:s,stats:e,networkDetails:i,sessionData:h,sessionKeys:d,startTimeOffset:c,variableList:f})}},e.handleTrackOrLevelPlaylist=function(t,e,r,i,n){var a=this.hls,s=r.id,o=r.level,l=r.type,u=xe(t,r),h=y(o)?o:y(s)?s:0,d=_e(r),c=me.parseLevelPlaylist(t.data,u,h,d,0,this.variableList);if(l===Re){var f={attrs:new x({}),bitrate:0,details:c,name:"",url:u};a.trigger(S.MANIFEST_LOADED,{levels:[f],audioTracks:[],url:u,stats:e,networkDetails:i,sessionData:null,sessionKeys:null,contentSteering:null,startTimeOffset:null,variableList:null})}e.parsing.end=performance.now(),r.levelDetails=c,this.handlePlaylistLoaded(c,t,e,r,i,n)},e.handleManifestParsingError=function(t,e,r,i,n){this.hls.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.MANIFEST_PARSING_ERROR,fatal:e.type===Re,url:t.url,err:r,error:r,reason:r.message,response:t,context:e,networkDetails:i,stats:n})},e.handleNetworkError=function(t,e,r,n,a){void 0===r&&(r=!1);var s="A network "+(r?"timeout":"error"+(n?" (status "+n.code+")":""))+" occurred while loading "+t.type;t.type===ke?s+=": "+t.level+" id: "+t.id:t.type!==be&&t.type!==De||(s+=" id: "+t.id+' group-id: "'+t.groupId+'"');var o=new Error(s);w.warn("[playlist-loader]: "+s);var l=A.UNKNOWN,u=!1,h=this.getInternalLoader(t);switch(t.type){case Re:l=r?A.MANIFEST_LOAD_TIMEOUT:A.MANIFEST_LOAD_ERROR,u=!0;break;case ke:l=r?A.LEVEL_LOAD_TIMEOUT:A.LEVEL_LOAD_ERROR,u=!1;break;case be:l=r?A.AUDIO_TRACK_LOAD_TIMEOUT:A.AUDIO_TRACK_LOAD_ERROR,u=!1;break;case De:l=r?A.SUBTITLE_TRACK_LOAD_TIMEOUT:A.SUBTITLE_LOAD_ERROR,u=!1}h&&this.resetInternalLoader(t.type);var d={type:L.NETWORK_ERROR,details:l,fatal:u,url:t.url,loader:h,context:t,error:o,networkDetails:e,stats:a};if(n){var c=(null==e?void 0:e.url)||t.url;d.response=i({url:c,data:void 0},n)}this.hls.trigger(S.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i,n,a){var s=this.hls,o=i.type,l=i.level,u=i.id,h=i.groupId,d=i.deliveryDirectives,c=xe(e,i),f=_e(i),g="number"==typeof i.level&&f===Ie?l:void 0;if(t.fragments.length){t.targetduration||(t.playlistParsingError=new Error("Missing Target Duration"));var v=t.playlistParsingError;if(v)s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_PARSING_ERROR,fatal:!1,url:c,error:v,reason:v.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r});else switch(t.live&&a&&(a.getCacheAge&&(t.ageHeader=a.getCacheAge()||0),a.getCacheAge&&!isNaN(t.ageHeader)||(t.ageHeader=0)),o){case Re:case ke:s.trigger(S.LEVEL_LOADED,{details:t,level:g||0,id:u||0,stats:r,networkDetails:n,deliveryDirectives:d});break;case be:s.trigger(S.AUDIO_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d});break;case De:s.trigger(S.SUBTITLE_TRACK_LOADED,{details:t,id:u||0,groupId:h||"",stats:r,networkDetails:n,deliveryDirectives:d})}}else{var m=new Error("No Segments found in Playlist");s.trigger(S.ERROR,{type:L.NETWORK_ERROR,details:A.LEVEL_EMPTY_ERROR,fatal:!1,url:c,error:m,reason:m.message,response:e,context:i,level:g,parent:f,networkDetails:n,stats:r})}},t}();function Fe(t,e){var r;try{r=new Event("addtrack")}catch(t){(r=document.createEvent("Event")).initEvent("addtrack",!1,!1)}r.track=t,e.dispatchEvent(r)}function Me(t,e){var r=t.mode;if("disabled"===r&&(t.mode="hidden"),t.cues&&!t.cues.getCueById(e.id))try{if(t.addCue(e),!t.cues.getCueById(e.id))throw new Error("addCue is failed for: "+e)}catch(r){w.debug("[texttrack-utils]: "+r);try{var i=new self.TextTrackCue(e.startTime,e.endTime,e.text);i.id=e.id,t.addCue(i)}catch(t){w.debug("[texttrack-utils]: Legacy TextTrackCue fallback failed: "+t)}}"disabled"===r&&(t.mode=r)}function Oe(t){var e=t.mode;if("disabled"===e&&(t.mode="hidden"),t.cues)for(var r=t.cues.length;r--;)t.removeCue(t.cues[r]);"disabled"===e&&(t.mode=e)}function Ne(t,e,r,i){var n=t.mode;if("disabled"===n&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],n=function(t,e){if(et[r].endTime)return-1;for(var i=0,n=r;i<=n;){var a=Math.floor((n+i)/2);if(et[a].startTime&&i-1)for(var a=n,s=t.length;a=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),s=0;sYe&&(d=Ye),d-h<=0&&(d=h+.25);for(var c=0;ce.startDate&&(!t||e.startDate.05&&this.forwardBufferLength>1){var l=Math.min(2,Math.max(1,a)),u=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(l,Math.max(1,u))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},e.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},e.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},s(t,[{key:"latency",get:function(){return this._latency||0}},{key:"maxLatency",get:function(){var t=this.config,e=this.levelDetails;return void 0!==t.liveMaxLatencyDuration?t.liveMaxLatencyDuration:e?t.liveMaxLatencyDurationCount*e.targetduration:0}},{key:"targetLatency",get:function(){var t=this.levelDetails;if(null===t)return null;var e=t.holdBack,r=t.partHoldBack,i=t.targetduration,n=this.config,a=n.liveSyncDuration,s=n.liveSyncDurationCount,o=n.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==a?a:s*i);var h=i;return u+Math.min(1*this.stallCount,h)}},{key:"liveSyncPosition",get:function(){var t=this.estimateLiveEdge(),e=this.targetLatency,r=this.levelDetails;if(null===t||null===e||null===r)return null;var i=r.edge,n=t-e-this.edgeStalled,a=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(a,n),s)}},{key:"drift",get:function(){var t=this.levelDetails;return null===t?1:t.drift}},{key:"edgeStalled",get:function(){var t=this.levelDetails;if(null===t)return 0;var e=3*(this.config.lowLatencyMode&&t.partTarget||t.targetduration);return Math.max(t.age-e,0)}},{key:"forwardBufferLength",get:function(){var t=this.media,e=this.levelDetails;if(!t||!e)return 0;var r=t.buffered.length;return(r?t.buffered.end(r-1):e.edge)-this.currentTime}}]),t}(),Xe=["NONE","TYPE-0","TYPE-1",null],ze=["SDR","PQ","HLG"],Qe="",Je="YES",$e="v2",Ze=function(){function t(t,e,r){this.msn=void 0,this.part=void 0,this.skip=void 0,this.msn=t,this.part=e,this.skip=r}return t.prototype.addDirectives=function(t){var e=new self.URL(t);return void 0!==this.msn&&e.searchParams.set("_HLS_msn",this.msn.toString()),void 0!==this.part&&e.searchParams.set("_HLS_part",this.part.toString()),this.skip&&e.searchParams.set("_HLS_skip",this.skip),e.href},t}(),tr=function(){function t(t){this._attrs=void 0,this.audioCodec=void 0,this.bitrate=void 0,this.codecSet=void 0,this.url=void 0,this.frameRate=void 0,this.height=void 0,this.id=void 0,this.name=void 0,this.videoCodec=void 0,this.width=void 0,this.details=void 0,this.fragmentError=0,this.loadError=0,this.loaded=void 0,this.realBitrate=0,this.supportedPromise=void 0,this.supportedResult=void 0,this._avgBitrate=0,this._audioGroups=void 0,this._subtitleGroups=void 0,this._urlId=0,this.url=[t.url],this._attrs=[t.attrs],this.bitrate=t.bitrate,t.details&&(this.details=t.details),this.id=t.id||0,this.name=t.name,this.width=t.width||0,this.height=t.height||0,this.frameRate=t.attrs.optionalFloat("FRAME-RATE",0),this._avgBitrate=t.attrs.decimalInteger("AVERAGE-BANDWIDTH"),this.audioCodec=t.audioCodec,this.videoCodec=t.videoCodec,this.codecSet=[t.videoCodec,t.audioCodec].filter((function(t){return!!t})).map((function(t){return t.substring(0,4)})).join(","),this.addGroupId("audio",t.attrs.AUDIO),this.addGroupId("text",t.attrs.SUBTITLES)}var e=t.prototype;return e.hasAudioGroup=function(t){return er(this._audioGroups,t)},e.hasSubtitleGroup=function(t){return er(this._subtitleGroups,t)},e.addGroupId=function(t,e){if(e)if("audio"===t){var r=this._audioGroups;r||(r=this._audioGroups=[]),-1===r.indexOf(e)&&r.push(e)}else if("text"===t){var i=this._subtitleGroups;i||(i=this._subtitleGroups=[]),-1===i.indexOf(e)&&i.push(e)}},e.addFallback=function(){},s(t,[{key:"maxBitrate",get:function(){return Math.max(this.realBitrate,this.bitrate)}},{key:"averageBitrate",get:function(){return this._avgBitrate||this.realBitrate||this.bitrate}},{key:"attrs",get:function(){return this._attrs[0]}},{key:"codecs",get:function(){return this.attrs.CODECS||""}},{key:"pathwayId",get:function(){return this.attrs["PATHWAY-ID"]||"."}},{key:"videoRange",get:function(){return this.attrs["VIDEO-RANGE"]||"SDR"}},{key:"score",get:function(){return this.attrs.optionalFloat("SCORE",0)}},{key:"uri",get:function(){return this.url[0]||""}},{key:"audioGroups",get:function(){return this._audioGroups}},{key:"subtitleGroups",get:function(){return this._subtitleGroups}},{key:"urlId",get:function(){return 0},set:function(t){}},{key:"audioGroupIds",get:function(){return this.audioGroups?[this.audioGroupId]:void 0}},{key:"textGroupIds",get:function(){return this.subtitleGroups?[this.textGroupId]:void 0}},{key:"audioGroupId",get:function(){var t;return null==(t=this.audioGroups)?void 0:t[0]}},{key:"textGroupId",get:function(){var t;return null==(t=this.subtitleGroups)?void 0:t[0]}}]),t}();function er(t,e){return!(!e||!t)&&-1!==t.indexOf(e)}function rr(t,e){var r=e.startPTS;if(y(r)){var i,n=0;e.sn>t.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else e.sn>t.sn?t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration:e.start=Math.max(t.start-e.duration,0)}function ir(t,e,r,i,n,a){i-r<=0&&(w.warn("Fragment should have a positive duration",e),i=r+e.duration,a=n+e.duration);var s=r,o=i,l=e.startPTS,u=e.endPTS;if(y(l)){var h=Math.abs(l-r);y(e.deltaPTS)?e.deltaPTS=Math.max(h,e.deltaPTS):e.deltaPTS=h,s=Math.max(r,l),r=Math.min(r,l),n=Math.min(n,e.startDTS),o=Math.min(i,u),i=Math.max(i,u),a=Math.max(a,e.endDTS)}var d=r-e.start;0!==e.start&&(e.start=r),e.duration=i-e.start,e.startPTS=r,e.maxStartPTS=s,e.startDTS=n,e.endPTS=i,e.minEndPTS=o,e.endDTS=a;var c,f=e.sn;if(!t||ft.endSN)return 0;var g=f-t.startSN,v=t.fragments;for(v[g]=e,c=g;c>0;c--)rr(v[c],v[c-1]);for(c=g;c=0;n--){var a=i[n].initSegment;if(a){r=a;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var s,l,u,h,d,c=0;if(function(t,e,r){for(var i=e.skippedSegments,n=Math.max(t.startSN,e.startSN)-e.startSN,a=(t.fragmentHint?1:0)+(i?e.endSN:Math.min(t.endSN,e.endSN))-e.startSN,s=e.startSN-t.startSN,o=e.fragmentHint?e.fragments.concat(e.fragmentHint):e.fragments,l=t.fragmentHint?t.fragments.concat(t.fragmentHint):t.fragments,u=n;u<=a;u++){var h=l[s+u],d=o[u];i&&!d&&u=i.length||sr(e,i[r].start)}function sr(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;i499)}(n)||!!r);return t.shouldRetry?t.shouldRetry(t,e,r,i,a):a}var vr=function(t,e){for(var r=0,i=t.length-1,n=null,a=null;r<=i;){var s=e(a=t[n=(r+i)/2|0]);if(s>0)r=n+1;else{if(!(s<0))return a;i=n-1}}return null};function mr(t,e,r,i){void 0===r&&(r=0),void 0===i&&(i=0);var n=null;if(t){n=e[t.sn-e[0].sn+1]||null;var a=t.endDTS-r;a>0&&a<15e-7&&(r+=15e-7)}else 0===r&&0===e[0].start&&(n=e[0]);if(n&&(!t||t.level===n.level)&&0===pr(r,i,n))return n;var s=vr(e,pr.bind(null,r,i));return!s||s===t&&n?n:s}function pr(t,e,r){if(void 0===t&&(t=0),void 0===e&&(e=0),r.start<=t&&r.start+r.duration>t)return 0;var i=Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return r.start+r.duration-i<=t?1:r.start-i>t&&r.start?-1:0}function yr(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}var Er=0,Tr=2,Sr=3,Lr=5,Ar=0,Rr=1,kr=2,br=function(){function t(t){this.hls=void 0,this.playlistError=0,this.penalizedRenditions={},this.log=void 0,this.warn=void 0,this.error=void 0,this.hls=t,this.log=w.log.bind(w,"[info]:"),this.warn=w.warn.bind(w,"[warning]:"),this.error=w.error.bind(w,"[error]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.ERROR,this.onError,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.ERROR,this.onError,this),t.off(S.ERROR,this.onErrorOut,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this))},e.destroy=function(){this.unregisterListeners(),this.hls=null,this.penalizedRenditions={}},e.startLoad=function(t){},e.stopLoad=function(){this.playlistError=0},e.getVariantLevelIndex=function(t){return(null==t?void 0:t.type)===Ie?t.level:this.hls.loadLevel},e.onManifestLoading=function(){this.playlistError=0,this.penalizedRenditions={}},e.onLevelUpdated=function(){this.playlistError=0},e.onError=function(t,e){var r,i;if(!e.fatal){var n=this.hls,a=e.context;switch(e.details){case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:return void(e.errorAction=this.getFragRetryOrSwitchAction(e));case A.FRAG_PARSING_ERROR:if(null!=(r=e.frag)&&r.gap)return void(e.errorAction={action:Er,flags:Ar});case A.FRAG_GAP:case A.FRAG_DECRYPT_ERROR:return e.errorAction=this.getFragRetryOrSwitchAction(e),void(e.errorAction.action=Tr);case A.LEVEL_EMPTY_ERROR:case A.LEVEL_PARSING_ERROR:var s,o,l=e.parent===Ie?e.level:n.loadLevel;return void(e.details===A.LEVEL_EMPTY_ERROR&&null!=(s=e.context)&&null!=(o=s.levelDetails)&&o.live?e.errorAction=this.getPlaylistRetryOrSwitchAction(e,l):(e.levelRetry=!1,e.errorAction=this.getLevelSwitchAction(e,l)));case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:return void("number"==typeof(null==a?void 0:a.level)&&(e.errorAction=this.getPlaylistRetryOrSwitchAction(e,a.level)));case A.AUDIO_TRACK_LOAD_ERROR:case A.AUDIO_TRACK_LOAD_TIMEOUT:case A.SUBTITLE_LOAD_ERROR:case A.SUBTITLE_TRACK_LOAD_TIMEOUT:if(a){var u=n.levels[n.loadLevel];if(u&&(a.type===be&&u.hasAudioGroup(a.groupId)||a.type===De&&u.hasSubtitleGroup(a.groupId)))return e.errorAction=this.getPlaylistRetryOrSwitchAction(e,n.loadLevel),e.errorAction.action=Tr,void(e.errorAction.flags=Rr)}return;case A.KEY_SYSTEM_STATUS_OUTPUT_RESTRICTED:var h=n.levels[n.loadLevel],d=null==h?void 0:h.attrs["HDCP-LEVEL"];return void(d?e.errorAction={action:Tr,flags:kr,hdcpLevel:d}:this.keySystemError(e));case A.BUFFER_ADD_CODEC_ERROR:case A.REMUX_ALLOC_ERROR:case A.BUFFER_APPEND_ERROR:return void(e.errorAction=this.getLevelSwitchAction(e,null!=(i=e.level)?i:n.loadLevel));case A.INTERNAL_EXCEPTION:case A.BUFFER_APPENDING_ERROR:case A.BUFFER_FULL_ERROR:case A.LEVEL_SWITCH_ERROR:case A.BUFFER_STALLED_ERROR:case A.BUFFER_SEEK_OVER_HOLE:case A.BUFFER_NUDGE_ON_STALL:return void(e.errorAction={action:Er,flags:Ar})}e.type===L.KEY_SYSTEM_ERROR&&this.keySystemError(e)}},e.keySystemError=function(t){var e=this.getVariantLevelIndex(t.frag);t.levelRetry=!1,t.errorAction=this.getLevelSwitchAction(t,e)},e.getPlaylistRetryOrSwitchAction=function(t,e){var r=dr(this.hls.config.playlistLoadPolicy,t),i=this.playlistError++;if(gr(r,i,hr(t),t.response))return{action:Lr,flags:Ar,retryConfig:r,retryCount:i};var n=this.getLevelSwitchAction(t,e);return r&&(n.retryConfig=r,n.retryCount=i),n},e.getFragRetryOrSwitchAction=function(t){var e=this.hls,r=this.getVariantLevelIndex(t.frag),i=e.levels[r],n=e.config,a=n.fragLoadPolicy,s=n.keyLoadPolicy,o=dr(t.details.startsWith("key")?s:a,t),l=e.levels.reduce((function(t,e){return t+e.fragmentError}),0);if(i&&(t.details!==A.FRAG_GAP&&i.fragmentError++,gr(o,l,hr(t),t.response)))return{action:Lr,flags:Ar,retryConfig:o,retryCount:l};var u=this.getLevelSwitchAction(t,r);return o&&(u.retryConfig=o,u.retryCount=l),u},e.getLevelSwitchAction=function(t,e){var r=this.hls;null==e&&(e=r.loadLevel);var i=this.hls.levels[e];if(i){var n,a,s=t.details;i.loadError++,s===A.BUFFER_APPEND_ERROR&&i.fragmentError++;var o=-1,l=r.levels,u=r.loadLevel,h=r.minAutoLevel,d=r.maxAutoLevel;r.autoLevelEnabled||(r.loadLevel=-1);for(var c,f=null==(n=t.frag)?void 0:n.type,g=(f===we&&s===A.FRAG_PARSING_ERROR||"audio"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR))&&l.some((function(t){var e=t.audioCodec;return i.audioCodec!==e})),v="video"===t.sourceBufferName&&(s===A.BUFFER_ADD_CODEC_ERROR||s===A.BUFFER_APPEND_ERROR)&&l.some((function(t){var e=t.codecSet,r=t.audioCodec;return i.codecSet!==e&&i.audioCodec===r})),m=null!=(a=t.context)?a:{},p=m.type,y=m.groupId,E=function(){var e=(T+u)%l.length;if(e!==u&&e>=h&&e<=d&&0===l[e].loadError){var r,n,a=l[e];if(s===A.FRAG_GAP&&t.frag){var c=l[e].details;if(c){var m=mr(t.frag,c.fragments,t.frag.start);if(null!=m&&m.gap)return 0}}else{if(p===be&&a.hasAudioGroup(y)||p===De&&a.hasSubtitleGroup(y))return 0;if(f===we&&null!=(r=i.audioGroups)&&r.some((function(t){return a.hasAudioGroup(t)}))||f===Ce&&null!=(n=i.subtitleGroups)&&n.some((function(t){return a.hasSubtitleGroup(t)}))||g&&i.audioCodec===a.audioCodec||!g&&i.audioCodec!==a.audioCodec||v&&i.codecSet===a.codecSet)return 0}return o=e,1}},T=l.length;T--&&(0===(c=E())||1!==c););if(o>-1&&r.loadLevel!==o)return t.levelRetry=!0,this.playlistError=0,{action:Tr,flags:Ar,nextAutoLevel:o}}return{action:Tr,flags:Rr}},e.onErrorOut=function(t,e){var r;switch(null==(r=e.errorAction)?void 0:r.action){case Er:break;case Tr:this.sendAlternateToPenaltyBox(e),e.errorAction.resolved||e.details===A.FRAG_GAP?/MediaSource readyState: ended/.test(e.error.message)&&(this.warn('MediaSource ended after "'+e.sourceBufferName+'" sourceBuffer append error. Attempting to recover from media error.'),this.hls.recoverMediaError()):e.fatal=!0}e.fatal&&this.hls.stopLoad()},e.sendAlternateToPenaltyBox=function(t){var e=this.hls,r=t.errorAction;if(r){var i=r.flags,n=r.hdcpLevel,a=r.nextAutoLevel;switch(i){case Ar:this.switchLevel(t,a);break;case kr:n&&(e.maxHdcpLevel=Xe[Xe.indexOf(n)-1],r.resolved=!0),this.warn('Restricting playback to HDCP-LEVEL of "'+e.maxHdcpLevel+'" or lower')}r.resolved||this.switchLevel(t,a)}},e.switchLevel=function(t,e){void 0!==e&&t.errorAction&&(this.warn("switching to level "+e+" after "+t.details),this.hls.nextAutoLevel=e,t.errorAction.resolved=!0,this.hls.nextLoadLevel=this.hls.nextAutoLevel)},t}(),Dr=function(){function t(t,e){this.hls=void 0,this.timer=-1,this.requestScheduled=-1,this.canLoad=!1,this.log=void 0,this.warn=void 0,this.log=w.log.bind(w,e+":"),this.warn=w.warn.bind(w,e+":"),this.hls=t}var e=t.prototype;return e.destroy=function(){this.clearTimer(),this.hls=this.log=this.warn=null},e.clearTimer=function(){-1!==this.timer&&(self.clearTimeout(this.timer),this.timer=-1)},e.startLoad=function(){this.canLoad=!0,this.requestScheduled=-1,this.loadPlaylist()},e.stopLoad=function(){this.canLoad=!1,this.clearTimer()},e.switchParams=function(t,e){var r=null==e?void 0:e.renditionReports;if(r){for(var i=-1,n=0;n=0&&h>e.partTarget&&(u+=1)}return new Ze(l,u>=0?u:void 0,Qe)}}},e.loadPlaylist=function(t){-1===this.requestScheduled&&(this.requestScheduled=self.performance.now())},e.shouldLoadPlaylist=function(t){return this.canLoad&&!!t&&!!t.url&&(!t.details||t.details.live)},e.shouldReloadPlaylist=function(t){return-1===this.timer&&-1===this.requestScheduled&&this.shouldLoadPlaylist(t)},e.playlistLoaded=function(t,e,r){var i=this,n=e.details,a=e.stats,s=self.performance.now(),o=a.loading.first?Math.max(0,s-a.loading.first):0;if(n.advancedDateTime=Date.now()-o,n.live||null!=r&&r.live){if(n.reloaded(r),r&&this.log("live playlist "+t+" "+(n.advanced?"REFRESHED "+n.lastPartSn+"-"+n.lastPartIndex:n.updated?"UPDATED":"MISSED")),r&&n.fragments.length>0&&nr(r,n),!this.canLoad||!n.live)return;var l,u=void 0,h=void 0;if(n.canBlockReload&&n.endSN&&n.advanced){var d=this.hls.config.lowLatencyMode,c=n.lastPartSn,f=n.endSN,g=n.lastPartIndex,v=c===f;-1!==g?(u=v?f+1:c,h=v?d?0:g:g+1):u=f+1;var m=n.age,p=m+n.ageHeader,y=Math.min(p-n.partTarget,1.5*n.targetduration);if(y>0){if(r&&y>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+y+" with playlist age: "+n.age),y=0;else{var E=Math.floor(y/n.targetduration);u+=E,void 0!==h&&(h+=Math.round(y%n.targetduration/n.partTarget)),this.log("CDN Tune-in age: "+n.ageHeader+"s last advanced "+m.toFixed(2)+"s goal: "+y+" skip sn "+E+" to part "+h)}n.tuneInGoal=y}if(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h),d||!v)return void this.loadPlaylist(l)}else(n.canBlockReload||n.canSkipUntil)&&(l=this.getDeliveryDirectives(n,e.deliveryDirectives,u,h));var T=this.hls.mainForwardBufferInfo,S=T?T.end-T.len:0,L=function(t,e){void 0===e&&(e=1/0);var r=1e3*t.targetduration;if(t.updated){var i=t.fragments;if(i.length&&4*r>e){var n=1e3*i[i.length-1].duration;nthis.requestScheduled+L&&(this.requestScheduled=a.loading.start),void 0!==u&&n.canBlockReload?this.requestScheduled=a.loading.first+L-(1e3*n.partTarget||1e3):-1===this.requestScheduled||this.requestScheduled+L=u.maxNumRetry)return!1;if(i&&null!=(d=t.context)&&d.deliveryDirectives)this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" without delivery-directives'),this.loadPlaylist();else{var c=cr(u,l);this.timer=self.setTimeout((function(){return e.loadPlaylist()}),c),this.warn("Retrying playlist loading "+(l+1)+"/"+u.maxNumRetry+' after "'+r+'" in '+c+"ms")}t.levelRetry=!0,n.resolved=!0}return h},t}(),Ir=function(){function t(t,e,r){void 0===e&&(e=0),void 0===r&&(r=0),this.halfLife=void 0,this.alpha_=void 0,this.estimate_=void 0,this.totalWeight_=void 0,this.halfLife=t,this.alpha_=t?Math.exp(Math.log(.5)/t):0,this.estimate_=e,this.totalWeight_=r}var e=t.prototype;return e.sample=function(t,e){var r=Math.pow(this.alpha_,t);this.estimate_=e*(1-r)+r*this.estimate_,this.totalWeight_+=t},e.getTotalWeight=function(){return this.totalWeight_},e.getEstimate=function(){if(this.alpha_){var t=1-Math.pow(this.alpha_,this.totalWeight_);if(t)return this.estimate_/t}return this.estimate_},t}(),wr=function(){function t(t,e,r,i){void 0===i&&(i=100),this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultTTFB_=void 0,this.ttfb_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new Ir(t),this.fast_=new Ir(e),this.defaultTTFB_=i,this.ttfb_=new Ir(t)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_,n=this.ttfb_;r.halfLife!==t&&(this.slow_=new Ir(t,r.getEstimate(),r.getTotalWeight())),i.halfLife!==e&&(this.fast_=new Ir(e,i.getEstimate(),i.getTotalWeight())),n.halfLife!==t&&(this.ttfb_=new Ir(t,n.getEstimate(),n.getTotalWeight()))},e.sample=function(t,e){var r=(t=Math.max(t,this.minDelayMs_))/1e3,i=8*e/r;this.fast_.sample(r,i),this.slow_.sample(r,i)},e.sampleTTFB=function(t){var e=t/1e3,r=Math.sqrt(2)*Math.exp(-Math.pow(e,2)/2);this.ttfb_.sample(r,Math.max(t,5))},e.canEstimate=function(){return this.fast_.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.getEstimateTTFB=function(){return this.ttfb_.getTotalWeight()>=this.minWeight_?this.ttfb_.getEstimate():this.defaultTTFB_},e.destroy=function(){},t}(),Cr={supported:!0,configurations:[],decodingInfoResults:[{supported:!0,powerEfficient:!0,smooth:!0}]},_r={};function xr(t,e,r){var n=t.videoCodec,a=t.audioCodec;if(!n||!a||!r)return Promise.resolve(Cr);var s={width:t.width,height:t.height,bitrate:Math.ceil(Math.max(.9*t.bitrate,t.averageBitrate)),framerate:t.frameRate||30},o=t.videoRange;"SDR"!==o&&(s.transferFunction=o.toLowerCase());var l=n.split(",").map((function(t){return{type:"media-source",video:i(i({},s),{},{contentType:ne(t,"video")})}}));return a&&t.audioGroups&&t.audioGroups.forEach((function(t){var r;t&&(null==(r=e.groups[t])||r.tracks.forEach((function(e){if(e.groupId===t){var r=e.channels||"",i=parseFloat(r);y(i)&&i>2&&l.push.apply(l,a.split(",").map((function(t){return{type:"media-source",audio:{contentType:ne(t,"audio"),channels:""+i}}})))}})))})),Promise.all(l.map((function(t){var e=function(t){var e=t.audio,r=t.video,i=r||e;if(i){var n=i.contentType.split('"')[1];if(r)return"r"+r.height+"x"+r.width+"f"+Math.ceil(r.framerate)+(r.transferFunction||"sd")+"_"+n+"_"+Math.ceil(r.bitrate/1e5);if(e)return"c"+e.channels+(e.spatialRendering?"s":"n")+"_"+n}return""}(t);return _r[e]||(_r[e]=r.decodingInfo(t))}))).then((function(t){return{supported:!t.some((function(t){return!t.supported})),configurations:l,decodingInfoResults:t}})).catch((function(t){return{supported:!1,configurations:l,decodingInfoResults:[],error:t}}))}function Pr(t,e){var r=!1,i=[];return t&&(r="SDR"!==t,i=[t]),e&&(i=e.allowedVideoRanges||ze.slice(0),i=(r=void 0!==e.preferHDR?e.preferHDR:function(){if("function"==typeof matchMedia){var t=matchMedia("(dynamic-range: high)"),e=matchMedia("bad query");if(t.media!==e.media)return!0===t.matches}return!1}())?i.filter((function(t){return"SDR"!==t})):["SDR"]),{preferHDR:r,allowedVideoRanges:i}}function Fr(t,e){w.log('[abr] start candidates with "'+t+'" ignored because '+e)}function Mr(t,e,r){if("attrs"in t){var i=e.indexOf(t);if(-1!==i)return i}for(var n=0;n-1,p=e.getBwEstimate(),E=i.levels,T=E[t.level],L=o.total||Math.max(o.loaded,Math.round(l*T.maxBitrate/8)),A=m?u-v:u;A<1&&m&&(A=Math.min(u,8*o.loaded/p));var R=m?1e3*o.loaded/A:0,k=R?(L-o.loaded)/R:8*L/p+c/1e3;if(!(k<=g)){var b,D=R?8*R:p,I=Number.POSITIVE_INFINITY;for(b=t.level-1;b>h;b--){var C=E[b].maxBitrate;if((I=e.getTimeToLoadFrag(c/1e3,D,l*C,!E[b].details))=k||I>10*l)){i.nextLoadLevel=i.nextAutoLevel=b,m?e.bwEstimator.sample(u-Math.min(c,v),o.loaded):e.bwEstimator.sampleTTFB(u);var _=E[b].bitrate;e.getBwEstimate()*e.hls.config.abrBandWidthUpFactor>_&&e.resetEstimator(_),e.clearTimer(),w.warn("[abr] Fragment "+t.sn+(r?" part "+r.index:"")+" of level "+t.level+" is loading too slowly;\n Time to underbuffer: "+g.toFixed(3)+" s\n Estimated load time for current fragment: "+k.toFixed(3)+" s\n Estimated load time for down switch fragment: "+I.toFixed(3)+" s\n TTFB estimate: "+(0|v)+" ms\n Current BW estimate: "+(y(p)?0|p:"Unknown")+" bps\n New BW estimate: "+(0|e.getBwEstimate())+" bps\n Switching to level "+b+" @ "+(0|_)+" bps"),i.trigger(S.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:r,stats:o})}}}}}}},this.hls=t,this.bwEstimator=this.initEstimator(),this.registerListeners()}var e=t.prototype;return e.resetEstimator=function(t){t&&(w.log("setting initial bwe to "+t),this.hls.config.abrEwmaDefaultEstimate=t),this.firstSelection=-1,this.bwEstimator=this.initEstimator()},e.initEstimator=function(){var t=this.hls.config;return new wr(t.abrEwmaSlowVoD,t.abrEwmaFastVoD,t.abrEwmaDefaultEstimate)},e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.LEVEL_SWITCHING,this.onLevelSwitching,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.MAX_AUTO_LEVEL_UPDATED,this.onMaxAutoLevelUpdated,this),t.off(S.ERROR,this.onError,this))},e.destroy=function(){this.unregisterListeners(),this.clearTimer(),this.hls=this._abandonRulesCheck=null,this.fragCurrent=this.partCurrent=null},e.onManifestLoading=function(t,e){this.lastLoadedFragLevel=-1,this.firstSelection=-1,this.lastLevelLoadSec=0,this.fragCurrent=this.partCurrent=null,this.onLevelsUpdated(),this.clearTimer()},e.onLevelsUpdated=function(){this.lastLoadedFragLevel>-1&&this.fragCurrent&&(this.lastLoadedFragLevel=this.fragCurrent.level),this._nextAutoLevel=-1,this.onMaxAutoLevelUpdated(),this.codecTiers=null,this.audioTracksByGroup=null},e.onMaxAutoLevelUpdated=function(){this.firstSelection=-1,this.nextAutoLevelKey=""},e.onFragLoading=function(t,e){var r,i=e.frag;this.ignoreFragment(i)||(i.bitrateTest||(this.fragCurrent=i,this.partCurrent=null!=(r=e.part)?r:null),this.clearTimer(),this.timer=self.setInterval(this._abandonRulesCheck,100))},e.onLevelSwitching=function(t,e){this.clearTimer()},e.onError=function(t,e){if(!e.fatal)switch(e.details){case A.BUFFER_ADD_CODEC_ERROR:case A.BUFFER_APPEND_ERROR:this.lastLoadedFragLevel=-1,this.firstSelection=-1;break;case A.FRAG_LOAD_TIMEOUT:var r=e.frag,i=this.fragCurrent,n=this.partCurrent;if(r&&i&&r.sn===i.sn&&r.level===i.level){var a=performance.now(),s=n?n.stats:r.stats,o=a-s.loading.start,l=s.loading.first?s.loading.first-s.loading.start:-1;if(s.loaded&&l>-1){var u=this.bwEstimator.getEstimateTTFB();this.bwEstimator.sample(o-Math.min(u,l),s.loaded)}else this.bwEstimator.sampleTTFB(o)}}},e.getTimeToLoadFrag=function(t,e,r,i){return t+r/e+(i?this.lastLevelLoadSec:0)},e.onLevelLoaded=function(t,e){var r=this.hls.config,i=e.stats.loading,n=i.end-i.start;y(n)&&(this.lastLevelLoadSec=n/1e3),e.details.live?this.bwEstimator.update(r.abrEwmaSlowLive,r.abrEwmaFastLive):this.bwEstimator.update(r.abrEwmaSlowVoD,r.abrEwmaFastVoD)},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part,n=i?i.stats:r.stats;if(r.type===Ie&&this.bwEstimator.sampleTTFB(n.loading.first-n.loading.start),!this.ignoreFragment(r)){if(this.clearTimer(),r.level===this._nextAutoLevel&&(this._nextAutoLevel=-1),this.firstSelection=-1,this.hls.config.abrMaxWithRealBitrate){var a=i?i.duration:r.duration,s=this.hls.levels[r.level],o=(s.loaded?s.loaded.bytes:0)+n.loaded,l=(s.loaded?s.loaded.duration:0)+a;s.loaded={bytes:o,duration:l},s.realBitrate=Math.round(8*o/l)}if(r.bitrateTest){var u={stats:n,frag:r,part:i,id:r.type};this.onFragBuffered(S.FRAG_BUFFERED,u),r.bitrateTest=!1}else this.lastLoadedFragLevel=r.level}},e.onFragBuffered=function(t,e){var r=e.frag,i=e.part,n=null!=i&&i.stats.loaded?i.stats:r.stats;if(!n.aborted&&!this.ignoreFragment(r)){var a=n.parsing.end-n.loading.start-Math.min(n.loading.first-n.loading.start,this.bwEstimator.getEstimateTTFB());this.bwEstimator.sample(a,n.loaded),n.bwEstimate=this.getBwEstimate(),r.bitrateTest?this.bitrateTestDelay=a/1e3:this.bitrateTestDelay=0}},e.ignoreFragment=function(t){return t.type!==Ie||"initSegment"===t.sn},e.clearTimer=function(){this.timer>-1&&(self.clearInterval(this.timer),this.timer=-1)},e.getAutoLevelKey=function(){var t;return this.getBwEstimate()+"_"+(null==(t=this.hls.mainForwardBufferInfo)?void 0:t.len)},e.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,n=r.config,a=r.minAutoLevel,s=r.media,o=e?e.duration:t?t.duration:0,l=s&&0!==s.playbackRate?Math.abs(s.playbackRate):1,u=this.getBwEstimate(),h=r.mainForwardBufferInfo,d=(h?h.len:0)/l,c=n.abrBandWidthFactor,f=n.abrBandWidthUpFactor;if(d){var g=this.findBestLevel(u,a,i,d,0,c,f);if(g>=0)return g}var v=o?Math.min(o,n.maxStarvationDelay):n.maxStarvationDelay;if(!d){var m=this.bitrateTestDelay;m&&(v=(o?Math.min(o,n.maxLoadingDelay):n.maxLoadingDelay)-m,w.info("[abr] bitrate test took "+Math.round(1e3*m)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*v)+" ms"),c=f=1)}var p=this.findBestLevel(u,a,i,d,v,c,f);if(w.info("[abr] "+(d?"rebuffering expected":"buffer is empty")+", optimal quality level "+p),p>-1)return p;var y=r.levels[a],E=r.levels[r.loadLevel];return(null==y?void 0:y.bitrate)<(null==E?void 0:E.bitrate)?a:r.loadLevel},e.getBwEstimate=function(){return this.bwEstimator.canEstimate()?this.bwEstimator.getEstimate():this.hls.config.abrEwmaDefaultEstimate},e.findBestLevel=function(t,e,r,i,n,a,s){var o,l=this,u=i+n,h=this.lastLoadedFragLevel,d=-1===h?this.hls.firstLevel:h,c=this.fragCurrent,f=this.partCurrent,g=this.hls,v=g.levels,m=g.allAudioTracks,p=g.loadLevel,E=g.config;if(1===v.length)return 0;var T,S=v[d],L=!(null==S||null==(o=S.details)||!o.live),A=-1===p||-1===h,R="SDR",k=(null==S?void 0:S.frameRate)||0,b=E.audioPreference,D=E.videoPreference,I=this.audioTracksByGroup||(this.audioTracksByGroup=function(t){return t.reduce((function(t,e){var r=t.groups[e.groupId];r||(r=t.groups[e.groupId]={tracks:[],channels:{2:0},hasDefault:!1,hasAutoSelect:!1}),r.tracks.push(e);var i=e.channels||"2";return r.channels[i]=(r.channels[i]||0)+1,r.hasDefault=r.hasDefault||e.default,r.hasAutoSelect=r.hasAutoSelect||e.autoselect,r.hasDefault&&(t.hasDefaultAudio=!0),r.hasAutoSelect&&(t.hasAutoSelectAudio=!0),t}),{hasDefaultAudio:!1,hasAutoSelectAudio:!1,groups:{}})}(m));if(A){if(-1!==this.firstSelection)return this.firstSelection;var C=this.codecTiers||(this.codecTiers=function(t,e,r,i){return t.slice(r,i+1).reduce((function(t,r){if(!r.codecSet)return t;var i=r.audioGroups,n=t[r.codecSet];n||(t[r.codecSet]=n={minBitrate:1/0,minHeight:1/0,minFramerate:1/0,maxScore:0,videoRanges:{SDR:0},channels:{2:0},hasDefaultAudio:!i,fragmentError:0}),n.minBitrate=Math.min(n.minBitrate,r.bitrate);var a=Math.min(r.height,r.width);return n.minHeight=Math.min(n.minHeight,a),n.minFramerate=Math.min(n.minFramerate,r.frameRate),n.maxScore=Math.max(n.maxScore,r.score),n.fragmentError+=r.fragmentError,n.videoRanges[r.videoRange]=(n.videoRanges[r.videoRange]||0)+1,i&&i.forEach((function(t){if(t){var r=e.groups[t];n.hasDefaultAudio=n.hasDefaultAudio||e.hasDefaultAudio?r.hasDefault:r.hasAutoSelect||!e.hasDefaultAudio&&!e.hasAutoSelectAudio,Object.keys(r.channels).forEach((function(t){n.channels[t]=(n.channels[t]||0)+r.channels[t]}))}})),t}),{})}(v,I,e,r)),_=function(t,e,r,i,n){for(var a=Object.keys(t),s=null==i?void 0:i.channels,o=null==i?void 0:i.audioCodec,l=s&&2===parseInt(s),u=!0,h=!1,d=1/0,c=1/0,f=1/0,g=0,v=[],m=Pr(e,n),p=m.preferHDR,E=m.allowedVideoRanges,T=function(){var e=t[a[S]];u=e.channels[2]>0,d=Math.min(d,e.minHeight),c=Math.min(c,e.minFramerate),f=Math.min(f,e.minBitrate);var r=E.filter((function(t){return e.videoRanges[t]>0}));r.length>0&&(h=!0,v=r)},S=a.length;S--;)T();d=y(d)?d:0,c=y(c)?c:0;var L=Math.max(1080,d),A=Math.max(30,c);return f=y(f)?f:r,r=Math.max(f,r),h||(e=void 0,v=[]),{codecSet:a.reduce((function(e,i){var n=t[i];if(i===e)return e;if(n.minBitrate>r)return Fr(i,"min bitrate of "+n.minBitrate+" > current estimate of "+r),e;if(!n.hasDefaultAudio)return Fr(i,"no renditions with default or auto-select sound found"),e;if(o&&i.indexOf(o.substring(0,4))%5!=0)return Fr(i,'audio codec preference "'+o+'" not found'),e;if(s&&!l){if(!n.channels[s])return Fr(i,"no renditions with "+s+" channel sound found (channels options: "+Object.keys(n.channels)+")"),e}else if((!o||l)&&u&&0===n.channels[2])return Fr(i,"no renditions with stereo sound found"),e;return n.minHeight>L?(Fr(i,"min resolution of "+n.minHeight+" > maximum of "+L),e):n.minFramerate>A?(Fr(i,"min framerate of "+n.minFramerate+" > maximum of "+A),e):v.some((function(t){return n.videoRanges[t]>0}))?n.maxScore=se(e)||n.fragmentError>t[e].fragmentError)?e:(g=n.maxScore,i):(Fr(i,"no variants with VIDEO-RANGE of "+JSON.stringify(v)+" found"),e)}),void 0),videoRanges:v,preferHDR:p,minFramerate:c,minBitrate:f}}(C,R,t,b,D),x=_.codecSet,P=_.videoRanges,F=_.minFramerate,M=_.minBitrate,O=_.preferHDR;T=x,R=O?P[P.length-1]:P[0],k=F,t=Math.max(t,M),w.log("[abr] picked start tier "+JSON.stringify(_))}else T=null==S?void 0:S.codecSet,R=null==S?void 0:S.videoRange;for(var N,U=f?f.duration:c?c.duration:0,B=this.bwEstimator.getEstimateTTFB()/1e3,G=[],K=function(){var e,o,c=v[H],g=H>d;if(!c)return 0;if(E.useMediaCapabilities&&!c.supportedResult&&!c.supportedPromise){var m=navigator.mediaCapabilities;"function"==typeof(null==m?void 0:m.decodingInfo)&&function(t,e,r,i,n,a){var s=t.audioCodec?t.audioGroups:null,o=null==a?void 0:a.audioCodec,l=null==a?void 0:a.channels,u=l?parseInt(l):o?1/0:2,h=null;if(null!=s&&s.length)try{h=1===s.length&&s[0]?e.groups[s[0]].channels:s.reduce((function(t,r){if(r){var i=e.groups[r];if(!i)throw new Error("Audio track group "+r+" not found");Object.keys(i.channels).forEach((function(e){t[e]=(t[e]||0)+i.channels[e]}))}return t}),{2:0})}catch(t){return!0}return void 0!==t.videoCodec&&(t.width>1920&&t.height>1088||t.height>1920&&t.width>1088||t.frameRate>Math.max(i,30)||"SDR"!==t.videoRange&&t.videoRange!==r||t.bitrate>Math.max(n,8e6))||!!h&&y(u)&&Object.keys(h).some((function(t){return parseInt(t)>u}))}(c,I,R,k,t,b)?(c.supportedPromise=xr(c,I,m),c.supportedPromise.then((function(t){c.supportedResult=t;var e=l.hls.levels,r=e.indexOf(c);t.error?w.warn('[abr] MediaCapabilities decodingInfo error: "'+t.error+'" for level '+r+" "+JSON.stringify(t)):t.supported||(w.warn("[abr] Unsupported MediaCapabilities decodingInfo result for level "+r+" "+JSON.stringify(t)),r>-1&&e.length>1&&(w.log("[abr] Removing unsupported level "+r),l.hls.removeLevel(r)))}))):c.supportedResult=Cr}if(T&&c.codecSet!==T||R&&c.videoRange!==R||g&&k>c.frameRate||!g&&k>0&&k=2*U&&0===n?v[H].averageBitrate:v[H].maxBitrate,P=l.getTimeToLoadFrag(B,D,x*_,void 0===C);if(D>=x&&(H===h||0===c.loadError&&0===c.fragmentError)&&(P<=B||!y(P)||L&&!l.bitrateTestDelay||P"+H+" adjustedbw("+Math.round(D)+")-bitrate="+Math.round(D-x)+" ttfb:"+B.toFixed(1)+" avgDuration:"+_.toFixed(1)+" maxFetchDuration:"+u.toFixed(1)+" fetchDuration:"+P.toFixed(1)+" firstSelection:"+A+" codecSet:"+T+" videoRange:"+R+" hls.loadLevel:"+p)),A&&(l.firstSelection=H),{v:H}}},H=r;H>=e;H--)if(0!==(N=K())&&N)return N.v;return-1},s(t,[{key:"firstAutoLevel",get:function(){var t=this.hls,e=t.maxAutoLevel,r=t.minAutoLevel,i=this.getBwEstimate(),n=this.hls.config.maxStarvationDelay,a=this.findBestLevel(i,r,e,0,n,1,1);if(a>-1)return a;var s=this.hls.firstLevel,o=Math.min(Math.max(s,r),e);return w.warn("[abr] Could not find best starting auto level. Defaulting to first in playlist "+s+" clamped to "+o),o}},{key:"forcedAutoLevel",get:function(){return this.nextAutoLevelKey?-1:this._nextAutoLevel}},{key:"nextAutoLevel",get:function(){var t=this.forcedAutoLevel,e=this.bwEstimator.canEstimate(),r=this.lastLoadedFragLevel>-1;if(!(-1===t||e&&r&&this.nextAutoLevelKey!==this.getAutoLevelKey()))return t;var i=e&&r?this.getNextABRAutoLevel():this.firstAutoLevel;if(-1!==t){var n=this.hls.levels;if(n.length>Math.max(t,i)&&n[t].loadError<=n[i].loadError)return t}return this._nextAutoLevel=i,this.nextAutoLevelKey=this.getAutoLevelKey(),i},set:function(t){var e=Math.max(this.hls.minAutoLevel,t);this._nextAutoLevel!=e&&(this.nextAutoLevelKey="",this._nextAutoLevel=e)}}]),t}(),Gr=function(){function t(){this._boundTick=void 0,this._tickTimer=null,this._tickInterval=null,this._tickCallCount=0,this._boundTick=this.tick.bind(this)}var e=t.prototype;return e.destroy=function(){this.onHandlerDestroying(),this.onHandlerDestroyed()},e.onHandlerDestroying=function(){this.clearNextTick(),this.clearInterval()},e.onHandlerDestroyed=function(){},e.hasInterval=function(){return!!this._tickInterval},e.hasNextTick=function(){return!!this._tickTimer},e.setInterval=function(t){return!this._tickInterval&&(this._tickCallCount=0,this._tickInterval=self.setInterval(this._boundTick,t),!0)},e.clearInterval=function(){return!!this._tickInterval&&(self.clearInterval(this._tickInterval),this._tickInterval=null,!0)},e.clearNextTick=function(){return!!this._tickTimer&&(self.clearTimeout(this._tickTimer),this._tickTimer=null,!0)},e.tick=function(){this._tickCallCount++,1===this._tickCallCount&&(this.doTick(),this._tickCallCount>1&&this.tickImmediate(),this._tickCallCount=0)},e.tickImmediate=function(){this.clearNextTick(),this._tickTimer=self.setTimeout(this._boundTick,0)},e.doTick=function(){},t}(),Kr="NOT_LOADED",Hr="APPENDING",Vr="PARTIAL",Yr="OK",Wr=function(){function t(t){this.activePartLists=Object.create(null),this.endListFragments=Object.create(null),this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hasGaps=!1,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(S.BUFFER_APPENDED,this.onBufferAppended,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(S.BUFFER_APPENDED,this.onBufferAppended,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.activePartLists=this.endListFragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){var r=this.activePartLists[e];if(r)for(var i=r.length;i--;){var n=r[i];if(!n)break;var a=n.end;if(n.start<=t&&null!==a&&t<=a)return n}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),n=i.length;n--;){var a=r[i[n]];if((null==a?void 0:a.body.type)===e&&a.buffered){var s=a.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r,i){var n=this;this.timeRanges&&(this.timeRanges[t]=e);var a=(null==i?void 0:i.fragment.sn)||-1;Object.keys(this.fragments).forEach((function(i){var s=n.fragments[i];if(s&&!(a>=s.body.sn))if(s.buffered||s.loaded){var o=s.range[t];o&&o.time.some((function(t){var r=!n.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&n.removeFragment(s.body),r}))}else s.body.type===r&&n.removeFragment(s.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,n=t.part;if(r&&"initSegment"!==i.sn){var a=qr(i),s=this.fragments[a];if(!(!s||s.buffered&&i.gap)){var o=!i.relurl;Object.keys(r).forEach((function(t){var a=i.elementaryStreams[t];if(a){var l=r[t],u=o||!0===a.partial;s.range[t]=e.getBufferedTimes(i,n,u,l)}})),s.loaded=null,Object.keys(s.range).length?(s.buffered=!0,(s.body.endList=i.endList||s.body.endList)&&(this.endListFragments[s.body.type]=s),jr(s)||this.removeParts(i.sn-1,i.type)):this.removeFragment(s.body)}}},e.removeParts=function(t,e){var r=this.activePartLists[e];r&&(this.activePartLists[e]=r.filter((function(e){return e.fragment.sn>=t})))},e.fragBuffered=function(t,e){var r=qr(t),i=this.fragments[r];!i&&e&&(i=this.fragments[r]={body:t,appendedPTS:null,loaded:null,buffered:!1,range:Object.create(null)},t.gap&&(this.hasGaps=!0)),i&&(i.loaded=null,i.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var n={time:[],partial:r},a=t.start,s=t.end,o=t.minEndPTS||s,l=t.maxStartPTS||a,u=0;u=h&&o<=d){n.time.push({startPTS:Math.max(a,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(ah){var c=Math.max(a,i.start(u)),f=Math.min(s,i.end(u));f>c&&(n.partial=!0,n.time.push({startPTS:c,endPTS:f}))}else if(s<=h)break}return n},e.getPartialFragment=function(t){var e,r,i,n=null,a=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&jr(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),a<=e&&(n=u.body,a=e)))})),n},e.isEndListAppended=function(t){var e=this.endListFragments[t];return void 0!==e&&(e.buffered||jr(e))},e.getState=function(t){var e=qr(t),r=this.fragments[e];return r?r.buffered?jr(r)?Vr:Yr:Hr:Kr},e.isTimeBuffered=function(t,e,r){for(var i,n,a=0;a=i&&e<=n)return!0;if(e<=i)return!1}return!1},e.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if("initSegment"!==r.sn&&!r.bitrateTest){var n=i?null:e,a=qr(r);this.fragments[a]={body:r,appendedPTS:null,loaded:n,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,n=e.part,a=e.timeRanges;if("initSegment"!==i.sn){var s=i.type;if(n){var o=this.activePartLists[s];o||(this.activePartLists[s]=o=[]),o.push(n)}this.timeRanges=a,Object.keys(a).forEach((function(t){var e=a[t];r.detectEvictedFragments(t,e,s,n)}))}},e.onFragBuffered=function(t,e){this.detectPartialFragments(e)},e.hasFragment=function(t){var e=qr(t);return!!this.fragments[e]},e.hasParts=function(t){var e;return!(null==(e=this.activePartLists[t])||!e.length)},e.removeFragmentsInRange=function(t,e,r,i,n){var a=this;i&&!this.hasGaps||Object.keys(this.fragments).forEach((function(s){var o=a.fragments[s];if(o){var l=o.body;l.type!==r||i&&!l.gap||l.startt&&(o.buffered||n)&&a.removeFragment(l)}}))},e.removeFragment=function(t){var e=qr(t);t.stats.loaded=0,t.clearElementaryStreamInfo();var r=this.activePartLists[t.type];if(r){var i=t.sn;this.activePartLists[t.type]=r.filter((function(t){return t.fragment.sn!==i}))}delete this.fragments[e],t.endList&&delete this.endListFragments[t.type]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.endListFragments=Object.create(null),this.activePartLists=Object.create(null),this.hasGaps=!1},t}();function jr(t){var e,r,i;return t.buffered&&(t.body.gap||(null==(e=t.range.video)?void 0:e.partial)||(null==(r=t.range.audio)?void 0:r.partial)||(null==(i=t.range.audiovideo)?void 0:i.partial))}function qr(t){return t.type+"_"+t.level+"_"+t.sn}var Xr={length:0,start:function(){return 0},end:function(){return 0}},zr=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),n=0;n=i.start(n)&&r<=i.end(n))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var n,a=t.getBuffered(e),s=[];for(n=0;ns&&(i[a-1].end=t[n].end):i.push(t[n])}else i.push(t[n])}else i=t;for(var o,l=0,u=e,h=e,d=0;d=c&&er.startCC||t&&t.cc>>8^255&m^99,t[f]=m,e[m]=f;var p=c[f],y=c[p],E=c[y],T=257*c[m]^16843008*m;i[f]=T<<24|T>>>8,n[f]=T<<16|T>>>16,a[f]=T<<8|T>>>24,s[f]=T,T=16843009*E^65537*y^257*p^16843008*f,l[m]=T<<24|T>>>8,u[m]=T<<16|T>>>16,h[m]=T<<8|T>>>24,d[m]=T,f?(f=p^c[c[c[E^p]]],g^=c[c[g]]):f=g=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;is.end){var h=a>u;(a0&&null!=a&&a.key&&a.iv&&"AES-128"===a.method){var s=self.performance.now();return r.decrypter.decrypt(new Uint8Array(n),a.key.buffer,a.iv.buffer).catch((function(e){throw i.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:e,reason:e.message,frag:t}),e})).then((function(n){var a=self.performance.now();return i.trigger(S.FRAG_DECRYPTED,{frag:t,payload:n,stats:{tstart:s,tdecrypt:a}}),e.payload=n,r.completeInitSegmentLoad(e)}))}return r.completeInitSegmentLoad(e)})).catch((function(e){r.state!==ci&&r.state!==Si&&(r.warn(e),r.resetFragmentLoading(t))}))},r.completeInitSegmentLoad=function(t){if(!this.levels)throw new Error("init load aborted, missing levels");var e=t.frag.stats;this.state=fi,t.frag.data=new Uint8Array(t.payload),e.parsing.start=e.buffering.start=self.performance.now(),e.parsing.end=e.buffering.end=self.performance.now(),this.tick()},r.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.sn!==e.sn||t.level!==e.level},r.fragBufferedComplete=function(t,e){var r,i,n,a,s=this.mediaBuffer?this.mediaBuffer:this.media;if(this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+(this.playlistType===Ie?"level":"track")+" "+t.level+" (frag:["+(null!=(r=t.startPTS)?r:NaN).toFixed(3)+"-"+(null!=(i=t.endPTS)?i:NaN).toFixed(3)+"] > buffer:"+(s?di(zr.getBuffered(s)):"(detached)")+")"),"initSegment"!==t.sn){var o;if(t.type!==Ce){var l=t.elementaryStreams;if(!Object.keys(l).some((function(t){return!!l[t]})))return void(this.state=fi)}var u=null==(o=this.levels)?void 0:o[t.level];null!=u&&u.fragmentError&&(this.log("Resetting level fragment error count of "+u.fragmentError+" on frag buffered"),u.fragmentError=0)}this.state=fi,s&&(!this.loadedmetadata&&t.type==Ie&&s.buffered.length&&(null==(n=this.fragCurrent)?void 0:n.sn)===(null==(a=this.fragPrevious)?void 0:a.sn)&&(this.loadedmetadata=!0,this.seekToStartPos()),this.tick())},r.seekToStartPos=function(){},r._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,n=t.partsLoaded,a=!n||0===n.length||n.some((function(t){return!t})),s=new Qr(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!a);e.flush(s)}},r._handleFragmentLoadProgress=function(t){},r._doFragLoad=function(t,e,r,i){var n,a=this;void 0===r&&(r=null);var s=null==e?void 0:e.details;if(!this.levels||!s)throw new Error("frag load aborted, missing level"+(s?"":" detail")+"s");var o=null;if(!t.encrypted||null!=(n=t.decryptdata)&&n.key?!t.encrypted&&s.encryptedFragments.length&&this.keyLoader.loadClear(t,s.encryptedFragments):(this.log("Loading key for "+t.sn+" of ["+s.startSN+"-"+s.endSN+"], "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level),this.state=gi,this.fragCurrent=t,o=this.keyLoader.load(t).then((function(t){if(!a.fragContextChanged(t.frag))return a.hls.trigger(S.KEY_LOADED,t),a.state===gi&&(a.state=fi),t})),this.hls.trigger(S.KEY_LOADING,{frag:t}),null===this.fragCurrent&&(o=Promise.reject(new Error("frag load aborted, context changed in KEY_LOADING")))),r=Math.max(t.start,r||0),this.config.lowLatencyMode&&"initSegment"!==t.sn){var l=s.partList;if(l&&i){r>t.end&&s.fragmentHint&&(t=s.fragmentHint);var u=this.getNextPart(l,t,r);if(u>-1){var h,d=l[u];return this.log("Loading part sn: "+t.sn+" p: "+d.index+" cc: "+t.cc+" of playlist ["+s.startSN+"-"+s.endSN+"] parts [0-"+u+"-"+(l.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=d.start+d.duration,this.state=vi,h=o?o.then((function(r){return!r||a.fragContextChanged(r.frag)?null:a.doFragPartsLoad(t,d,e,i)})).catch((function(t){return a.handleFragLoadError(t)})):this.doFragPartsLoad(t,d,e,i).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,part:d,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING parts")):h}if(!t.url||this.loadedEndOfParts(l,r))return Promise.resolve(null)}}this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(s?"of ["+s.startSN+"-"+s.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),y(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=vi;var c,f=this.config.progressive;return c=f&&o?o.then((function(e){return!e||a.fragContextChanged(null==e?void 0:e.frag)?null:a.fragmentLoader.load(t,i)})).catch((function(t){return a.handleFragLoadError(t)})):Promise.all([this.fragmentLoader.load(t,f?i:void 0),o]).then((function(t){var e=t[0];return!f&&e&&i&&i(e),e})).catch((function(t){return a.handleFragLoadError(t)})),this.hls.trigger(S.FRAG_LOADING,{frag:t,targetBufferTime:r}),null===this.fragCurrent?Promise.reject(new Error("frag load aborted, context changed in FRAG_LOADING")):c},r.doFragPartsLoad=function(t,e,r,i){var n=this;return new Promise((function(a,s){var o,l=[],u=null==(o=r.details)?void 0:o.partList;!function e(o){n.fragmentLoader.loadPart(t,o,i).then((function(i){l[o.index]=i;var s=i.part;n.hls.trigger(S.FRAG_LOADED,i);var h=or(r,t.sn,o.index+1)||lr(u,t.sn,o.index+1);if(!h)return a({frag:t,part:s,partsLoaded:l});e(h)})).catch(s)}(e)}))},r.handleFragLoadError=function(t){if("data"in t){var e=t.data;t.data&&e.details===A.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(S.ERROR,e)}else this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,err:t,error:t,fatal:!0});return null},r._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===yi){var r=e.frag,i=e.part,n=e.level,a=self.performance.now();r.stats.parsing.end=a,i&&(i.stats.parsing.end=a),this.updateLevelTiming(r,i,n,t.partial)}else this.fragCurrent||this.state===ci||this.state===Si||(this.state=fi)},r.getCurrentContext=function(t){var e=this.levels,r=this.fragCurrent,i=t.level,n=t.sn,a=t.part;if(null==e||!e[i])return this.warn("Levels object was unset while buffering fragment "+n+" of level "+i+". The current chunk will not be buffered."),null;var s=e[i],o=a>-1?or(s,n,a):null,l=o?o.fragment:function(t,e,r){if(null==t||!t.details)return null;var i=t.details,n=i.fragments[e-i.startSN];return n||((n=i.fragmentHint)&&n.sn===e?n:ea&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},r.getFwdBufferInfo=function(t,e){var r=this.getLoadPosition();return y(r)?this.getFwdBufferInfoAtPos(t,r,e):null},r.getFwdBufferInfoAtPos=function(t,e,r){var i=this.config.maxBufferHole,n=zr.bufferInfo(t,e,i);if(0===n.len&&void 0!==n.nextStart){var a=this.fragmentTracker.getBufferedFrag(e,r);if(a&&n.nextStart=r&&(e.maxMaxBufferLength/=2,this.warn("Reduce max buffer length to "+e.maxMaxBufferLength+"s"),!0)},r.getAppendedFrag=function(t,e){var r=this.fragmentTracker.getAppendedFrag(t,Ie);return r&&"fragment"in r?r.fragment:r},r.getNextFragment=function(t,e){var r=e.fragments,i=r.length;if(!i)return null;var n,a=this.config,s=r[0].start;if(e.live){var o=a.initialLiveManifestSize;if(ie},r.getNextFragmentLoopLoading=function(t,e,r,i,n){var a=t.gap,s=this.getNextFragment(this.nextLoadPosition,e);if(null===s)return s;if(t=s,a&&t&&!t.gap&&r.nextStart){var o=this.getFwdBufferInfoAtPos(this.mediaBuffer?this.mediaBuffer:this.media,r.nextStart,i);if(null!==o&&r.len+o.len>=n)return this.log('buffer full after gaps in "'+i+'" playlist starting at sn: '+t.sn),null}return t},r.mapToInitFragWhenRequired=function(t){return null==t||!t.initSegment||null!=t&&t.initSegment.data||this.bitrateTest?t:t.initSegment},r.getNextPart=function(t,e,r){for(var i=-1,n=!1,a=!0,s=0,o=t.length;s-1&&rr.start&&r.loaded},r.getInitialLiveFragment=function(t,e){var r=this.fragPrevious,i=null;if(r){if(t.hasProgramDateTime&&(this.log("Live playlist, switching playlist, load frag with same PDT: "+r.programDateTime),i=function(t,e,r){if(null===e||!Array.isArray(t)||!t.length||!y(e))return null;if(e<(t[0].programDateTime||0))return null;if(e>=(t[t.length-1].endProgramDateTime||0))return null;r=r||0;for(var i=0;i=t.startSN&&n<=t.endSN){var a=e[n-t.startSN];r.cc===a.cc&&(i=a,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return vr(t,(function(t){return t.cce?-1:0}))}(e,r.cc),i&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn))}}else{var s=this.hls.liveSyncPosition;null!==s&&(i=this.getFragmentAtPosition(s,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i},r.getFragmentAtPosition=function(t,e,r){var i,n=this.config,a=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=n.maxFragLookUpTolerance,h=r.partList,d=!!(n.lowLatencyMode&&null!=h&&h.length&&l);if(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),i=te-u?0:u):s[s.length-1]){var c=i.sn-r.startSN,f=this.fragmentTracker.getState(i);if((f===Yr||f===Vr&&i.gap)&&(a=i),a&&i.sn===a.sn&&(!d||h[0].fragment.sn>i.sn)&&a&&i.level===a.level){var g=s[c+1];i=i.sn=a-e.maxFragLookUpTolerance&&n<=s;if(null!==i&&r.duration>i&&(n"+t.startSN+" prev-sn: "+(o?o.sn:"na")+" fragments: "+i),l}return n},r.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.partTarget&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},r.setStartPosition=function(t,e){var r=this.startPosition;if(r "+(null==(n=this.fragCurrent)?void 0:n.url))}else{var a=e.details===A.FRAG_GAP;a&&this.fragmentTracker.fragBuffered(i,!0);var s=e.errorAction,o=s||{},l=o.action,u=o.retryCount,h=void 0===u?0:u,d=o.retryConfig;if(s&&l===Lr&&d){this.resetStartWhenNotLoaded(this.levelLastLoaded);var c=cr(d,h);this.warn("Fragment "+i.sn+" of "+t+" "+i.level+" errored with "+e.details+", retrying loading "+(h+1)+"/"+d.maxNumRetry+" in "+c+"ms"),s.resolved=!0,this.retryDate=self.performance.now()+c,this.state=mi}else if(d&&s){if(this.resetFragmentErrors(t),!(h.5;i&&this.reduceMaxBufferLength(r.len);var n=!i;return n&&this.warn("Buffer full error while media.currentTime is not buffered, flush "+e+" buffer"),t.frag&&(this.fragmentTracker.removeFragment(t.frag),this.nextLoadPosition=t.frag.start),this.resetLoadingState(),n}return!1},r.resetFragmentErrors=function(t){t===we&&(this.fragCurrent=null),this.loadedmetadata||(this.startFragRequested=!1),this.state!==ci&&(this.state=fi)},r.afterBufferFlushed=function(t,e,r){if(t){var i=zr.getBuffered(t);this.fragmentTracker.detectEvictedFragments(e,i,r),this.state===Ti&&this.resetLoadingState()}},r.resetLoadingState=function(){this.log("Reset loading state"),this.fragCurrent=null,this.fragPrevious=null,this.state=fi},r.resetStartWhenNotLoaded=function(t){if(!this.loadedmetadata){this.startFragRequested=!1;var e=t?t.details:null;null!=e&&e.live?(this.startPosition=-1,this.setStartPosition(e,0),this.resetLoadingState()):this.nextLoadPosition=this.startPosition}},r.resetWhenMissingContext=function(t){this.warn("The loading context changed while buffering fragment "+t.sn+" of level "+t.level+". This chunk will not be buffered."),this.removeUnbufferedFrags(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState()},r.removeUnbufferedFrags=function(t){void 0===t&&(t=0),this.fragmentTracker.removeFragmentsInRange(t,1/0,this.playlistType,!1,!0)},r.updateLevelTiming=function(t,e,r,i){var n,a=this,s=r.details;if(s){if(!Object.keys(t.elementaryStreams).reduce((function(e,n){var o=t.elementaryStreams[n];if(o){var l=o.endPTS-o.startPTS;if(l<=0)return a.warn("Could not parse fragment "+t.sn+" "+n+" duration reliably ("+l+")"),e||!1;var u=i?0:ir(s,t,o.startPTS,o.endPTS,o.startDTS,o.endDTS);return a.hls.trigger(S.LEVEL_PTS_UPDATED,{details:s,level:r,drift:u,type:n,frag:t,start:o.startPTS,end:o.endPTS}),!0}return e}),!1)&&null===(null==(n=this.transmuxer)?void 0:n.error)){var o=new Error("Found no media in fragment "+t.sn+" of level "+t.level+" resetting transmuxer to fallback to playlist timing");if(0===r.fragmentError&&(r.fragmentError++,t.gap=!0,this.fragmentTracker.removeFragment(t),this.fragmentTracker.fragBuffered(t,!0)),this.warn(o.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:o,frag:t,reason:"Found no media in msn "+t.sn+' of level "'+r.url+'"'}),!this.hls)return;this.resetTransmuxer()}this.state=Ei,this.hls.trigger(S.FRAG_PARSED,{frag:t,part:e})}else this.warn("level.details undefined")},r.resetTransmuxer=function(){this.transmuxer&&(this.transmuxer.destroy(),this.transmuxer=null)},r.recoverWorkerError=function(t){"demuxerWorker"===t.event&&(this.fragmentTracker.removeAllFragments(),this.resetTransmuxer(),this.resetStartWhenNotLoaded(this.levelLastLoaded),this.resetLoadingState())},s(e,[{key:"state",get:function(){return this._state},set:function(t){var e=this._state;e!==t&&(this._state=t,this.log(e+"->"+t))}}]),e}(Gr),ki=function(){function t(){this.chunks=[],this.dataLength=0}var e=t.prototype;return e.push=function(t){this.chunks.push(t),this.dataLength+=t.length},e.flush=function(){var t,e=this.chunks,r=this.dataLength;return e.length?(t=1===e.length?e[0]:function(t,e){for(var r=new Uint8Array(e),i=0,n=0;n0&&s.samples.push({pts:this.lastPTS,dts:this.lastPTS,data:i,type:Be,duration:Number.POSITIVE_INFINITY});n>>5}function xi(t,e){return e+1=t.length)return!1;var i=_i(t,e);if(i<=r)return!1;var n=e+i;return n===t.length||xi(t,n)}return!1}function Fi(t,e,r,i,n){if(!t.samplerate){var a=function(t,e,r,i){var n,a,s,o,l=navigator.userAgent.toLowerCase(),u=i,h=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];n=1+((192&e[r+2])>>>6);var d=(60&e[r+2])>>>2;if(!(d>h.length-1))return s=(1&e[r+2])<<2,s|=(192&e[r+3])>>>6,w.log("manifest codec:"+i+", ADTS type:"+n+", samplingIndex:"+d),/firefox/i.test(l)?d>=6?(n=5,o=new Array(4),a=d-3):(n=2,o=new Array(2),a=d):-1!==l.indexOf("android")?(n=2,o=new Array(2),a=d):(n=5,o=new Array(4),i&&(-1!==i.indexOf("mp4a.40.29")||-1!==i.indexOf("mp4a.40.5"))||!i&&d>=6?a=d-3:((i&&-1!==i.indexOf("mp4a.40.2")&&(d>=6&&1===s||/vivaldi/i.test(l))||!i&&1===s)&&(n=2,o=new Array(2)),a=d)),o[0]=n<<3,o[0]|=(14&d)>>1,o[1]|=(1&d)<<7,o[1]|=s<<3,5===n&&(o[1]|=(14&a)>>1,o[2]=(1&a)<<7,o[2]|=8,o[3]=0),{config:o,samplerate:h[d],channelCount:s,codec:"mp4a.40."+n,manifestCodec:u};var c=new Error("invalid ADTS sampling index:"+d);t.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!0,error:c,reason:c.message})}(e,r,i,n);if(!a)return;t.config=a.config,t.samplerate=a.samplerate,t.channelCount=a.channelCount,t.codec=a.codec,t.manifestCodec=a.manifestCodec,w.log("parsed codec:"+t.codec+", rate:"+a.samplerate+", channels:"+a.channelCount)}}function Mi(t){return 9216e4/t}function Oi(t,e,r,i,n){var a,s=i+n*Mi(t.samplerate),o=function(t,e){var r=Ci(t,e);if(e+r<=t.length){var i=_i(t,e)-r;if(i>0)return{headerLength:r,frameLength:i}}}(e,r);if(o){var l=o.frameLength,u=o.headerLength,h=u+l,d=Math.max(0,r+h-e.length);d?(a=new Uint8Array(h-u)).set(e.subarray(r+u,e.length),0):a=e.subarray(r+u,r+h);var c={unit:a,pts:s};return d||t.samples.push(c),{sample:c,length:h,missing:d}}var f=e.length-r;return(a=new Uint8Array(f)).set(e.subarray(r,e.length),0),{sample:{unit:a,pts:s},length:f,missing:-1}}var Ni=null,Ui=[32,64,96,128,160,192,224,256,288,320,352,384,416,448,32,48,56,64,80,96,112,128,160,192,224,256,320,384,32,40,48,56,64,80,96,112,128,160,192,224,256,320,32,48,56,64,80,96,112,128,144,160,176,192,224,256,8,16,24,32,40,48,56,64,80,96,112,128,144,160],Bi=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],Gi=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],Ki=[0,1,1,4];function Hi(t,e,r,i,n){if(!(r+24>e.length)){var a=Vi(e,r);if(a&&r+a.frameLength<=e.length){var s=i+n*(9e4*a.samplesPerFrame/a.sampleRate),o={unit:e.subarray(r,r+a.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=a.channelCount,t.samplerate=a.sampleRate,t.samples.push(o),{sample:o,length:a.frameLength,missing:0}}}}function Vi(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,n=t[e+2]>>4&15,a=t[e+2]>>2&3;if(1!==r&&0!==n&&15!==n&&3!==a){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*Ui[14*(3===r?3-i:3===i?3:4)+n-1],u=Bi[3*(3===r?0:2===r?1:2)+a],h=3===o?1:2,d=Gi[r][i],c=Ki[i],f=8*d*c,g=Math.floor(d*l/u+s)*c;if(null===Ni){var v=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);Ni=v?parseInt(v[1]):0}return!!Ni&&Ni<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:h,frameLength:g,samplesPerFrame:f}}}function Yi(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function Wi(t,e){return e+18&&109===t[r+4]&&111===t[r+5]&&111===t[r+6]&&102===t[r+7])return!0;r=i>1?r+i:e}return!1}(t)},e.demux=function(t,e){this.timeOffset=e;var r=t,i=this.videoTrack,n=this.txtTrack;if(this.config.progressive){this.remainderData&&(r=Gt(this.remainderData,t));var a=function(t){var e={valid:null,remainder:null},r=_t(t,["moof"]);if(r.length<2)return e.remainder=t,e;var i=r[r.length-1];return e.valid=nt(t,0,i.byteOffset-8),e.remainder=nt(t,i.byteOffset-8),e}(r);this.remainderData=a.remainder,i.samples=a.valid||new Uint8Array}else i.samples=r;var s=this.extractID3Track(i,e);return n.samples=Kt(e,i),{videoTrack:i,audioTrack:this.audioTrack,id3Track:s,textTrack:this.txtTrack}},e.flush=function(){var t=this.timeOffset,e=this.videoTrack,r=this.txtTrack;e.samples=this.remainderData||new Uint8Array,this.remainderData=null;var i=this.extractID3Track(e,this.timeOffset);return r.samples=Kt(t,e),{videoTrack:e,audioTrack:bi(),id3Track:i,textTrack:bi()}},e.extractID3Track=function(t,e){var r=this.id3Track;if(t.samples.length){var i=_t(t.samples,["emsg"]);i&&i.forEach((function(t){var i=function(t){var e=t[0],r="",i="",n=0,a=0,s=0,o=0,l=0,u=0;if(0===e){for(;"\0"!==bt(t.subarray(u,u+1));)r+=bt(t.subarray(u,u+1)),u+=1;for(r+=bt(t.subarray(u,u+1)),u+=1;"\0"!==bt(t.subarray(u,u+1));)i+=bt(t.subarray(u,u+1)),u+=1;i+=bt(t.subarray(u,u+1)),u+=1,n=It(t,12),a=It(t,16),o=It(t,20),l=It(t,24),u=28}else if(1===e){n=It(t,u+=4);var h=It(t,u+=4),d=It(t,u+=4);for(u+=4,s=Math.pow(2,32)*h+d,E(s)||(s=Number.MAX_SAFE_INTEGER,w.warn("Presentation time exceeds safe integer limit and wrapped to max safe integer in parsing emsg box")),o=It(t,u),l=It(t,u+=4),u+=4;"\0"!==bt(t.subarray(u,u+1));)r+=bt(t.subarray(u,u+1)),u+=1;for(r+=bt(t.subarray(u,u+1)),u+=1;"\0"!==bt(t.subarray(u,u+1));)i+=bt(t.subarray(u,u+1)),u+=1;i+=bt(t.subarray(u,u+1)),u+=1}return{schemeIdUri:r,value:i,timeScale:n,presentationTime:s,presentationTimeDelta:a,eventDuration:o,id:l,payload:t.subarray(u,t.byteLength)}}(t);if(Xi.test(i.schemeIdUri)){var n=y(i.presentationTime)?i.presentationTime/i.timeScale:e+i.presentationTimeDelta/i.timeScale,a=4294967295===i.eventDuration?Number.POSITIVE_INFINITY:i.eventDuration/i.timeScale;a<=.001&&(a=Number.POSITIVE_INFINITY);var s=i.payload;r.samples.push({data:s,len:s.byteLength,dts:n,pts:n,type:Ke,duration:a})}}))}return r},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}(),Qi=function(t,e){var r=0,i=5;e+=i;for(var n=new Uint32Array(1),a=new Uint32Array(1),s=new Uint8Array(1);i>0;){s[0]=t[e];var o=Math.min(i,8),l=8-o;a[0]=4278190080>>>24+l<>l,r=r?r<e.length)return-1;if(11!==e[r]||119!==e[r+1])return-1;var a=e[r+4]>>6;if(a>=3)return-1;var s=[48e3,44100,32e3][a],o=63&e[r+4],l=2*[64,69,96,64,70,96,80,87,120,80,88,120,96,104,144,96,105,144,112,121,168,112,122,168,128,139,192,128,140,192,160,174,240,160,175,240,192,208,288,192,209,288,224,243,336,224,244,336,256,278,384,256,279,384,320,348,480,320,349,480,384,417,576,384,418,576,448,487,672,448,488,672,512,557,768,512,558,768,640,696,960,640,697,960,768,835,1152,768,836,1152,896,975,1344,896,976,1344,1024,1114,1536,1024,1115,1536,1152,1253,1728,1152,1254,1728,1280,1393,1920,1280,1394,1920][3*o+a];if(r+l>e.length)return-1;var u=e[r+6]>>5,h=0;2===u?h+=2:(1&u&&1!==u&&(h+=2),4&u&&(h+=2));var d=(e[r+6]<<8|e[r+7])>>12-h&1,c=[2,1,2,3,3,4,4,5][u]+d,f=e[r+5]>>3,g=7&e[r+5],v=new Uint8Array([a<<6|f<<1|g>>2,(3&g)<<6|u<<3|d<<2|o>>4,o<<4&224]),m=i+n*(1536/s*9e4),p=e.subarray(r,r+l);return t.config=v,t.channelCount=c,t.samplerate=s,t.samples.push({unit:p,pts:m}),l}var Zi=function(){function t(){this.VideoSample=null}var e=t.prototype;return e.createVideoSample=function(t,e,r,i){return{key:t,frame:!1,pts:e,dts:r,units:[],debug:i,length:0}},e.getLastNalUnit=function(t){var e,r,i=this.VideoSample;if(i&&0!==i.units.length||(i=t[t.length-1]),null!=(e=i)&&e.units){var n=i.units;r=n[n.length-1]}return r},e.pushAccessUnit=function(t,e){if(t.units.length&&t.frame){if(void 0===t.pts){var r=e.samples,i=r.length;if(!i)return void e.dropped++;var n=r[i-1];t.pts=n.pts,t.dts=n.dts}e.samples.push(t)}t.debug.length&&w.log(t.pts+"/"+t.dts+":"+t.debug)},t}(),tn=function(){function t(t){this.data=void 0,this.bytesAvailable=void 0,this.word=void 0,this.bitsAvailable=void 0,this.data=t,this.bytesAvailable=t.byteLength,this.word=0,this.bitsAvailable=0}var e=t.prototype;return e.loadWord=function(){var t=this.data,e=this.bytesAvailable,r=t.byteLength-e,i=new Uint8Array(4),n=Math.min(4,e);if(0===n)throw new Error("no bytes available");i.set(t.subarray(r,r+n)),this.word=new DataView(i.buffer).getUint32(0),this.bitsAvailable=8*n,this.bytesAvailable-=n},e.skipBits=function(t){var e;t=Math.min(t,8*this.bytesAvailable+this.bitsAvailable),this.bitsAvailable>t?(this.word<<=t,this.bitsAvailable-=t):(t-=this.bitsAvailable,t-=(e=t>>3)<<3,this.bytesAvailable-=e,this.loadWord(),this.word<<=t,this.bitsAvailable-=t)},e.readBits=function(t){var e=Math.min(this.bitsAvailable,t),r=this.word>>>32-e;if(t>32&&w.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0)this.word<<=e;else{if(!(this.bytesAvailable>0))throw new Error("no bits available");this.loadWord()}return(e=t-e)>0&&this.bitsAvailable?r<>>t))return this.word<<=t,this.bitsAvailable-=t,t;return this.loadWord(),t+this.skipLZ()},e.skipUEG=function(){this.skipBits(1+this.skipLZ())},e.skipEG=function(){this.skipBits(1+this.skipLZ())},e.readUEG=function(){var t=this.skipLZ();return this.readBits(t+1)-1},e.readEG=function(){var t=this.readUEG();return 1&t?1+t>>>1:-1*(t>>>1)},e.readBoolean=function(){return 1===this.readBits(1)},e.readUByte=function(){return this.readBits(8)},e.readUShort=function(){return this.readBits(16)},e.readUInt=function(){return this.readBits(32)},e.skipScalingList=function(t){for(var e=8,r=8,i=0;i4){var f=new tn(c).readSliceType();2!==f&&4!==f&&7!==f&&9!==f||(h=!0)}h&&null!=(d=l)&&d.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.frame=!0,l.key=h;break;case 5:a=!0,null!=(o=l)&&o.frame&&!l.key&&(s.pushAccessUnit(l,t),l=s.VideoSample=null),l||(l=s.VideoSample=s.createVideoSample(!0,r.pts,r.dts,"")),l.key=!0,l.frame=!0;break;case 6:a=!0,Vt(i.data,1,r.pts,e.samples);break;case 7:var g,v;a=!0,u=!0;var m=i.data,p=new tn(m).readSPS();if(!t.sps||t.width!==p.width||t.height!==p.height||(null==(g=t.pixelRatio)?void 0:g[0])!==p.pixelRatio[0]||(null==(v=t.pixelRatio)?void 0:v[1])!==p.pixelRatio[1]){t.width=p.width,t.height=p.height,t.pixelRatio=p.pixelRatio,t.sps=[m],t.duration=n;for(var y=m.subarray(1,4),E="avc1.",T=0;T<3;T++){var S=y[T].toString(16);S.length<2&&(S="0"+S),E+=S}t.codec=E}break;case 8:a=!0,t.pps=[i.data];break;case 9:a=!0,t.audFound=!0,l&&s.pushAccessUnit(l,t),l=s.VideoSample=s.createVideoSample(!1,r.pts,r.dts,"");break;case 12:a=!0;break;default:a=!1,l&&(l.debug+="unknown NAL "+i.type+" ")}l&&a&&l.units.push(i)})),i&&l&&(this.pushAccessUnit(l,t),this.VideoSample=null)},r.parseAVCNALu=function(t,e){var r,i,n=e.byteLength,a=t.naluState||0,s=a,o=[],l=0,u=-1,h=0;for(-1===a&&(u=0,h=31&e[0],a=0,l=1);l=0){var d={data:e.subarray(u,i),type:h};o.push(d)}else{var c=this.getLastNalUnit(t.samples);c&&(s&&l<=4-s&&c.state&&(c.data=c.data.subarray(0,c.data.byteLength-s)),i>0&&(c.data=Gt(c.data,e.subarray(0,i)),c.state=0))}l=0&&a>=0){var f={data:e.subarray(u,n),type:h,state:a};o.push(f)}if(0===o.length){var g=this.getLastNalUnit(t.samples);g&&(g.data=Gt(g.data,e))}return t.naluState=a,o},e}(Zi),rn=function(){function t(t,e,r){this.keyData=void 0,this.decrypter=void 0,this.keyData=r,this.decrypter=new hi(e,{removePKCS7Padding:!1})}var e=t.prototype;return e.decryptBuffer=function(t){return this.decrypter.decrypt(t,this.keyData.key.buffer,this.keyData.iv.buffer)},e.decryptAacSample=function(t,e,r){var i=this,n=t[e].unit;if(!(n.length<=16)){var a=n.subarray(16,n.length-n.length%16),s=a.buffer.slice(a.byteOffset,a.byteOffset+a.length);this.decryptBuffer(s).then((function(a){var s=new Uint8Array(a);n.set(s,16),i.decrypter.isSync()||i.decryptAacSamples(t,e+1,r)}))}},e.decryptAacSamples=function(t,e,r){for(;;e++){if(e>=t.length)return void r();if(!(t[e].unit.length<32||(this.decryptAacSample(t,e,r),this.decrypter.isSync())))return}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,n=32;n=t.length)return void i();for(var n=t[e].units;!(r>=n.length);r++){var a=n[r];if(!(a.data.length<=48||1!==a.type&&5!==a.type||(this.decryptAvcSample(t,e,r,i,a),this.decrypter.isSync())))return}}},t}(),nn=188,an=function(){function t(t,e,r){this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.sampleAes=null,this.pmtParsed=!1,this.audioCodec=void 0,this.videoCodec=void 0,this._duration=0,this._pmtId=-1,this._videoTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.remainderData=null,this.videoParser=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.videoParser=new en}t.probe=function(e){var r=t.syncOffset(e);return r>0&&w.warn("MPEG2-TS detected but first sync word found @ offset "+r),-1!==r},t.syncOffset=function(t){for(var e=t.length,r=Math.min(940,e-nn)+1,i=0;i1&&(0===a&&s>2||o+nn>r))return a}i++}return-1},t.createTrack=function(t,e){return{container:"video"===t||"audio"===t?"video/mp2t":void 0,type:t,id:kt[t],pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0,duration:"audio"===t?e:void 0}};var e=t.prototype;return e.resetInitSegment=function(e,r,i,n){this.pmtParsed=!1,this._pmtId=-1,this._videoTrack=t.createTrack("video"),this._audioTrack=t.createTrack("audio",n),this._id3Track=t.createTrack("id3"),this._txtTrack=t.createTrack("text"),this._audioTrack.segmentCodec="aac",this.aacOverFlow=null,this.remainderData=null,this.audioCodec=r,this.videoCodec=i,this._duration=n},e.resetTimeStamp=function(){},e.resetContiguity=function(){var t=this._audioTrack,e=this._videoTrack,r=this._id3Track;t&&(t.pesData=null),e&&(e.pesData=null),r&&(r.pesData=null),this.aacOverFlow=null,this.remainderData=null},e.demux=function(e,r,i,n){var a;void 0===i&&(i=!1),void 0===n&&(n=!1),i||(this.sampleAes=null);var s=this._videoTrack,o=this._audioTrack,l=this._id3Track,u=this._txtTrack,h=s.pid,d=s.pesData,c=o.pid,f=l.pid,g=o.pesData,v=l.pesData,m=null,p=this.pmtParsed,y=this._pmtId,E=e.length;if(this.remainderData&&(E=(e=Gt(this.remainderData,e)).length,this.remainderData=null),E>4>1){if((I=k+5+e[k+4])===k+nn)continue}else I=k+4;switch(D){case h:b&&(d&&(a=hn(d))&&this.videoParser.parseAVCPES(s,u,a,!1,this._duration),d={data:[],size:0}),d&&(d.data.push(e.subarray(I,k+nn)),d.size+=k+nn-I);break;case c:if(b){if(g&&(a=hn(g)))switch(o.segmentCodec){case"aac":this.parseAACPES(o,a);break;case"mp3":this.parseMPEGPES(o,a);break;case"ac3":this.parseAC3PES(o,a)}g={data:[],size:0}}g&&(g.data.push(e.subarray(I,k+nn)),g.size+=k+nn-I);break;case f:b&&(v&&(a=hn(v))&&this.parseID3PES(l,a),v={data:[],size:0}),v&&(v.data.push(e.subarray(I,k+nn)),v.size+=k+nn-I);break;case 0:b&&(I+=e[I]+1),y=this._pmtId=on(e,I);break;case y:b&&(I+=e[I]+1);var C=ln(e,I,this.typeSupported,i);(h=C.videoPid)>0&&(s.pid=h,s.segmentCodec=C.segmentVideoCodec),(c=C.audioPid)>0&&(o.pid=c,o.segmentCodec=C.segmentAudioCodec),(f=C.id3Pid)>0&&(l.pid=f),null===m||p||(w.warn("MPEG-TS PMT found at "+k+" after unknown PID '"+m+"'. Backtracking to sync byte @"+T+" to parse all TS packets."),m=null,k=T-188),p=this.pmtParsed=!0;break;case 17:case 8191:break;default:m=D}}else R++;if(R>0){var _=new Error("Found "+R+" TS packet/s that do not start with 0x47");this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:_,reason:_.message})}s.pesData=d,o.pesData=g,l.pesData=v;var x={audioTrack:o,videoTrack:s,id3Track:l,textTrack:u};return n&&this.extractRemainingSamples(x),x},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{videoTrack:this._videoTrack,audioTrack:this._audioTrack,id3Track:this._id3Track,textTrack:this._txtTrack},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.videoTrack,n=t.id3Track,a=t.textTrack,s=i.pesData,o=r.pesData,l=n.pesData;if(s&&(e=hn(s))?(this.videoParser.parseAVCPES(i,a,e,!0,this._duration),i.pesData=null):i.pesData=s,o&&(e=hn(o))){switch(r.segmentCodec){case"aac":this.parseAACPES(r,e);break;case"mp3":this.parseMPEGPES(r,e);break;case"ac3":this.parseAC3PES(r,e)}r.pesData=null}else null!=o&&o.size&&w.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=o;l&&(e=hn(l))?(this.parseID3PES(n,e),n.pesData=null):n.pesData=l},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),n=this.sampleAes=new rn(this.observer,this.config,e);return this.decrypt(i,n)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,n=t.videoTrack;i.samples&&"aac"===i.segmentCodec?e.decryptAacSamples(i.samples,0,(function(){n.samples?e.decryptAvcSamples(n.samples,0,0,(function(){r(t)})):r(t)})):n.samples&&e.decryptAvcSamples(n.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._duration=0},e.parseAACPES=function(t,e){var r,i,n,a=0,s=this.aacOverFlow,o=e.data;if(s){this.aacOverFlow=null;var l=s.missing,u=s.sample.unit.byteLength;if(-1===l)o=Gt(s.sample.unit,o);else{var h=u-l;s.sample.unit.set(o.subarray(0,l),h),t.samples.push(s.sample),a=s.missing}}for(r=a,i=o.length;r0;)o+=n;else w.warn("[tsdemuxer]: AC3 PES unknown PTS")},e.parseID3PES=function(t,e){if(void 0!==e.pts){var r=o({},e,{type:this._videoTrack?Ke:Be,duration:Number.POSITIVE_INFINITY});t.samples.push(r)}else w.warn("[tsdemuxer]: ID3 PES unknown PTS")},t}();function sn(t,e){return((31&t[e+1])<<8)+t[e+2]}function on(t,e){return(31&t[e+10])<<8|t[e+11]}function ln(t,e,r,i){var n={audioPid:-1,videoPid:-1,id3Pid:-1,segmentVideoCodec:"avc",segmentAudioCodec:"aac"},a=e+3+((15&t[e+1])<<8|t[e+2])-4;for(e+=12+((15&t[e+10])<<8|t[e+11]);e0)for(var l=e+5,u=o;u>2;){106===t[l]&&(!0!==r.ac3?w.log("AC-3 audio found, not supported in this browser for now"):(n.audioPid=s,n.segmentAudioCodec="ac3"));var h=t[l+1]+2;l+=h,u-=h}break;case 194:case 135:w.warn("Unsupported EC-3 in M2TS found");break;case 36:w.warn("Unsupported HEVC in M2TS found")}e+=o+5}return n}function un(t){w.log(t+" with AES-128-CBC encryption found in unencrypted stream")}function hn(t){var e,r,i,n,a,s=0,o=t.data;if(!t||0===t.size)return null;for(;o[0].length<19&&o.length>1;)o[0]=Gt(o[0],o[1]),o.splice(1,1);if(1===((e=o[0])[0]<<16)+(e[1]<<8)+e[2]){if((r=(e[4]<<8)+e[5])&&r>t.size-6)return null;var l=e[7];192&l&&(n=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&l?n-(a=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(w.warn(Math.round((n-a)/9e4)+"s delta between PTS and DTS, align them"),n=a):a=n);var u=(i=e[8])+9;if(t.size<=u)return null;t.size-=u;for(var h=new Uint8Array(t.size),d=0,c=o.length;df){u-=f;continue}e=e.subarray(u),f-=u,u=0}h.set(e,s),s+=f}return r&&(r-=i+3),{data:h,pts:n,dts:a,len:r}}return null}var dn=function(t){function e(){return t.apply(this,arguments)||this}l(e,t);var r=e.prototype;return r.resetInitSegment=function(e,r,i,n){t.prototype.resetInitSegment.call(this,e,r,i,n),this._audioTrack={container:"audio/mpeg",type:"audio",id:2,pid:-1,sequenceNumber:0,segmentCodec:"mp3",samples:[],manifestCodec:r,duration:n,inputTimeScale:9e4,dropped:0}},e.probe=function(t){if(!t)return!1;var e=lt(t,0),r=(null==e?void 0:e.length)||0;if(e&&11===t[r]&&119===t[r+1]&&void 0!==dt(e)&&Qi(t,r)<=16)return!1;for(var i=t.length;r1?r-1:0),n=1;n>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),a=0,e=8;a>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,85,196,0,0]))},t.mdia=function(e){return t.box(t.types.mdia,t.mdhd(e.timescale,e.duration),t.hdlr(e.type),t.minf(e))},t.mfhd=function(e){return t.box(t.types.mfhd,new Uint8Array([0,0,0,0,e>>24,e>>16&255,e>>8&255,255&e]))},t.minf=function(e){return"audio"===e.type?t.box(t.types.minf,t.box(t.types.smhd,t.SMHD),t.DINF,t.stbl(e)):t.box(t.types.minf,t.box(t.types.vmhd,t.VMHD),t.DINF,t.stbl(e))},t.moof=function(e,r,i){return t.box(t.types.moof,t.mfhd(e),t.traf(i,r))},t.moov=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trak(e[r]);return t.box.apply(null,[t.types.moov,t.mvhd(e[0].timescale,e[0].duration)].concat(i).concat(t.mvex(e)))},t.mvex=function(e){for(var r=e.length,i=[];r--;)i[r]=t.trex(e[r]);return t.box.apply(null,[t.types.mvex].concat(i))},t.mvhd=function(e,r){r*=e;var i=Math.floor(r/(fn+1)),n=Math.floor(r%(fn+1)),a=new Uint8Array([1,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,e>>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,n>>24,n>>16&255,n>>8&255,255&n,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,255,255,255,255]);return t.box(t.types.mvhd,a)},t.sdtp=function(e){var r,i,n=e.samples||[],a=new Uint8Array(4+n.length);for(r=0;r>>8&255),a.push(255&n),a=a.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&n),s=s.concat(Array.prototype.slice.call(i));var o=t.box(t.types.avcC,new Uint8Array([1,a[3],a[4],a[5],255,224|e.sps.length].concat(a).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,h=e.pixelRatio[0],d=e.pixelRatio[1];return t.box(t.types.avc1,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,l>>8&255,255&l,u>>8&255,255&u,0,72,0,0,0,72,0,0,0,0,0,0,0,1,18,100,97,105,108,121,109,111,116,105,111,110,47,104,108,115,46,106,115,0,0,0,0,0,0,0,0,0,0,0,0,0,0,24,17,17]),o,t.box(t.types.btrt,new Uint8Array([0,28,156,128,0,45,198,192,0,45,198,192])),t.box(t.types.pasp,new Uint8Array([h>>24,h>>16&255,h>>8&255,255&h,d>>24,d>>16&255,d>>8&255,255&d])))},t.esds=function(t){var e=t.config.length;return new Uint8Array([0,0,0,0,3,23+e,0,1,0,4,15+e,64,21,0,0,0,0,0,0,0,0,0,0,0,5].concat([e]).concat(t.config).concat([6,1,2]))},t.audioStsd=function(t){var e=t.samplerate;return new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,t.channelCount,0,16,0,0,0,0,e>>8&255,255&e,0,0])},t.mp4a=function(e){return t.box(t.types.mp4a,t.audioStsd(e),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){return t.box(t.types[".mp3"],t.audioStsd(e))},t.ac3=function(e){return t.box(t.types["ac-3"],t.audioStsd(e),t.box(t.types.dac3,e.config))},t.stsd=function(e){return"audio"===e.type?"mp3"===e.segmentCodec&&"mp3"===e.codec?t.box(t.types.stsd,t.STSD,t.mp3(e)):"ac3"===e.segmentCodec?t.box(t.types.stsd,t.STSD,t.ac3(e)):t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,n=e.width,a=e.height,s=Math.floor(i/(fn+1)),o=Math.floor(i%(fn+1));return t.box(t.types.tkhd,new Uint8Array([1,0,0,7,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,3,r>>24&255,r>>16&255,r>>8&255,255&r,0,0,0,0,s>>24,s>>16&255,s>>8&255,255&s,o>>24,o>>16&255,o>>8&255,255&o,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,64,0,0,0,n>>8&255,255&n,0,0,a>>8&255,255&a,0,0]))},t.traf=function(e,r){var i=t.sdtp(e),n=e.id,a=Math.floor(r/(fn+1)),s=Math.floor(r%(fn+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,n>>24,n>>16&255,n>>8&255,255&n])),t.box(t.types.tfdt,new Uint8Array([1,0,0,0,a>>24,a>>16&255,a>>8&255,255&a,s>>24,s>>16&255,s>>8&255,255&s])),t.trun(e,i.length+16+20+8+16+8+8),i)},t.trak=function(e){return e.duration=e.duration||4294967295,t.box(t.types.trak,t.tkhd(e),t.mdia(e))},t.trex=function(e){var r=e.id;return t.box(t.types.trex,new Uint8Array([0,0,0,0,r>>24,r>>16&255,r>>8&255,255&r,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,1]))},t.trun=function(e,r){var i,n,a,s,o,l,u=e.samples||[],h=u.length,d=12+16*h,c=new Uint8Array(d);for(r+=8+d,c.set(["video"===e.type?1:0,0,15,1,h>>>24&255,h>>>16&255,h>>>8&255,255&h,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,a>>>16&255,a>>>8&255,255&a,s>>>24&255,s>>>16&255,s>>>8&255,255&s,o.isLeading<<2|o.dependsOn,o.isDependedOn<<6|o.hasRedundancy<<4|o.paddingValue<<1|o.isNonSync,61440&o.degradPrio,15&o.degradPrio,l>>>24&255,l>>>16&255,l>>>8&255,255&l],12+16*i);return t.box(t.types.trun,c)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e);return Gt(t.FTYP,r)},t}();gn.types=void 0,gn.HDLR_TYPES=void 0,gn.STTS=void 0,gn.STSC=void 0,gn.STCO=void 0,gn.STSZ=void 0,gn.VMHD=void 0,gn.SMHD=void 0,gn.STSD=void 0,gn.FTYP=void 0,gn.DINF=void 0;var vn=9e4;function mn(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var n=t*e*r;return i?Math.round(n):n}function pn(t,e){return void 0===e&&(e=!1),mn(t,1e3,1/vn,e)}var yn=null,En=null,Tn=function(){function t(t,e,r,i){if(this.observer=void 0,this.config=void 0,this.typeSupported=void 0,this.ISGenerated=!1,this._initPTS=null,this._initDTS=null,this.nextAvcDts=null,this.nextAudioPts=null,this.videoSampleDuration=null,this.isAudioContiguous=!1,this.isVideoContiguous=!1,this.videoTrackConfig=void 0,this.observer=t,this.config=e,this.typeSupported=r,this.ISGenerated=!1,null===yn){var n=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);yn=n?parseInt(n[1]):0}if(null===En){var a=navigator.userAgent.match(/Safari\/(\d+)/i);En=a?parseInt(a[1]):0}}var e=t.prototype;return e.destroy=function(){this.config=this.videoTrackConfig=this._initPTS=this._initDTS=null},e.resetTimeStamp=function(t){w.log("[mp4-remuxer]: initPTS & initDTS reset"),this._initPTS=this._initDTS=t},e.resetNextTimestamp=function(){w.log("[mp4-remuxer]: reset next timestamp"),this.isVideoContiguous=!1,this.isAudioContiguous=!1},e.resetInitSegment=function(){w.log("[mp4-remuxer]: ISGenerated flag reset"),this.ISGenerated=!1,this.videoTrackConfig=void 0},e.getVideoStartPts=function(t){var e=!1,r=t.reduce((function(t,r){var i=r.pts-t;return i<-4294967296?(e=!0,Sn(t,r.pts)):i>0?t:r.pts}),t[0].pts);return e&&w.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,n,a,s,o){var l,u,h,d,c,f,g=n,v=n,m=t.pid>-1,p=e.pid>-1,y=e.samples.length,E=t.samples.length>0,T=s&&y>0||y>1;if((!m||E)&&(!p||T)||this.ISGenerated||s){if(this.ISGenerated){var S,L,A,R,k=this.videoTrackConfig;!k||e.width===k.width&&e.height===k.height&&(null==(S=e.pixelRatio)?void 0:S[0])===(null==(L=k.pixelRatio)?void 0:L[0])&&(null==(A=e.pixelRatio)?void 0:A[1])===(null==(R=k.pixelRatio)?void 0:R[1])||this.resetInitSegment()}else h=this.generateIS(t,e,n,a);var b,D=this.isVideoContiguous,I=-1;if(T&&(I=function(t){for(var e=0;e0){w.warn("[mp4-remuxer]: Dropped "+I+" out of "+y+" video samples due to a missing keyframe");var C=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(I),e.dropped+=I,b=v+=(e.samples[0].pts-C)/e.inputTimeScale}else-1===I&&(w.warn("[mp4-remuxer]: No keyframe found out of "+y+" video samples"),f=!1);if(this.ISGenerated){if(E&&T){var _=this.getVideoStartPts(e.samples),x=(Sn(t.samples[0].pts,_)-_)/e.inputTimeScale;g+=Math.max(0,x),v+=Math.max(0,-x)}if(E){if(t.samplerate||(w.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),h=this.generateIS(t,e,n,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,a,p||T||o===we?v:void 0),T){var P=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(w.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),h=this.generateIS(t,e,n,a)),l=this.remuxVideo(e,v,D,P)}}else T&&(l=this.remuxVideo(e,v,D,0));l&&(l.firstKeyFrame=I,l.independent=-1!==I,l.firstKeyFramePTS=b)}}return this.ISGenerated&&this._initPTS&&this._initDTS&&(r.samples.length&&(c=Ln(r,n,this._initPTS,this._initDTS)),i.samples.length&&(d=An(i,n,this._initPTS))),{audio:u,video:l,initSegment:h,independent:f,text:d,id3:c}},e.generateIS=function(t,e,r,i){var n,a,s,o=t.samples,l=e.samples,u=this.typeSupported,h={},d=this._initPTS,c=!d||i,f="audio/mp4";if(c&&(n=a=1/0),t.config&&o.length){switch(t.timescale=t.samplerate,t.segmentCodec){case"mp3":u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3");break;case"ac3":t.codec="ac-3"}h.audio={id:"audio",container:f,codec:t.codec,initSegment:"mp3"===t.segmentCodec&&u.mpeg?new Uint8Array(0):gn.initSegment([t]),metadata:{channelCount:t.channelCount}},c&&(s=t.inputTimeScale,d&&s===d.timescale?c=!1:n=a=o[0].pts-Math.round(s*r))}if(e.sps&&e.pps&&l.length){if(e.timescale=e.inputTimeScale,h.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:gn.initSegment([e]),metadata:{width:e.width,height:e.height}},c)if(s=e.inputTimeScale,d&&s===d.timescale)c=!1;else{var g=this.getVideoStartPts(l),v=Math.round(s*r);a=Math.min(a,Sn(l[0].dts,g)-v),n=Math.min(n,g-v)}this.videoTrackConfig={width:e.width,height:e.height,pixelRatio:e.pixelRatio}}if(Object.keys(h).length)return this.ISGenerated=!0,c?(this._initPTS={baseTime:n,timescale:s},this._initDTS={baseTime:a,timescale:s}):n=s=void 0,{tracks:h,initPTS:n,timescale:s}},e.remuxVideo=function(t,e,r,i){var n,a,s=t.inputTimeScale,l=t.samples,u=[],h=l.length,d=this._initPTS,c=this.nextAvcDts,f=8,g=this.videoSampleDuration,v=Number.POSITIVE_INFINITY,m=Number.NEGATIVE_INFINITY,p=!1;if(!r||null===c){var y=e*s,E=l[0].pts-Sn(l[0].dts,l[0].pts);yn&&null!==c&&Math.abs(y-E-c)<15e3?r=!0:c=y-E}for(var T=d.baseTime*s/d.timescale,R=0;R0?R-1:R].dts&&(p=!0)}p&&l.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),n=l[0].dts;var b=(a=l[l.length-1].dts)-n,D=b?Math.round(b/(h-1)):g||t.inputTimeScale/30;if(r){var I=n-c,C=I>D,_=I<-1;if((C||_)&&(C?w.warn("AVC: "+pn(I,!0)+" ms ("+I+"dts) hole between fragments detected at "+e.toFixed(3)):w.warn("AVC: "+pn(-I,!0)+" ms ("+I+"dts) overlapping between fragments detected at "+e.toFixed(3)),!_||c>=l[0].pts||yn)){n=c;var x=l[0].pts-I;if(C)l[0].dts=n,l[0].pts=x;else for(var P=0;Px);P++)l[P].dts-=I,l[P].pts-=I;w.log("Video: Initial PTS/DTS adjusted: "+pn(x,!0)+"/"+pn(n,!0)+", delta: "+pn(I,!0)+" ms")}}for(var F=0,M=0,O=n=Math.max(0,n),N=0;N0?$.dts-l[J-1].dts:D;if(st=J>0?$.pts-l[J-1].pts:D,ot.stretchShortVideoTrack&&null!==this.nextAudioPts){var ut=Math.floor(ot.maxBufferHole*s),ht=(i?v+i*s:this.nextAudioPts)-$.pts;ht>ut?((g=ht-lt)<0?g=lt:j=!0,w.log("[mp4-remuxer]: It is approximately "+ht/90+" ms to the next segment; using duration "+g/90+" ms for the last video frame.")):g=lt}else g=lt}var dt=Math.round($.pts-$.dts);q=Math.min(q,g),z=Math.max(z,g),X=Math.min(X,st),Q=Math.max(Q,st),u.push(new kn($.key,g,tt,dt))}if(u.length)if(yn){if(yn<70){var ct=u[0].flags;ct.dependsOn=2,ct.isNonSync=0}}else if(En&&Q-X0&&(i&&Math.abs(p-m)<9e3||Math.abs(Sn(g[0].pts-y,p)-m)<20*u),g.forEach((function(t){t.pts=Sn(t.pts-y,p)})),!r||m<0){if(g=g.filter((function(t){return t.pts>=0})),!g.length)return;m=0===n?0:i&&!f?Math.max(0,p):g[0].pts}if("aac"===t.segmentCodec)for(var E=this.config.maxAudioFramesDrift,T=0,R=m;T=E*u&&I<1e4&&f){var C=Math.round(D/u);(R=b-C*u)<0&&(C--,R+=u),0===T&&(this.nextAudioPts=m=R),w.warn("[mp4-remuxer]: Injecting "+C+" audio frame @ "+(R/a).toFixed(3)+"s due to "+Math.round(1e3*D/a)+" ms gap.");for(var _=0;_0))return;N+=v;try{F=new Uint8Array(N)}catch(t){return void this.observer.emit(S.ERROR,S.ERROR,{type:L.MUX_ERROR,details:A.REMUX_ALLOC_ERROR,fatal:!1,error:t,bytes:N,reason:"fail allocating audio mdat "+N})}d||(new DataView(F.buffer).setUint32(0,N),F.set(gn.types.mdat,4))}F.set(H,v);var Y=H.byteLength;v+=Y,c.push(new kn(!0,l,Y,0)),O=V}var W=c.length;if(W){var j=c[c.length-1];this.nextAudioPts=m=O+s*j.duration;var q=d?new Uint8Array(0):gn.moof(t.sequenceNumber++,M/s,o({},t,{samples:c}));t.samples=[];var X=M/a,z=m/a,Q={data1:q,data2:F,startPTS:X,endPTS:z,startDTS:X,endDTS:z,type:"audio",hasAudio:!0,hasVideo:!1,nb:W};return this.isAudioContiguous=!0,Q}},e.remuxEmptyAudio=function(t,e,r,i){var n=t.inputTimeScale,a=n/(t.samplerate?t.samplerate:n),s=this.nextAudioPts,o=this._initDTS,l=9e4*o.baseTime/o.timescale,u=(null!==s?s:i.startDTS*n)+l,h=i.endDTS*n+l,d=1024*a,c=Math.ceil((h-u)/d),f=cn.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(w.warn("[mp4-remuxer]: remux empty Audio"),f){for(var g=[],v=0;v4294967296;)t+=r;return t}function Ln(t,e,r,i){var n=t.samples.length;if(n){for(var a=t.inputTimeScale,s=0;s0;n||(i=_t(e,["encv"])),i.forEach((function(t){_t(n?t.subarray(28):t.subarray(78),["sinf"]).forEach((function(t){var e=Ut(t);if(e){var i=e.subarray(8,24);i.some((function(t){return 0!==t}))||(w.log("[eme] Patching keyId in 'enc"+(n?"a":"v")+">sinf>>tenc' box: "+Lt(i)+" -> "+Lt(r)),e.set(r,8))}}))}))})),t}(t,i)),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(null==t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Pt(t);i.audio&&(e=Dn(i.audio,O)),i.video&&(r=Dn(i.video,N));var n={};i.audio&&i.video?n.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?n.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?n.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:w.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=n},e.remux=function(t,e,r,i,n,a){var s,o,l=this.initPTS,u=this.lastEndTime,h={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};y(u)||(u=this.lastEndTime=n||0);var d=e.samples;if(null==d||!d.length)return h;var c={initPTS:void 0,timescale:1},f=this.initData;if(null!=(s=f)&&s.length||(this.generateInitSegment(d),f=this.initData),null==(o=f)||!o.length)return w.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),h;this.emitInitSegment&&(c.tracks=this.initTracks,this.emitInitSegment=!1);var g=function(t,e){for(var r=0,i=0,n=0,a=_t(t,["moof","traf"]),s=0;sn}(l,m,n,g)||c.timescale!==l.timescale&&a)&&(c.initPTS=m-n,l&&1===l.timescale&&w.warn("Adjusting initPTS by "+(c.initPTS-l.baseTime)),this.initPTS=l={baseTime:c.initPTS,timescale:1});var p=t?m-l.baseTime/l.timescale:u,E=p+g;!function(t,e,r){_t(e,["moof","traf"]).forEach((function(e){_t(e,["tfhd"]).forEach((function(i){var n=It(i,4),a=t[n];if(a){var s=a.timescale||9e4;_t(e,["tfdt"]).forEach((function(t){var e=t[0],i=r*s;if(i){var n=It(t,4);if(0===e)n-=i,Ct(t,4,n=Math.max(n,0));else{n*=Math.pow(2,32),n+=It(t,8),n-=i,n=Math.max(n,0);var a=Math.floor(n/(At+1)),o=Math.floor(n%(At+1));Ct(t,4,a),Ct(t,8,o)}}}))}}))}))}(f,d,l.baseTime/l.timescale),g>0?this.lastEndTime=E:(w.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var T=!!f.audio,S=!!f.video,L="";T&&(L+="audio"),S&&(L+="video");var A={data1:d,startPTS:p,startDTS:p,endPTS:E,endDTS:E,type:L,hasAudio:T,hasVideo:S,nb:1,dropped:0};return h.audio="audio"===A.type?A:void 0,h.video="audio"!==A.type?A:void 0,h.initSegment=c,h.id3=Ln(r,n,l,l),i.samples.length&&(h.text=An(i,n,l)),h},t}();function Dn(t,e){var r=null==t?void 0:t.codec;if(r&&r.length>4)return r;if(e===O){if("ec-3"===r||"ac-3"===r||"alac"===r)return r;if("fLaC"===r||"Opus"===r)return ue(r,!1);var i="mp4a.40.5";return w.info('Parsed audio codec "'+r+'" or audio object type not handled. Using "'+i+'"'),i}return w.warn('Unhandled video codec "'+r+'"'),"hvc1"===r||"hev1"===r?"hvc1.1.6.L120.90":"av01"===r?"av01.0.04M.08":"avc1.42e01e"}try{Rn=self.performance.now.bind(self.performance)}catch(t){w.debug("Unable to use Performance API on this environment"),Rn=null==j?void 0:j.Date.now}var In=[{demux:zi,remux:bn},{demux:an,remux:Tn},{demux:qi,remux:Tn},{demux:dn,remux:Tn}];In.splice(2,0,{demux:Ji,remux:Tn});var wn=function(){function t(t,e,r,i,n){this.async=!1,this.observer=void 0,this.typeSupported=void 0,this.config=void 0,this.vendor=void 0,this.id=void 0,this.demuxer=void 0,this.remuxer=void 0,this.decrypter=void 0,this.probe=void 0,this.decryptionPromise=null,this.transmuxConfig=void 0,this.currentTransmuxState=void 0,this.observer=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=n}var e=t.prototype;return e.configure=function(t){this.transmuxConfig=t,this.decrypter&&this.decrypter.reset()},e.push=function(t,e,r,i){var n=this,a=r.transmuxing;a.executeStart=Rn();var s=new Uint8Array(t),o=this.currentTransmuxState,l=this.transmuxConfig;i&&(this.currentTransmuxState=i);var u=i||o,h=u.contiguous,d=u.discontinuity,c=u.trackSwitch,f=u.accurateTimeOffset,g=u.timeOffset,v=u.initSegmentChange,m=l.audioCodec,p=l.videoCodec,y=l.defaultInitPts,E=l.duration,T=l.initSegmentData,R=function(t,e){var r=null;return t.byteLength>0&&null!=(null==e?void 0:e.key)&&null!==e.iv&&null!=e.method&&(r=e),r}(s,e);if(R&&"AES-128"===R.method){var k=this.getDecrypter();if(!k.isSync())return this.decryptionPromise=k.webCryptoDecrypt(s,R.key.buffer,R.iv.buffer).then((function(t){var e=n.push(t,null,r);return n.decryptionPromise=null,e})),this.decryptionPromise;var b=k.softwareDecrypt(s,R.key.buffer,R.iv.buffer);if(r.part>-1&&(b=k.flush()),!b)return a.executeEnd=Rn(),Cn(r);s=new Uint8Array(b)}var D=this.needsProbing(d,c);if(D){var I=this.configureTransmuxer(s);if(I)return w.warn("[transmuxer] "+I.message),this.observer.emit(S.ERROR,S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,fatal:!1,error:I,reason:I.message}),a.executeEnd=Rn(),Cn(r)}(d||c||v||D)&&this.resetInitSegment(T,m,p,E,e),(d||v||D)&&this.resetInitialTimestamp(y),h||this.resetContiguity();var C=this.transmux(s,R,g,f,r),_=this.currentTransmuxState;return _.contiguous=!0,_.discontinuity=!1,_.trackSwitch=!1,a.executeEnd=Rn(),C},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=Rn();var i=this.decrypter,n=this.currentTransmuxState,a=this.decryptionPromise;if(a)return a.then((function(){return e.flush(t)}));var s=[],o=n.timeOffset;if(i){var l=i.flush();l&&s.push(this.push(l,null,t))}var u=this.demuxer,h=this.remuxer;if(!u||!h)return r.executeEnd=Rn(),[Cn(t)];var d=u.flush(o);return _n(d)?d.then((function(r){return e.flushRemux(s,r,t),s})):(this.flushRemux(s,d,t),s)},e.flushRemux=function(t,e,r){var i=e.audioTrack,n=e.videoTrack,a=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;w.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var h=this.remuxer.remux(i,n,a,s,u,l,!0,this.id);t.push({remuxResult:h,chunkMeta:r}),r.transmuxing.executeEnd=Rn()},e.resetInitialTimestamp=function(t){var e=this.demuxer,r=this.remuxer;e&&r&&(e.resetTimeStamp(t),r.resetTimeStamp(t))},e.resetContiguity=function(){var t=this.demuxer,e=this.remuxer;t&&e&&(t.resetContiguity(),e.resetNextTimestamp())},e.resetInitSegment=function(t,e,r,i,n){var a=this.demuxer,s=this.remuxer;a&&s&&(a.resetInitSegment(t,e,r,i),s.resetInitSegment(t,e,r,n))},e.destroy=function(){this.demuxer&&(this.demuxer.destroy(),this.demuxer=void 0),this.remuxer&&(this.remuxer.destroy(),this.remuxer=void 0)},e.transmux=function(t,e,r,i,n){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,n):this.transmuxUnencrypted(t,r,i,n)},e.transmuxUnencrypted=function(t,e,r,i){var n=this.demuxer.demux(t,e,!1,!this.config.progressive),a=n.audioTrack,s=n.videoTrack,o=n.id3Track,l=n.textTrack;return{remuxResult:this.remuxer.remux(a,s,o,l,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,n){var a=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:a.remuxer.remux(t.audioTrack,t.videoTrack,t.id3Track,t.textTrack,r,i,!1,a.id),chunkMeta:n}}))},e.configureTransmuxer=function(t){for(var e,r=this.config,i=this.observer,n=this.typeSupported,a=this.vendor,s=0,o=In.length;s1&&l.id===(null==m?void 0:m.stats.chunkCount),L=!y&&(1===E||0===E&&(1===T||S&&T<=0)),A=self.performance.now();(y||E||0===n.stats.parsing.start)&&(n.stats.parsing.start=A),!a||!T&&L||(a.stats.parsing.start=A);var R=!(m&&(null==(h=n.initSegment)?void 0:h.url)===(null==(d=m.initSegment)?void 0:d.url)),k=new Pn(p,L,o,y,g,R);if(!L||p||R){w.log("[transmuxer-interface, "+n.type+"]: Starting new transmux session for sn: "+l.sn+" p: "+l.part+" level: "+l.level+" id: "+l.id+"\n discontinuity: "+p+"\n trackSwitch: "+y+"\n contiguous: "+L+"\n accurateTimeOffset: "+o+"\n timeOffset: "+g+"\n initSegmentChange: "+R);var b=new xn(r,i,e,s,u);this.configureTransmuxer(b)}if(this.frag=n,this.part=a,this.workerContext)this.workerContext.worker.postMessage({cmd:"demux",data:t,decryptdata:v,chunkMeta:l,state:k},t instanceof ArrayBuffer?[t]:[]);else if(f){var D=f.push(t,v,l,k);_n(D)?(f.async=!0,D.then((function(t){c.handleTransmuxComplete(t)})).catch((function(t){c.transmuxerError(t,l,"transmuxer-interface push error")}))):(f.async=!1,this.handleTransmuxComplete(D))}},r.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer;if(this.workerContext)this.workerContext.worker.postMessage({cmd:"flush",chunkMeta:t});else if(r){var i=r.flush(t);_n(i)||r.async?(_n(i)||(i=Promise.resolve(i)),i.then((function(r){e.handleFlushResult(r,t)})).catch((function(r){e.transmuxerError(r,t,"transmuxer-interface flush error")}))):this.handleFlushResult(i,t)}},r.transmuxerError=function(t,e,r){this.hls&&(this.error=t,this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_PARSING_ERROR,chunkMeta:e,fatal:!1,error:t,err:t,reason:r}))},r.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},r.onWorkerMessage=function(t){var e=t.data,r=this.hls;switch(e.event){case"init":var i,n=null==(i=this.workerContext)?void 0:i.objectURL;n&&self.URL.revokeObjectURL(n);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;case"workerLog":w[e.data.logType]&&w[e.data.logType](e.data.message);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}},r.configureTransmuxer=function(t){var e=this.transmuxer;this.workerContext?this.workerContext.worker.postMessage({cmd:"configure",config:t}):e&&e.configure(t)},r.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},e}();function Gn(t,e){if(t.length!==e.length)return!1;for(var r=0;r0&&-1===t?(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e,this.state=fi):(this.loadedmetadata=!1,this.state=pi),this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()},r.doTick=function(){switch(this.state){case fi:this.doTickIdle();break;case pi:var e,r=this.levels,i=this.trackId,n=null==r||null==(e=r[i])?void 0:e.details;if(n){if(this.waitForCdnTuneIn(n))break;this.state=Li}break;case mi:var a,s=performance.now(),o=this.retryDate;if(!o||s>=o||null!=(a=this.media)&&a.seeking){var l=this.levels,u=this.trackId;this.log("RetryDate reached, switch back to IDLE state"),this.resetStartWhenNotLoaded((null==l?void 0:l[u])||null),this.state=fi}break;case Li:var h=this.waitingData;if(h){var d=h.frag,c=h.part,f=h.cache,g=h.complete;if(void 0!==this.initPTS[d.cc]){this.waitingData=null,this.waitingVideoCC=-1,this.state=vi;var v={frag:d,part:c,payload:f.flush(),networkDetails:null};this._handleFragmentLoadProgress(v),g&&t.prototype._handleFragmentLoadComplete.call(this,v)}else if(this.videoTrackCC!==this.waitingVideoCC)this.log("Waiting fragment cc ("+d.cc+") cancelled because video is at cc "+this.videoTrackCC),this.clearWaitingFragment();else{var m=this.getLoadPosition(),p=zr.bufferInfo(this.mediaBuffer,m,this.config.maxBufferHole);pr(p.end,this.config.maxFragLookUpTolerance,d)<0&&(this.log("Waiting fragment cc ("+d.cc+") @ "+d.start+" cancelled because another fragment at "+p.end+" is needed"),this.clearWaitingFragment())}}else this.state=fi}this.onTickEnd()},r.clearWaitingFragment=function(){var t=this.waitingData;t&&(this.fragmentTracker.removeFragment(t.frag),this.waitingData=null,this.waitingVideoCC=-1,this.state=fi)},r.resetLoadingState=function(){this.clearWaitingFragment(),t.prototype.resetLoadingState.call(this)},r.onTickEnd=function(){var t=this.media;null!=t&&t.readyState&&(this.lastCurrentTime=t.currentTime)},r.doTickIdle=function(){var t=this.hls,e=this.levels,r=this.media,i=this.trackId,n=t.config;if((r||!this.startFragRequested&&n.startFragPrefetch)&&null!=e&&e[i]){var a=e[i],s=a.details;if(!s||s.live&&this.levelLastLoaded!==a||this.waitForCdnTuneIn(s))this.state=pi;else{var o=this.mediaBuffer?this.mediaBuffer:this.media;this.bufferFlushed&&o&&(this.bufferFlushed=!1,this.afterBufferFlushed(o,O,we));var l=this.getFwdBufferInfo(o,we);if(null!==l){var u=this.bufferedTrack,h=this.switchingTrack;if(!h&&this._streamEnded(l,s))return t.trigger(S.BUFFER_EOS,{type:"audio"}),void(this.state=Ti);var d=this.getFwdBufferInfo(this.videoBuffer?this.videoBuffer:this.media,Ie),c=l.len,f=this.getMaxBufferLength(null==d?void 0:d.len),g=s.fragments,v=g[0].start,m=this.flushing?this.getLoadPosition():l.end;if(h&&r){var p=this.getLoadPosition();u&&!Kn(h.attrs,u.attrs)&&(m=p),s.PTSKnown&&pv||l.nextStart)&&(this.log("Alt audio track ahead of main track, seek to start of alt audio track"),r.currentTime=v+.05)}if(!(c>=f&&!h&&md.end+s.targetduration;if(T||(null==d||!d.len)&&l.len){var L=this.getAppendedFrag(y.start,Ie);if(null===L)return;if(E||(E=!!L.gap||!!T&&0===d.len),T&&!E||E&&l.nextStart&&l.nextStart-1)n=a[o];else{var l=Mr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={audioTracks:a};this.log("Updating audio tracks, "+a.length+" track(s) found in group(s): "+(null==r?void 0:r.join(","))),this.hls.trigger(S.AUDIO_TRACKS_UPDATED,h);var d=this.trackId;if(-1!==u&&-1===d)this.setAudioTrack(u);else if(a.length&&-1===d){var c,f=new Error("No audio track selected for current audio group-ID(s): "+(null==(c=this.groupIds)?void 0:c.join(","))+" track count: "+a.length);this.warn(f.message),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.AUDIO_TRACK_LOAD_ERROR,fatal:!0,error:f})}}else this.shouldReloadPlaylist(n)&&this.setAudioTrack(this.trackId)}},r.onError=function(t,e){!e.fatal&&e.context&&(e.context.type!==be||e.context.id!==this.trackId||this.groupIds&&-1===this.groupIds.indexOf(e.context.groupId)||(this.requestScheduled=-1,this.checkRetry(e)))},r.setAudioOption=function(t){var e=this.hls;if(e.config.audioPreference=t,t){var r=this.allAudioTracks;if(this.selectDefaultTrack=!1,r.length){var i=this.currentTrack;if(i&&Or(t,i,Nr))return i;var n=Mr(t,this.tracksInGroup,Nr);if(n>-1){var a=this.tracksInGroup[n];return this.setAudioTrack(n),a}if(i){var s=e.loadLevel;-1===s&&(s=e.firstAutoLevel);var o=function(t,e,r,i,n){var a=e[i],s=e.reduce((function(t,e,r){var i=e.uri;return(t[i]||(t[i]=[])).push(r),t}),{})[a.uri];s.length>1&&(i=Math.max.apply(Math,s));var o=a.videoRange,l=a.frameRate,u=a.codecSet.substring(0,4),h=Ur(e,i,(function(e){if(e.videoRange!==o||e.frameRate!==l||e.codecSet.substring(0,4)!==u)return!1;var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Mr(t,a,n)>-1}));return h>-1?h:Ur(e,i,(function(e){var i=e.audioGroups,a=r.filter((function(t){return!i||-1!==i.indexOf(t.groupId)}));return Mr(t,a,n)>-1}))}(t,e.levels,r,s,Nr);if(-1===o)return null;e.nextLoadLevel=o}if(t.channels||t.audioCodec){var l=Mr(t,r);if(l>-1)return r[l]}}}return null},r.setAudioTrack=function(t){var e=this.tracksInGroup;if(t<0||t>=e.length)this.warn("Invalid audio track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,n=e[t],a=n.details&&!n.details.live;if(!(t===this.trackId&&n===r&&a||(this.log("Switching to audio-track "+t+' "'+n.name+'" lang:'+n.lang+" group:"+n.groupId+" channels:"+n.channels),this.trackId=t,this.currentTrack=n,this.hls.trigger(S.AUDIO_TRACK_SWITCHING,i({},n)),a))){var s=this.switchParams(n.url,null==r?void 0:r.details);this.loadPlaylist(s)}}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=0;r=n[o].start&&s<=n[o].end){a=n[o];break}var l=r.start+r.duration;a?a.end=l:(a={start:s,end:l},n.push(a)),this.fragmentTracker.fragBuffered(r),this.fragBufferedComplete(r,null)}}},r.onBufferFlushing=function(t,e){var r=e.startOffset,i=e.endOffset;if(0===r&&i!==Number.POSITIVE_INFINITY){var n=i-1;if(n<=0)return;e.endOffsetSubtitles=Math.max(0,n),this.tracksBuffered.forEach((function(t){for(var e=0;e=n.length||s!==i)&&o){this.log("Subtitle track "+s+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+",duration:"+a.totalduration),this.mediaBuffer=this.mediaBufferTimeRanges;var l=0;if(a.live||null!=(r=o.details)&&r.live){var u=this.mainDetails;if(a.deltaUpdateFailed||!u)return;var h,d=u.fragments[0];o.details?0===(l=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details))&&d&&sr(a,l=d.start):a.hasProgramDateTime&&u.hasProgramDateTime?(ei(a,u),l=a.fragments[0].start):d&&sr(a,l=d.start)}o.details=a,this.levelLastLoaded=o,this.startFragRequested||!this.mainDetails&&a.live||this.setStartPosition(o.details,l),this.tick(),a.live&&!this.fragCurrent&&this.media&&this.state===fi&&(mr(null,a.fragments,this.media.currentTime,0)||(this.warn("Subtitle playlist not aligned with playback"),o.details=void 0))}}else this.warn("Subtitle tracks were reset while loading level "+s)},r._handleFragmentLoadComplete=function(t){var e=this,r=t.frag,i=t.payload,n=r.decryptdata,a=this.hls;if(!this.fragContextChanged(r)&&i&&i.byteLength>0&&null!=n&&n.key&&n.iv&&"AES-128"===n.method){var s=performance.now();this.decrypter.decrypt(new Uint8Array(i),n.key.buffer,n.iv.buffer).catch((function(t){throw a.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.FRAG_DECRYPT_ERROR,fatal:!1,error:t,reason:t.message,frag:r}),t})).then((function(t){var e=performance.now();a.trigger(S.FRAG_DECRYPTED,{frag:r,payload:t,stats:{tstart:s,tdecrypt:e}})})).catch((function(t){e.warn(t.name+": "+t.message),e.state=fi}))}},r.doTick=function(){if(this.media){if(this.state===fi){var t=this.currentTrackId,e=this.levels,r=null==e?void 0:e[t];if(!r||!e.length||!r.details)return;var i=this.config,n=this.getLoadPosition(),a=zr.bufferedInfo(this.tracksBuffered[this.currentTrackId]||[],n,i.maxBufferHole),s=a.end,o=a.len,l=this.getFwdBufferInfo(this.media,Ie),u=r.details;if(o>this.getMaxBufferLength(null==l?void 0:l.len)+u.levelTargetDuration)return;var h=u.fragments,d=h.length,c=u.edge,f=null,g=this.fragPrevious;if(sc-v?0:v;!(f=mr(g,h,Math.max(h[0].start,s),m))&&g&&g.start>>=0)>i-1)throw new DOMException("Failed to execute '"+e+"' on 'TimeRanges': The index provided ("+r+") is greater than the maximum bound ("+i+")");return t[r][e]};this.buffered={get length(){return t.length},end:function(r){return e("end",r,t.length)},start:function(r){return e("start",r,t.length)}}},qn=function(t){function e(e){var r;return(r=t.call(this,e,"[subtitle-track-controller]")||this).media=null,r.tracks=[],r.groupIds=null,r.tracksInGroup=[],r.trackId=-1,r.currentTrack=null,r.selectDefaultTrack=!0,r.queuedDefaultTrack=-1,r.asyncPollTrackChange=function(){return r.pollTrackChange(0)},r.useTextTrackPolling=!1,r.subtitlePollingInterval=-1,r._subtitleDisplay=!0,r.onTextTracksChanged=function(){if(r.useTextTrackPolling||self.clearInterval(r.subtitlePollingInterval),r.media&&r.hls.config.renderTextTracksNatively){for(var t=null,e=Ue(r.media.textTracks),i=0;i-1&&(this.subtitleTrack=this.queuedDefaultTrack,this.queuedDefaultTrack=-1),this.useTextTrackPolling=!(this.media.textTracks&&"onchange"in this.media.textTracks),this.useTextTrackPolling?this.pollTrackChange(500):this.media.textTracks.addEventListener("change",this.asyncPollTrackChange))},r.pollTrackChange=function(t){self.clearInterval(this.subtitlePollingInterval),this.subtitlePollingInterval=self.setInterval(this.onTextTracksChanged,t)},r.onMediaDetaching=function(){this.media&&(self.clearInterval(this.subtitlePollingInterval),this.useTextTrackPolling||this.media.textTracks.removeEventListener("change",this.asyncPollTrackChange),this.trackId>-1&&(this.queuedDefaultTrack=this.trackId),Ue(this.media.textTracks).forEach((function(t){Oe(t)})),this.subtitleTrack=-1,this.media=null)},r.onManifestLoading=function(){this.tracks=[],this.groupIds=null,this.tracksInGroup=[],this.trackId=-1,this.currentTrack=null,this.selectDefaultTrack=!0},r.onManifestParsed=function(t,e){this.tracks=e.subtitleTracks},r.onSubtitleTrackLoaded=function(t,e){var r=e.id,i=e.groupId,n=e.details,a=this.tracksInGroup[r];if(a&&a.groupId===i){var s=a.details;a.details=e.details,this.log("Subtitle track "+r+' "'+a.name+'" lang:'+a.lang+" group:"+i+" loaded ["+n.startSN+"-"+n.endSN+"]"),r===this.trackId&&this.playlistLoaded(r,e,s)}else this.warn("Subtitle track with id:"+r+" and group:"+i+" not found in active group "+(null==a?void 0:a.groupId))},r.onLevelLoading=function(t,e){this.switchLevel(e.level)},r.onLevelSwitching=function(t,e){this.switchLevel(e.level)},r.switchLevel=function(t){var e=this.hls.levels[t];if(e){var r=e.subtitleGroups||null,i=this.groupIds,n=this.currentTrack;if(!r||(null==i?void 0:i.length)!==(null==r?void 0:r.length)||null!=r&&r.some((function(t){return-1===(null==i?void 0:i.indexOf(t))}))){this.groupIds=r,this.trackId=-1,this.currentTrack=null;var a=this.tracks.filter((function(t){return!r||-1!==r.indexOf(t.groupId)}));if(a.length)this.selectDefaultTrack&&!a.some((function(t){return t.default}))&&(this.selectDefaultTrack=!1),a.forEach((function(t,e){t.id=e}));else if(!n&&!this.tracksInGroup.length)return;this.tracksInGroup=a;var s=this.hls.config.subtitlePreference;if(!n&&s){this.selectDefaultTrack=!1;var o=Mr(s,a);if(o>-1)n=a[o];else{var l=Mr(s,this.tracks);n=this.tracks[l]}}var u=this.findTrackId(n);-1===u&&n&&(u=this.findTrackId(null));var h={subtitleTracks:a};this.log("Updating subtitle tracks, "+a.length+' track(s) found in "'+(null==r?void 0:r.join(","))+'" group-id'),this.hls.trigger(S.SUBTITLE_TRACKS_UPDATED,h),-1!==u&&-1===this.trackId&&this.setSubtitleTrack(u)}else this.shouldReloadPlaylist(n)&&this.setSubtitleTrack(this.trackId)}},r.findTrackId=function(t){for(var e=this.tracksInGroup,r=this.selectDefaultTrack,i=0;i-1){var n=this.tracksInGroup[i];return this.setSubtitleTrack(i),n}if(r)return null;var a=Mr(t,e);if(a>-1)return e[a]}}return null},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentTrack;if(this.shouldLoadPlaylist(r)&&r){var i=r.id,n=r.groupId,a=r.url;if(e)try{a=e.addDirectives(a)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}this.log("Loading subtitle playlist for id "+i),this.hls.trigger(S.SUBTITLE_TRACK_LOADING,{url:a,id:i,groupId:n,deliveryDirectives:e||null})}},r.toggleTrackModes=function(){var t=this.media;if(t){var e,r=Ue(t.textTracks),i=this.currentTrack;if(i&&((e=r.filter((function(t){return Hn(i,t)}))[0])||this.warn('Unable to find subtitle TextTrack with name "'+i.name+'" and language "'+i.lang+'"')),[].slice.call(r).forEach((function(t){"disabled"!==t.mode&&t!==e&&(t.mode="disabled")})),e){var n=this.subtitleDisplay?"showing":"hidden";e.mode!==n&&(e.mode=n)}}},r.setSubtitleTrack=function(t){var e=this.tracksInGroup;if(this.media)if(t<-1||t>=e.length||!y(t))this.warn("Invalid subtitle track id: "+t);else{this.clearTimer(),this.selectDefaultTrack=!1;var r=this.currentTrack,i=e[t]||null;if(this.trackId=t,this.currentTrack=i,this.toggleTrackModes(),i){var n=!!i.details&&!i.details.live;if(t!==this.trackId||i!==r||!n){this.log("Switching to subtitle-track "+t+(i?' "'+i.name+'" lang:'+i.lang+" group:"+i.groupId:""));var a=i.id,s=i.groupId,o=void 0===s?"":s,l=i.name,u=i.type,h=i.url;this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:a,groupId:o,name:l,type:u,url:h});var d=this.switchParams(i.url,null==r?void 0:r.details);this.loadPlaylist(d)}}else this.hls.trigger(S.SUBTITLE_TRACK_SWITCH,{id:t})}else this.queuedDefaultTrack=t},s(e,[{key:"subtitleDisplay",get:function(){return this._subtitleDisplay},set:function(t){this._subtitleDisplay=t,this.trackId>-1&&this.toggleTrackModes()}},{key:"allSubtitleTracks",get:function(){return this.tracks}},{key:"subtitleTracks",get:function(){return this.tracksInGroup}},{key:"subtitleTrack",get:function(){return this.trackId},set:function(t){this.selectDefaultTrack=!1,this.setSubtitleTrack(t)}}]),e}(Dr),Xn=function(){function t(t){this.buffers=void 0,this.queues={video:[],audio:[],audiovideo:[]},this.buffers=t}var e=t.prototype;return e.append=function(t,e,r){var i=this.queues[e];i.push(t),1!==i.length||r||this.executeNext(e)},e.insertAbort=function(t,e){this.queues[e].unshift(t),this.executeNext(e)},e.appendBlocker=function(t){var e,r=new Promise((function(t){e=t})),i={execute:e,onStart:function(){},onComplete:function(){},onError:function(){}};return this.append(i,t),r},e.executeNext=function(t){var e=this.queues[t];if(e.length){var r=e[0];try{r.execute()}catch(e){w.warn('[buffer-operation-queue]: Exception executing "'+t+'" SourceBuffer operation: '+e),r.onError(e);var i=this.buffers[t];null!=i&&i.updating||this.shiftAndExecuteNext(t)}}},e.shiftAndExecuteNext=function(t){this.queues[t].shift(),this.executeNext(t)},e.current=function(t){return this.queues[t][0]},t}(),zn=/(avc[1234]|hvc1|hev1|dvh[1e]|vp09|av01)(?:\.[^.,]+)+/,Qn=function(){function t(t){var e=this;this.details=null,this._objectUrl=null,this.operationQueue=void 0,this.listeners=void 0,this.hls=void 0,this.bufferCodecEventsExpected=0,this._bufferCodecEventsTotal=0,this.media=null,this.mediaSource=null,this.lastMpegAudioChunk=null,this.appendSource=void 0,this.appendErrors={audio:0,video:0,audiovideo:0},this.tracks={},this.pendingTracks={},this.sourceBuffer=void 0,this.log=void 0,this.warn=void 0,this.error=void 0,this._onEndStreaming=function(t){e.hls&&e.hls.pauseBuffering()},this._onStartStreaming=function(t){e.hls&&e.hls.resumeBuffering()},this._onMediaSourceOpen=function(){var t=e.media,r=e.mediaSource;e.log("Media source opened"),t&&(t.removeEventListener("emptied",e._onMediaEmptied),e.updateMediaElementDuration(),e.hls.trigger(S.MEDIA_ATTACHED,{media:t,mediaSource:r})),r&&r.removeEventListener("sourceopen",e._onMediaSourceOpen),e.checkPendingTracks()},this._onMediaSourceClose=function(){e.log("Media source closed")},this._onMediaSourceEnded=function(){e.log("Media source ended")},this._onMediaEmptied=function(){var t=e.mediaSrc,r=e._objectUrl;t!==r&&w.error("Media element src was set while attaching MediaSource ("+r+" > "+t+")")},this.hls=t;var r="[buffer-controller]";this.appendSource=t.config.preferManagedMediaSource,this.log=w.log.bind(w,r),this.warn=w.warn.bind(w,r),this.error=w.error.bind(w,r),this._initSourceBuffer(),this.registerListeners()}var e=t.prototype;return e.hasSourceTypes=function(){return this.getSourceBufferTypes().length>0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null,this.lastMpegAudioChunk=null,this.hls=null},e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.BUFFER_RESET,this.onBufferReset,this),t.on(S.BUFFER_APPENDING,this.onBufferAppending,this),t.on(S.BUFFER_CODECS,this.onBufferCodecs,this),t.on(S.BUFFER_EOS,this.onBufferEos,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(S.FRAG_PARSED,this.onFragParsed,this),t.on(S.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.BUFFER_RESET,this.onBufferReset,this),t.off(S.BUFFER_APPENDING,this.onBufferAppending,this),t.off(S.BUFFER_CODECS,this.onBufferCodecs,this),t.off(S.BUFFER_EOS,this.onBufferEos,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(S.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(S.FRAG_PARSED,this.onFragParsed,this),t.off(S.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new Xn(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]},this.appendErrors={audio:0,video:0,audiovideo:0},this.lastMpegAudioChunk=null},e.onManifestLoading=function(){this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=0,this.details=null},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,this.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media,i=te(this.appendSource);if(r&&i){var n,a=this.mediaSource=new i;this.log("created media source: "+(null==(n=a.constructor)?void 0:n.name)),a.addEventListener("sourceopen",this._onMediaSourceOpen),a.addEventListener("sourceended",this._onMediaSourceEnded),a.addEventListener("sourceclose",this._onMediaSourceClose),a.addEventListener("startstreaming",this._onStartStreaming),a.addEventListener("endstreaming",this._onEndStreaming);var s=this._objectUrl=self.URL.createObjectURL(a);if(this.appendSource)try{r.removeAttribute("src");var o=self.ManagedMediaSource;r.disableRemotePlayback=r.disableRemotePlayback||o&&a instanceof o,Jn(r),function(t,e){var r=self.document.createElement("source");r.type="video/mp4",r.src=e,t.appendChild(r)}(r,s),r.load()}catch(t){r.src=s}else r.src=s;r.addEventListener("emptied",this._onMediaEmptied)}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(this.log("media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){this.warn("onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),e.removeEventListener("startstreaming",this._onStartStreaming),e.removeEventListener("endstreaming",this._onEndStreaming),t&&(t.removeEventListener("emptied",this._onMediaEmptied),r&&self.URL.revokeObjectURL(r),this.mediaSrc===r?(t.removeAttribute("src"),this.appendSource&&Jn(t),t.load()):this.warn("media|source.src was changed by a third party - skip cleanup")),this.mediaSource=null,this.media=null,this._objectUrl=null,this.bufferCodecEventsExpected=this._bufferCodecEventsTotal,this.pendingTracks={},this.tracks={}}this.hls.trigger(S.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){t.resetBuffer(e)})),this._initSourceBuffer()},e.resetBuffer=function(t){var e=this.sourceBuffer[t];try{var r;e&&(this.removeBufferListeners(t),this.sourceBuffer[t]=void 0,null!=(r=this.mediaSource)&&r.sourceBuffers.length&&this.mediaSource.removeSourceBuffer(e))}catch(e){this.warn("onBufferReset "+t,e)}},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length,n=Object.keys(e);if(n.forEach((function(t){if(i){var n=r.tracks[t];if(n&&"function"==typeof n.buffer.changeType){var a,s=e[t],o=s.id,l=s.codec,u=s.levelCodec,h=s.container,d=s.metadata,c=he(n.codec,n.levelCodec),f=null==c?void 0:c.replace(zn,"$1"),g=he(l,u),v=null==(a=g)?void 0:a.replace(zn,"$1");if(g&&f!==v){"audio"===t.slice(0,5)&&(g=ue(g,r.hls.config.preferManagedMediaSource));var m=h+";codecs="+g;r.appendChangeType(t,m),r.log("switching codec "+c+" to "+g),r.tracks[t]={buffer:n.buffer,codec:l,container:h,levelCodec:u,metadata:d,id:o}}}}else r.pendingTracks[t]=e[t]})),!i){var a=Math.max(this.bufferCodecEventsExpected-1,0);this.bufferCodecEventsExpected!==a&&(this.log(a+" bufferCodec event(s) expected "+n.join(",")),this.bufferCodecEventsExpected=a),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks()}},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,n={execute:function(){var n=r.sourceBuffer[t];n&&(r.log("changing "+t+" sourceBuffer type to "+e),n.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){r.warn("Failed to change "+t+" SourceBuffer type",e)}};i.append(n,t,!!this.pendingTracks[t])},e.onBufferAppending=function(t,e){var r=this,i=this.hls,n=this.operationQueue,a=this.tracks,s=e.data,o=e.type,l=e.frag,u=e.part,h=e.chunkMeta,d=h.buffering[o],c=self.performance.now();d.start=c;var f=l.stats.buffering,g=u?u.stats.buffering:null;0===f.start&&(f.start=c),g&&0===g.start&&(g.start=c);var v=a.audio,m=!1;"audio"===o&&"audio/mpeg"===(null==v?void 0:v.container)&&(m=!this.lastMpegAudioChunk||1===h.id||this.lastMpegAudioChunk.sn!==h.sn,this.lastMpegAudioChunk=h);var p=l.start,y={execute:function(){if(d.executeStart=self.performance.now(),m){var t=r.sourceBuffer[o];if(t){var e=p-t.timestampOffset;Math.abs(e)>=.1&&(r.log("Updating audio SourceBuffer timestampOffset to "+p+" (delta: "+e+") sn: "+l.sn+")"),t.timestampOffset=p)}}r.appendExecutor(s,o)},onStart:function(){},onComplete:function(){var t=self.performance.now();d.executeEnd=d.end=t,0===f.first&&(f.first=t),g&&0===g.first&&(g.first=t);var e=r.sourceBuffer,i={};for(var n in e)i[n]=zr.getBuffered(e[n]);r.appendErrors[o]=0,"audio"===o||"video"===o?r.appendErrors.audiovideo=0:(r.appendErrors.audio=0,r.appendErrors.video=0),r.hls.trigger(S.BUFFER_APPENDED,{type:o,frag:l,part:u,chunkMeta:h,parent:l.type,timeRanges:i})},onError:function(t){var e={type:L.MEDIA_ERROR,parent:l.type,details:A.BUFFER_APPEND_ERROR,sourceBufferName:o,frag:l,part:u,chunkMeta:h,error:t,err:t,fatal:!1};if(t.code===DOMException.QUOTA_EXCEEDED_ERR)e.details=A.BUFFER_FULL_ERROR;else{var n=++r.appendErrors[o];e.details=A.BUFFER_APPEND_ERROR,r.warn("Failed "+n+"/"+i.config.appendErrorMaxRetry+' times to append segment in "'+o+'" sourceBuffer'),n>=i.config.appendErrorMaxRetry&&(e.fatal=!0)}i.trigger(S.ERROR,e)}};n.append(y,o,!!this.pendingTracks[o])},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,n=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(S.BUFFER_FLUSHED,{type:t})},onError:function(e){r.warn("Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(n(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(n(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,n=e.part,a=[],s=n?n.elementaryStreams:i.elementaryStreams;s[U]?a.push("audiovideo"):(s[O]&&a.push("audio"),s[N]&&a.push("video")),0===a.length&&this.warn("Fragments must have at least one ElementaryStreamType set. type: "+i.type+" level: "+i.level+" sn: "+i.sn),this.blockBuffers((function(){var t=self.performance.now();i.stats.buffering.end=t,n&&(n.stats.buffering.end=t);var e=n?n.stats:i.stats;r.hls.trigger(S.FRAG_BUFFERED,{frag:i,part:n,stats:e,id:i.type})}),a)},e.onFragChanged=function(t,e){this.trimBuffers()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var n=r.sourceBuffer[i];return!n||e.type&&e.type!==i||(n.ending=!0,n.ended||(n.ended=!0,r.log(i+" sourceBuffer now EOS"))),t&&!(n&&!n.ended)}),!0)&&(this.log("Queueing mediaSource.endOfStream()"),this.blockBuffers((function(){r.getSourceBufferTypes().forEach((function(t){var e=r.sourceBuffer[t];e&&(e.ending=!1)}));var t=r.mediaSource;t&&"open"===t.readyState?(r.log("Calling mediaSource.endOfStream()"),t.endOfStream()):t&&r.log("Could not call mediaSource.endOfStream(). mediaSource.readyState: "+t.readyState)})))},e.onLevelUpdated=function(t,e){var r=e.details;r.fragments.length&&(this.details=r,this.getSourceBufferTypes().length?this.blockBuffers(this.updateMediaElementDuration.bind(this)):this.updateMediaElementDuration())},e.trimBuffers=function(){var t=this.hls,e=this.details,r=this.media;if(r&&null!==e&&this.getSourceBufferTypes().length){var i=t.config,n=r.currentTime,a=e.levelTargetDuration,s=e.live&&null!==i.liveBackBufferLength?i.liveBackBufferLength:i.backBufferLength;if(y(s)&&s>0){var o=Math.max(s,a),l=Math.floor(n/a)*a-o;this.flushBackBuffer(n,a,l)}if(y(i.frontBufferFlushThreshold)&&i.frontBufferFlushThreshold>0){var u=Math.max(i.maxBufferLength,i.frontBufferFlushThreshold),h=Math.max(u,a),d=Math.floor(n/a)*a+h;this.flushFrontBuffer(n,a,d)}}},e.flushBackBuffer=function(t,e,r){var i=this,n=this.details,a=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(s){var o=a[s];if(o){var l=zr.getBuffered(o);if(l.length>0&&r>l.start(0)){if(i.hls.trigger(S.BACK_BUFFER_REACHED,{bufferEnd:r}),null!=n&&n.live)i.hls.trigger(S.LIVE_BACK_BUFFER_REACHED,{bufferEnd:r});else if(o.ended&&l.end(l.length-1)-t<2*e)return void i.log("Cannot flush "+s+" back buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:r,type:s})}}}))},e.flushFrontBuffer=function(t,e,r){var i=this,n=this.sourceBuffer;this.getSourceBufferTypes().forEach((function(a){var s=n[a];if(s){var o=zr.getBuffered(s),l=o.length;if(l<2)return;var u=o.start(l-1),h=o.end(l-1);if(r>u||t>=u&&t<=h)return;if(s.ended&&t-h<2*e)return void i.log("Cannot flush "+a+" front buffer while SourceBuffer is in ended state");i.hls.trigger(S.BUFFER_FLUSHING,{startOffset:u,endOffset:1/0,type:a})}}))},e.updateMediaElementDuration=function(){if(this.details&&this.media&&this.mediaSource&&"open"===this.mediaSource.readyState){var t=this.details,e=this.hls,r=this.media,i=this.mediaSource,n=t.fragments[0].start+t.totalduration,a=r.duration,s=y(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(i.duration=1/0,this.updateSeekableRange(t)):(n>s&&n>a||!y(a))&&(this.log("Updating Media Source duration to "+n.toFixed(3)),i.duration=n)}},e.updateSeekableRange=function(t){var e=this.mediaSource,r=t.fragments;if(r.length&&t.live&&null!=e&&e.setLiveSeekableRange){var i=Math.max(0,r[0].start),n=Math.max(i,i+t.totalduration);this.log("Media Source duration is set to "+e.duration+". Setting seekable range to "+i+"-"+n+"."),e.setLiveSeekableRange(i,n)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&(!t||2===i||"audiovideo"in r)){this.createSourceBuffers(r),this.pendingTracks={};var n=this.getSourceBufferTypes();if(n.length)this.hls.trigger(S.BUFFER_CREATED,{tracks:this.tracks}),n.forEach((function(t){e.executeNext(t)}));else{var a=new Error("could not create source buffer for media codec(s)");this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,error:a,reason:a.message})}}},e.createSourceBuffers=function(t){var e=this,r=this.sourceBuffer,i=this.mediaSource;if(!i)throw Error("createSourceBuffers called when mediaSource was null");var n=function(n){if(!r[n]){var a=t[n];if(!a)throw Error("source buffer exists for track "+n+", however track does not");var s=a.levelCodec||a.codec;s&&"audio"===n.slice(0,5)&&(s=ue(s,e.hls.config.preferManagedMediaSource));var o=a.container+";codecs="+s;e.log("creating sourceBuffer("+o+")");try{var l=r[n]=i.addSourceBuffer(o),u=n;e.addBufferListener(u,"updatestart",e._onSBUpdateStart),e.addBufferListener(u,"updateend",e._onSBUpdateEnd),e.addBufferListener(u,"error",e._onSBUpdateError),e.addBufferListener(u,"bufferedchange",(function(t,r){var i=r.removedRanges;null!=i&&i.length&&e.hls.trigger(S.BUFFER_FLUSHED,{type:n})})),e.tracks[n]={buffer:l,codec:s,container:a.container,levelCodec:a.levelCodec,metadata:a.metadata,id:a.id}}catch(t){e.error("error while trying to add sourceBuffer: "+t.message),e.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,sourceBufferName:n,mimeType:o})}}};for(var a in t)n(a)},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e;if("closed"!==(null==(e=this.mediaSource)?void 0:e.readyState)){var r=this.operationQueue;r.current(t).onComplete(),r.shiftAndExecuteNext(t)}else this.resetBuffer(t)},e._onSBUpdateError=function(t,e){var r,i=new Error(t+" SourceBuffer error. MediaSource readyState: "+(null==(r=this.mediaSource)?void 0:r.readyState));this.error(""+i,e),this.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_APPENDING_ERROR,sourceBufferName:t,error:i,fatal:!1});var n=this.operationQueue.current(t);n&&n.onError(i)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,a=this.operationQueue,s=this.sourceBuffer[t];if(!i||!n||!s)return this.warn("Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void a.shiftAndExecuteNext(t);var o=y(i.duration)?i.duration:1/0,l=y(n.duration)?n.duration:1/0,u=Math.max(0,e),h=Math.min(r,o,l);h>u&&(!s.ending||s.ended)?(s.ended=!1,this.log("Removing ["+u+","+h+"] from the "+t+" SourceBuffer"),s.remove(u,h)):a.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.sourceBuffer[e];if(r)r.ended=!1,r.appendBuffer(t);else if(!this.pendingTracks[e])throw new Error("Attempting to append to the "+e+" SourceBuffer, but it does not exist")},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return this.log("Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve().then(t);var i=this.operationQueue,n=e.map((function(t){return i.appendBlocker(t)}));Promise.all(n).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];null!=e&&e.updating||i.shiftAndExecuteNext(t)}))}))},e.getSourceBufferTypes=function(){return Object.keys(this.sourceBuffer)},e.addBufferListener=function(t,e,r){var i=this.sourceBuffer[t];if(i){var n=r.bind(this,t);this.listeners[t].push({event:e,listener:n}),i.addEventListener(e,n)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},s(t,[{key:"mediaSrc",get:function(){var t,e=(null==(t=this.media)?void 0:t.firstChild)||this.media;return null==e?void 0:e.src}}]),t}();function Jn(t){var e=t.querySelectorAll("source");[].slice.call(e).forEach((function(e){t.removeChild(e)}))}var $n={42:225,92:233,94:237,95:243,96:250,123:231,124:247,125:209,126:241,127:9608,128:174,129:176,130:189,131:191,132:8482,133:162,134:163,135:9834,136:224,137:32,138:232,139:226,140:234,141:238,142:244,143:251,144:193,145:201,146:211,147:218,148:220,149:252,150:8216,151:161,152:42,153:8217,154:9473,155:169,156:8480,157:8226,158:8220,159:8221,160:192,161:194,162:199,163:200,164:202,165:203,166:235,167:206,168:207,169:239,170:212,171:217,172:249,173:219,174:171,175:187,176:195,177:227,178:205,179:204,180:236,181:210,182:242,183:213,184:245,185:123,186:125,187:92,188:94,189:95,190:124,191:8764,192:196,193:228,194:214,195:246,196:223,197:165,198:164,199:9475,200:197,201:229,202:216,203:248,204:9487,205:9491,206:9495,207:9499},Zn=function(t){var e=t;return $n.hasOwnProperty(t)&&(e=$n[t]),String.fromCharCode(e)},ta=15,ea=100,ra={17:1,18:3,21:5,22:7,23:9,16:11,19:12,20:14},ia={17:2,18:4,21:6,22:8,23:10,19:13,20:15},na={25:1,26:3,29:5,30:7,31:9,24:11,27:12,28:14},aa={25:2,26:4,29:6,30:8,31:10,27:13,28:15},sa=["white","green","blue","cyan","red","yellow","magenta","black","transparent"],oa=function(){function t(){this.time=null,this.verboseLevel=0}return t.prototype.log=function(t,e){if(this.verboseLevel>=t){var r="function"==typeof e?e():e;w.log(this.time+" ["+t+"] "+r)}},t}(),la=function(t){for(var e=[],r=0;rea&&(this.logger.log(3,"Too large cursor position "+this.pos),this.pos=ea)},e.moveCursor=function(t){var e=this.pos+t;if(t>1)for(var r=this.pos+1;r=144&&this.backSpace();var r=Zn(t);this.pos>=ea?this.logger.log(0,(function(){return"Cannot insert "+t.toString(16)+" ("+r+") at position "+e.pos+". Skipping it!"})):(this.chars[this.pos].setChar(r,this.currPenState),this.moveCursor(1))},e.clearFromPos=function(t){var e;for(e=t;e0&&(r=t?"["+e.join(" | ")+"]":e.join("\n")),r},e.getTextAndFormat=function(){return this.rows},t}(),fa=function(){function t(t,e,r){this.chNr=void 0,this.outputFilter=void 0,this.mode=void 0,this.verbose=void 0,this.displayedMemory=void 0,this.nonDisplayedMemory=void 0,this.lastOutputScreen=void 0,this.currRollUpRow=void 0,this.writeScreen=void 0,this.cueStartTime=void 0,this.logger=void 0,this.chNr=t,this.outputFilter=e,this.mode=null,this.verbose=0,this.displayedMemory=new ca(r),this.nonDisplayedMemory=new ca(r),this.lastOutputScreen=new ca(r),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null,this.logger=r}var e=t.prototype;return e.reset=function(){this.mode=null,this.displayedMemory.reset(),this.nonDisplayedMemory.reset(),this.lastOutputScreen.reset(),this.outputFilter.reset(),this.currRollUpRow=this.displayedMemory.rows[14],this.writeScreen=this.displayedMemory,this.mode=null,this.cueStartTime=null},e.getHandler=function(){return this.outputFilter},e.setHandler=function(t){this.outputFilter=t},e.setPAC=function(t){this.writeScreen.setPAC(t)},e.setBkgData=function(t){this.writeScreen.setBkgData(t)},e.setMode=function(t){t!==this.mode&&(this.mode=t,this.logger.log(2,(function(){return"MODE="+t})),"MODE_POP-ON"===this.mode?this.writeScreen=this.nonDisplayedMemory:(this.writeScreen=this.displayedMemory,this.writeScreen.reset()),"MODE_ROLL-UP"!==this.mode&&(this.displayedMemory.nrRollUpRows=null,this.nonDisplayedMemory.nrRollUpRows=null),this.mode=t)},e.insertChars=function(t){for(var e=this,r=0;r=46,e.italics)e.foreground="white";else{var r=Math.floor(t/2)-16;e.foreground=["white","green","blue","cyan","red","yellow","magenta"][r]}this.logger.log(2,"MIDROW: "+JSON.stringify(e)),this.writeScreen.setPen(e)},e.outputDataUpdate=function(t){void 0===t&&(t=!1);var e=this.logger.time;null!==e&&this.outputFilter&&(null!==this.cueStartTime||this.displayedMemory.isEmpty()?this.displayedMemory.equals(this.lastOutputScreen)||(this.outputFilter.newCue(this.cueStartTime,e,this.lastOutputScreen),t&&this.outputFilter.dispatchCue&&this.outputFilter.dispatchCue(),this.cueStartTime=this.displayedMemory.isEmpty()?null:e):this.cueStartTime=e,this.lastOutputScreen.copy(this.displayedMemory))},e.cueSplitAtTime=function(t){this.outputFilter&&(this.displayedMemory.isEmpty()||(this.outputFilter.newCue&&this.outputFilter.newCue(this.cueStartTime,t,this.displayedMemory),this.cueStartTime=t))},t}(),ga=function(){function t(t,e,r){this.channels=void 0,this.currentChannel=0,this.cmdHistory={a:null,b:null},this.logger=void 0;var i=this.logger=new oa;this.channels=[null,new fa(t,e,i),new fa(t+1,r,i)]}var e=t.prototype;return e.getHandler=function(t){return this.channels[t].getHandler()},e.setHandler=function(t,e){this.channels[t].setHandler(e)},e.addData=function(t,e){var r,i,n,a=!1;this.logger.time=t;for(var s=0;s ("+la([i,n])+")"),(r=this.parseCmd(i,n))||(r=this.parseMidrow(i,n)),r||(r=this.parsePAC(i,n)),r||(r=this.parseBackgroundAttributes(i,n)),!r&&(a=this.parseChars(i,n))){var o=this.currentChannel;o&&o>0?this.channels[o].insertChars(a):this.logger.log(2,"No channel found yet. TEXT-MODE?")}r||a||this.logger.log(2,"Couldn't parse cleaned data "+la([i,n])+" orig: "+la([e[s],e[s+1]]))}},e.parseCmd=function(t,e){var r=this.cmdHistory;if(!((20===t||28===t||21===t||29===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=33&&e<=35))return!1;if(ma(t,e,r))return va(null,null,r),this.logger.log(3,"Repeated command ("+la([t,e])+") is dropped"),!0;var i=20===t||21===t||23===t?1:2,n=this.channels[i];return 20===t||21===t||28===t||29===t?32===e?n.ccRCL():33===e?n.ccBS():34===e?n.ccAOF():35===e?n.ccAON():36===e?n.ccDER():37===e?n.ccRU(2):38===e?n.ccRU(3):39===e?n.ccRU(4):40===e?n.ccFON():41===e?n.ccRDC():42===e?n.ccTR():43===e?n.ccRTD():44===e?n.ccEDM():45===e?n.ccCR():46===e?n.ccENM():47===e&&n.ccEOC():n.ccTO(e-32),va(t,e,r),this.currentChannel=i,!0},e.parseMidrow=function(t,e){var r=0;if((17===t||25===t)&&e>=32&&e<=47){if((r=17===t?1:2)!==this.currentChannel)return this.logger.log(0,"Mismatch channel in midrow parsing"),!1;var i=this.channels[r];return!!i&&(i.ccMIDROW(e),this.logger.log(3,"MIDROW ("+la([t,e])+")"),!0)}return!1},e.parsePAC=function(t,e){var r,i=this.cmdHistory;if(!((t>=17&&t<=23||t>=25&&t<=31)&&e>=64&&e<=127||(16===t||24===t)&&e>=64&&e<=95))return!1;if(ma(t,e,i))return va(null,null,i),!0;var n=t<=23?1:2;r=e>=64&&e<=95?1===n?ra[t]:na[t]:1===n?ia[t]:aa[t];var a=this.channels[n];return!!a&&(a.setPAC(this.interpretPAC(r,e)),va(t,e,i),this.currentChannel=n,!0)},e.interpretPAC=function(t,e){var r,i={color:null,italics:!1,indent:null,underline:!1,row:t};return r=e>95?e-96:e-64,i.underline=1==(1&r),r<=13?i.color=["white","green","blue","cyan","red","yellow","magenta","white"][Math.floor(r/2)]:r<=15?(i.italics=!0,i.color="white"):i.indent=4*Math.floor((r-16)/2),i},e.parseChars=function(t,e){var r,i,n=null,a=null;if(t>=25?(r=2,a=t-8):(r=1,a=t),a>=17&&a<=19?(i=17===a?e+80:18===a?e+112:e+144,this.logger.log(2,"Special char '"+Zn(i)+"' in channel "+r),n=[i]):t>=32&&t<=127&&(n=0===e?[t]:[t,e]),n){var s=la(n);this.logger.log(3,"Char codes = "+s.join(",")),va(t,e,this.cmdHistory)}return n},e.parseBackgroundAttributes=function(t,e){var r;if(!((16===t||24===t)&&e>=32&&e<=47||(23===t||31===t)&&e>=45&&e<=47))return!1;var i={};16===t||24===t?(r=Math.floor((e-32)/2),i.background=sa[r],e%2==1&&(i.background=i.background+"_semi")):45===e?i.background="transparent":(i.foreground="black",47===e&&(i.underline=!0));var n=t<=23?1:2;return this.channels[n].setBkgData(i),va(t,e,this.cmdHistory),!0},e.reset=function(){for(var t=0;tt)&&(this.startTime=t),this.endTime=e,this.screen=r,this.timelineController.createCaptionsTrack(this.trackName)},e.reset=function(){this.cueRanges=[],this.startTime=null},t}(),ya=function(){if(null!=j&&j.VTTCue)return self.VTTCue;var t=["","lr","rl"],e=["start","middle","end","left","right"];function r(t,e){if("string"!=typeof e)return!1;if(!Array.isArray(t))return!1;var r=e.toLowerCase();return!!~t.indexOf(r)&&r}function i(t){return r(e,t)}function n(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i100)throw new Error("Position must be between 0 and 100.");E=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"positionAlign",n({},l,{get:function(){return T},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");T=e,this.hasBeenReset=!0}})),Object.defineProperty(o,"size",n({},l,{get:function(){return S},set:function(t){if(t<0||t>100)throw new Error("Size must be between 0 and 100.");S=t,this.hasBeenReset=!0}})),Object.defineProperty(o,"align",n({},l,{get:function(){return L},set:function(t){var e=i(t);if(!e)throw new SyntaxError("An invalid or illegal string was specified.");L=e,this.hasBeenReset=!0}})),o.displayState=void 0}return a.prototype.getCueAsHTML=function(){return self.WebVTT.convertCueToDOMTree(self,this.text)},a}(),Ea=function(){function t(){}return t.prototype.decode=function(t,e){if(!t)return"";if("string"!=typeof t)throw new Error("Error - expected string data.");return decodeURIComponent(encodeURIComponent(t))},t}();function Ta(t){function e(t,e,r,i){return 3600*(0|t)+60*(0|e)+(0|r)+parseFloat(i||0)}var r=t.match(/^(?:(\d+):)?(\d{2}):(\d{2})(\.\d+)?/);return r?parseFloat(r[2])>59?e(r[2],r[3],0,r[4]):e(r[1],r[2],r[3],r[4]):null}var Sa=function(){function t(){this.values=Object.create(null)}var e=t.prototype;return e.set=function(t,e){this.get(t)||""===e||(this.values[t]=e)},e.get=function(t,e,r){return r?this.has(t)?this.values[t]:e[r]:this.has(t)?this.values[t]:e},e.has=function(t){return t in this.values},e.alt=function(t,e,r){for(var i=0;i=0&&r<=100)return this.set(t,r),!0}return!1},t}();function La(t,e,r,i){var n=i?t.split(i):[t];for(var a in n)if("string"==typeof n[a]){var s=n[a].split(r);2===s.length&&e(s[0],s[1])}}var Aa=new ya(0,0,""),Ra="middle"===Aa.align?"middle":"center";function ka(t,e,r){var i=t;function n(){var e=Ta(t);if(null===e)throw new Error("Malformed timestamp: "+i);return t=t.replace(/^[^\sa-zA-Z-]+/,""),e}function a(){t=t.replace(/^\s+/,"")}if(a(),e.startTime=n(),a(),"--\x3e"!==t.slice(0,3))throw new Error("Malformed time stamp (time stamps must be separated by '--\x3e'): "+i);t=t.slice(3),a(),e.endTime=n(),a(),function(t,e){var i=new Sa;La(t,(function(t,e){var n;switch(t){case"region":for(var a=r.length-1;a>=0;a--)if(r[a].id===e){i.set(t,r[a].region);break}break;case"vertical":i.alt(t,e,["rl","lr"]);break;case"line":n=e.split(","),i.integer(t,n[0]),i.percent(t,n[0])&&i.set("snapToLines",!1),i.alt(t,n[0],["auto"]),2===n.length&&i.alt("lineAlign",n[1],["start",Ra,"end"]);break;case"position":n=e.split(","),i.percent(t,n[0]),2===n.length&&i.alt("positionAlign",n[1],["start",Ra,"end","line-left","line-right","auto"]);break;case"size":i.percent(t,e);break;case"align":i.alt(t,e,["start",Ra,"end","left","right"])}}),/:/,/\s/),e.region=i.get("region",null),e.vertical=i.get("vertical","");var n=i.get("line","auto");"auto"===n&&-1===Aa.line&&(n=-1),e.line=n,e.lineAlign=i.get("lineAlign","start"),e.snapToLines=i.get("snapToLines",!0),e.size=i.get("size",100),e.align=i.get("align",Ra);var a=i.get("position","auto");"auto"===a&&50===Aa.position&&(a="start"===e.align||"left"===e.align?0:"end"===e.align||"right"===e.align?100:50),e.position=a}(t,e)}function ba(t){return t.replace(//gi,"\n")}var Da=function(){function t(){this.state="INITIAL",this.buffer="",this.decoder=new Ea,this.regionList=[],this.cue=null,this.oncue=void 0,this.onparsingerror=void 0,this.onflush=void 0}var e=t.prototype;return e.parse=function(t){var e=this;function r(){var t=e.buffer,r=0;for(t=ba(t);r>>0).toString()};function _a(t,e,r){return Ca(t.toString())+Ca(e.toString())+Ca(r)}function xa(t,e,r,i,n,a,s){var o,l,u,h=new Da,d=Tt(new Uint8Array(t)).trim().replace(Ia,"\n").split("\n"),c=[],f=e?(o=e.baseTime,void 0===(l=e.timescale)&&(l=1),mn(o,vn,1/l)):0,g="00:00.000",v=0,m=0,p=!0;h.oncue=function(t){var a=r[i],s=r.ccOffset,o=(v-f)/9e4;if(null!=a&&a.new&&(void 0!==m?s=r.ccOffset=a.start:function(t,e,r){var i=t[e],n=t[i.prevCC];if(!n||!n.new&&i.new)return t.ccOffset=t.presentationOffset=i.start,void(i.new=!1);for(;null!=(a=n)&&a.new;){var a;t.ccOffset+=i.start-n.start,i.new=!1,n=t[(i=n).prevCC]}t.presentationOffset=r}(r,i,o)),o){if(!e)return void(u=new Error("Missing initPTS for VTT MPEGTS"));s=o-r.presentationOffset}var l=t.endTime-t.startTime,h=Sn(9e4*(t.startTime+s-m),9e4*n)/9e4;t.startTime=Math.max(h,0),t.endTime=Math.max(h+l,0);var d=t.text.trim();t.text=decodeURIComponent(encodeURIComponent(d)),t.id||(t.id=_a(t.startTime,t.endTime,d)),t.endTime>0&&c.push(t)},h.onparsingerror=function(t){u=t},h.onflush=function(){u?s(u):a(c)},d.forEach((function(t){if(p){if(wa(t,"X-TIMESTAMP-MAP=")){p=!1,t.slice(16).split(",").forEach((function(t){wa(t,"LOCAL:")?g=t.slice(6):wa(t,"MPEGTS:")&&(v=parseInt(t.slice(7)))}));try{m=function(t){var e=parseInt(t.slice(-3)),r=parseInt(t.slice(-6,-4)),i=parseInt(t.slice(-9,-7)),n=t.length>9?parseInt(t.substring(0,t.indexOf(":"))):0;if(!(y(e)&&y(r)&&y(i)&&y(n)))throw Error("Malformed X-TIMESTAMP-MAP: Local:"+t);return e+=1e3*r,(e+=6e4*i)+36e5*n}(g)/1e3}catch(t){u=t}return}""===t&&(p=!1)}h.parse(t+"\n")})),h.flush()}var Pa="stpp.ttml.im1t",Fa=/^(\d{2,}):(\d{2}):(\d{2}):(\d{2})\.?(\d+)?$/,Ma=/^(\d*(?:\.\d*)?)(h|m|s|ms|f|t)$/,Oa={left:"start",center:"center",right:"end",start:"start",end:"end"};function Na(t,e,r,i){var n=_t(new Uint8Array(t),["mdat"]);if(0!==n.length){var a,s,l,u,h=n.map((function(t){return Tt(t)})),d=(a=e.baseTime,s=1,void 0===(l=e.timescale)&&(l=1),void 0===u&&(u=!1),mn(a,s,1/l,u));try{h.forEach((function(t){return r(function(t,e){var r=(new DOMParser).parseFromString(t,"text/xml"),i=r.getElementsByTagName("tt")[0];if(!i)throw new Error("Invalid ttml");var n={frameRate:30,subFrameRate:1,frameRateMultiplier:0,tickRate:0},a=Object.keys(n).reduce((function(t,e){return t[e]=i.getAttribute("ttp:"+e)||n[e],t}),{}),s="preserve"!==i.getAttribute("xml:space"),l=Ba(Ua(i,"styling","style")),u=Ba(Ua(i,"layout","region")),h=Ua(i,"body","[begin]");return[].map.call(h,(function(t){var r=Ga(t,s);if(!r||!t.hasAttribute("begin"))return null;var i=Va(t.getAttribute("begin"),a),n=Va(t.getAttribute("dur"),a),h=Va(t.getAttribute("end"),a);if(null===i)throw Ha(t);if(null===h){if(null===n)throw Ha(t);h=i+n}var d=new ya(i-e,h-e,r);d.id=_a(d.startTime,d.endTime,d.text);var c=function(t,e,r){var i="http://www.w3.org/ns/ttml#styling",n=null,a=["displayAlign","textAlign","color","backgroundColor","fontSize","fontFamily"],s=null!=t&&t.hasAttribute("style")?t.getAttribute("style"):null;return s&&r.hasOwnProperty(s)&&(n=r[s]),a.reduce((function(r,a){var s=Ka(e,i,a)||Ka(t,i,a)||Ka(n,i,a);return s&&(r[a]=s),r}),{})}(u[t.getAttribute("region")],l[t.getAttribute("style")],l),f=c.textAlign;if(f){var g=Oa[f];g&&(d.lineAlign=g),d.align=f}return o(d,c),d})).filter((function(t){return null!==t}))}(t,d))}))}catch(t){i(t)}}else i(new Error("Could not parse IMSC1 mdat"))}function Ua(t,e,r){var i=t.getElementsByTagName(e)[0];return i?[].slice.call(i.querySelectorAll(r)):[]}function Ba(t){return t.reduce((function(t,e){var r=e.getAttribute("xml:id");return r&&(t[r]=e),t}),{})}function Ga(t,e){return[].slice.call(t.childNodes).reduce((function(t,r,i){var n;return"br"===r.nodeName&&i?t+"\n":null!=(n=r.childNodes)&&n.length?Ga(r,e):e?t+r.textContent.trim().replace(/\s+/g," "):t+r.textContent}),"")}function Ka(t,e,r){return t&&t.hasAttributeNS(e,r)?t.getAttributeNS(e,r):null}function Ha(t){return new Error("Could not parse ttml timestamp "+t)}function Va(t,e){if(!t)return null;var r=Ta(t);return null===r&&(Fa.test(t)?r=function(t,e){var r=Fa.exec(t),i=(0|r[4])+(0|r[5])/e.subFrameRate;return 3600*(0|r[1])+60*(0|r[2])+(0|r[3])+i/e.frameRate}(t,e):Ma.test(t)&&(r=function(t,e){var r=Ma.exec(t),i=Number(r[1]);switch(r[2]){case"h":return 3600*i;case"m":return 60*i;case"ms":return 1e3*i;case"f":return i/e.frameRate;case"t":return i/e.tickRate}return i}(t,e))),r}var Ya=function(){function t(t){this.hls=void 0,this.media=null,this.config=void 0,this.enabled=!0,this.Cues=void 0,this.textTracks=[],this.tracks=[],this.initPTS=[],this.unparsedVttFrags=[],this.captionsTracks={},this.nonNativeCaptionsTracks={},this.cea608Parser1=void 0,this.cea608Parser2=void 0,this.lastCc=-1,this.lastSn=-1,this.lastPartIndex=-1,this.prevCC=-1,this.vttCCs={ccOffset:0,presentationOffset:0,0:{start:0,prevCC:-1,new:!0}},this.captionsProperties=void 0,this.hls=t,this.config=t.config,this.Cues=t.config.cueHandler,this.captionsProperties={textTrack1:{label:this.config.captionsTextTrack1Label,languageCode:this.config.captionsTextTrack1LanguageCode},textTrack2:{label:this.config.captionsTextTrack2Label,languageCode:this.config.captionsTextTrack2LanguageCode},textTrack3:{label:this.config.captionsTextTrack3Label,languageCode:this.config.captionsTextTrack3LanguageCode},textTrack4:{label:this.config.captionsTextTrack4Label,languageCode:this.config.captionsTextTrack4LanguageCode}},t.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.on(S.FRAG_LOADING,this.onFragLoading,this),t.on(S.FRAG_LOADED,this.onFragLoaded,this),t.on(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.on(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.on(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.on(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.on(S.BUFFER_FLUSHING,this.onBufferFlushing,this)}var e=t.prototype;return e.destroy=function(){var t=this.hls;t.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(S.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.SUBTITLE_TRACKS_UPDATED,this.onSubtitleTracksUpdated,this),t.off(S.FRAG_LOADING,this.onFragLoading,this),t.off(S.FRAG_LOADED,this.onFragLoaded,this),t.off(S.FRAG_PARSING_USERDATA,this.onFragParsingUserdata,this),t.off(S.FRAG_DECRYPTED,this.onFragDecrypted,this),t.off(S.INIT_PTS_FOUND,this.onInitPtsFound,this),t.off(S.SUBTITLE_TRACKS_CLEARED,this.onSubtitleTracksCleared,this),t.off(S.BUFFER_FLUSHING,this.onBufferFlushing,this),this.hls=this.config=null,this.cea608Parser1=this.cea608Parser2=void 0},e.initCea608Parsers=function(){if(this.config.enableCEA708Captions&&(!this.cea608Parser1||!this.cea608Parser2)){var t=new pa(this,"textTrack1"),e=new pa(this,"textTrack2"),r=new pa(this,"textTrack3"),i=new pa(this,"textTrack4");this.cea608Parser1=new ga(1,t,e),this.cea608Parser2=new ga(3,r,i)}},e.addCues=function(t,e,r,i,n){for(var a,s,o,l,u=!1,h=n.length;h--;){var d=n[h],c=(a=d[0],s=d[1],o=e,l=r,Math.min(s,l)-Math.max(a,o));if(c>=0&&(d[0]=Math.min(d[0],e),d[1]=Math.max(d[1],r),u=!0,c/(r-e)>.5))return}if(u||n.push([e,r]),this.config.renderTextTracksNatively){var f=this.captionsTracks[t];this.Cues.newCue(f,e,r,i)}else{var g=this.Cues.newCue(null,e,r,i);this.hls.trigger(S.CUES_PARSED,{type:"captions",cues:g,track:t})}},e.onInitPtsFound=function(t,e){var r=this,i=e.frag,n=e.id,a=e.initPTS,s=e.timescale,o=this.unparsedVttFrags;"main"===n&&(this.initPTS[i.cc]={baseTime:a,timescale:s}),o.length&&(this.unparsedVttFrags=[],o.forEach((function(t){r.onFragLoaded(S.FRAG_LOADED,t)})))},e.getExistingTrack=function(t,e){var r=this.media;if(r)for(var i=0;ii.cc||l.trigger(S.SUBTITLE_FRAG_PROCESSED,{success:!1,frag:i,error:e})}))}else s.push(t)},e._fallbackToIMSC1=function(t,e){var r=this,i=this.tracks[t.level];i.textCodec||Na(e,this.initPTS[t.cc],(function(){i.textCodec=Pa,r._parseIMSC1(t,e)}),(function(){i.textCodec="wvtt"}))},e._appendCues=function(t,e){var r=this.hls;if(this.config.renderTextTracksNatively){var i=this.textTracks[e];if(!i||"disabled"===i.mode)return;t.forEach((function(t){return Me(i,t)}))}else{var n=this.tracks[e];if(!n)return;var a=n.default?"default":"subtitles"+e;r.trigger(S.CUES_PARSED,{type:"subtitles",cues:t,track:a})}},e.onFragDecrypted=function(t,e){e.frag.type===Ce&&this.onFragLoaded(S.FRAG_LOADED,e)},e.onSubtitleTracksCleared=function(){this.tracks=[],this.captionsTracks={}},e.onFragParsingUserdata=function(t,e){this.initCea608Parsers();var r=this.cea608Parser1,i=this.cea608Parser2;if(this.enabled&&r&&i){var n=e.frag,a=e.samples;if(n.type!==Ie||"NONE"!==this.closedCaptionsForLevel(n))for(var s=0;sthis.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},e.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var n=i.filter((function(t,i){return r.isLevelAllowed(t)&&i<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(n,this.mediaWidth,this.mediaHeight)},e.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},e.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},e.getDimensions=function(){if(this.clientRect)return this.clientRect;var t=this.media,e={width:0,height:0};if(t){var r=t.getBoundingClientRect();e.width=r.width,e.height=r.height,e.width||e.height||(e.width=r.right-r.left||t.width||0,e.height=r.bottom-r.top||t.height||0)}return this.clientRect=e,e},e.isLevelAllowed=function(t){return!this.restrictedLevels.some((function(e){return t.bitrate===e.bitrate&&t.width===e.width&&t.height===e.height}))},t.getMaxLevelByMediaSize=function(t,e,r){if(null==t||!t.length)return-1;for(var i,n,a=t.length-1,s=Math.max(e,r),o=0;o=s||l.height>=s)&&(i=l,!(n=t[o+1])||i.width!==n.width||i.height!==n.height)){a=o;break}}return a},s(t,[{key:"mediaWidth",get:function(){return this.getDimensions().width*this.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*this.contentScaleFactor}},{key:"contentScaleFactor",get:function(){var t=1;if(!this.hls.config.ignoreDevicePixelRatio)try{t=self.devicePixelRatio}catch(t){}return t}}]),t}(),Xa=function(){function t(t){this.hls=void 0,this.isVideoPlaybackQualityAvailable=!1,this.timer=void 0,this.media=null,this.lastTime=void 0,this.lastDroppedFrames=0,this.lastDecodedFrames=0,this.streamController=void 0,this.hls=t,this.registerListeners()}var e=t.prototype;return e.setStreamController=function(t){this.streamController=t},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.destroy=function(){this.timer&&clearInterval(this.timer),this.unregisterListeners(),this.isVideoPlaybackQualityAvailable=!1,this.media=null},e.onMediaAttaching=function(t,e){var r=this.hls.config;if(r.capLevelOnFPSDrop){var i=e.media instanceof self.HTMLVideoElement?e.media:null;this.media=i,i&&"function"==typeof i.getVideoPlaybackQuality&&(this.isVideoPlaybackQualityAvailable=!0),self.clearInterval(this.timer),this.timer=self.setInterval(this.checkFPSInterval.bind(this),r.fpsDroppedMonitoringPeriod)}},e.checkFPS=function(t,e,r){var i=performance.now();if(e){if(this.lastTime){var n=i-this.lastTime,a=r-this.lastDroppedFrames,s=e-this.lastDecodedFrames,o=1e3*a/n,l=this.hls;if(l.trigger(S.FPS_DROP,{currentDropped:a,currentDecoded:s,totalDroppedFrames:r}),o>0&&a>l.config.fpsDroppedMonitoringThreshold*s){var u=l.currentLevel;w.warn("drop FPS ratio greater than max allowed value for currentLevel: "+u),u>0&&(-1===l.autoLevelCapping||l.autoLevelCapping>=u)&&(u-=1,l.trigger(S.FPS_DROP_LEVEL_CAPPING,{level:u,droppedLevel:l.currentLevel}),l.autoLevelCapping=u,this.streamController.nextLevelSwitch())}}this.lastTime=i,this.lastDroppedFrames=r,this.lastDecodedFrames=e}},e.checkFPSInterval=function(){var t=this.media;if(t)if(this.isVideoPlaybackQualityAvailable){var e=t.getVideoPlaybackQuality();this.checkFPS(t,e.totalVideoFrames,e.droppedVideoFrames)}else this.checkFPS(t,t.webkitDecodedFrameCount,t.webkitDroppedFrameCount)},t}(),za="[eme]",Qa=function(){function t(e){this.hls=void 0,this.config=void 0,this.media=null,this.keyFormatPromise=null,this.keySystemAccessPromises={},this._requestLicenseFailureCount=0,this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},this.setMediaKeysQueue=t.CDMCleanupPromise?[t.CDMCleanupPromise]:[],this.onMediaEncrypted=this._onMediaEncrypted.bind(this),this.onWaitingForKey=this._onWaitingForKey.bind(this),this.debug=w.debug.bind(w,za),this.log=w.log.bind(w,za),this.warn=w.warn.bind(w,za),this.error=w.error.bind(w,za),this.hls=e,this.config=e.config,this.registerListeners()}var e=t.prototype;return e.destroy=function(){this.unregisterListeners(),this.onMediaDetached();var t=this.config;t.requestMediaKeySystemAccessFunc=null,t.licenseXhrSetup=t.licenseResponseCallback=void 0,t.drmSystems=t.drmSystemOptions={},this.hls=this.onMediaEncrypted=this.onWaitingForKey=this.keyIdToKeySessionPromise=null,this.config=null},e.registerListeners=function(){this.hls.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.on(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.on(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.on(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.unregisterListeners=function(){this.hls.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),this.hls.off(S.MEDIA_DETACHED,this.onMediaDetached,this),this.hls.off(S.MANIFEST_LOADING,this.onManifestLoading,this),this.hls.off(S.MANIFEST_LOADED,this.onManifestLoaded,this)},e.getLicenseServerUrl=function(t){var e=this.config,r=e.drmSystems,i=e.widevineLicenseUrl,n=r[t];if(n)return n.licenseUrl;if(t===q.WIDEVINE&&i)return i;throw new Error('no license server URL configured for key-system "'+t+'"')},e.getServerCertificateUrl=function(t){var e=this.config.drmSystems[t];if(e)return e.serverCertificateUrl;this.log('No Server Certificate in config.drmSystems["'+t+'"]')},e.attemptKeySystemAccess=function(t){var e=this,r=this.hls.levels,i=function(t,e,r){return!!t&&r.indexOf(t)===e},n=r.map((function(t){return t.audioCodec})).filter(i),a=r.map((function(t){return t.videoCodec})).filter(i);return n.length+a.length===0&&a.push("avc1.42e01e"),new Promise((function(r,i){!function t(s){var o=s.shift();e.getMediaKeysPromise(o,n,a).then((function(t){return r({keySystem:o,mediaKeys:t})})).catch((function(e){s.length?t(s):i(e instanceof es?e:new es({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_ACCESS,error:e,fatal:!0},e.message))}))}(t)}))},e.requestMediaKeySystemAccess=function(t,e){var r=this.config.requestMediaKeySystemAccessFunc;if("function"!=typeof r){var i="Configured requestMediaKeySystemAccess is not a function "+r;return null===it&&"http:"===self.location.protocol&&(i="navigator.requestMediaKeySystemAccess is not available over insecure protocol "+location.protocol),Promise.reject(new Error(i))}return r(t,e)},e.getMediaKeysPromise=function(t,e,r){var i=this,n=function(t,e,r,i){var n;switch(t){case q.FAIRPLAY:n=["cenc","sinf"];break;case q.WIDEVINE:case q.PLAYREADY:n=["cenc"];break;case q.CLEARKEY:n=["cenc","keyids"];break;default:throw new Error("Unknown key-system: "+t)}return function(t,e,r,i){return[{initDataTypes:t,persistentState:i.persistentState||"optional",distinctiveIdentifier:i.distinctiveIdentifier||"optional",sessionTypes:i.sessionTypes||[i.sessionType||"temporary"],audioCapabilities:e.map((function(t){return{contentType:'audio/mp4; codecs="'+t+'"',robustness:i.audioRobustness||"",encryptionScheme:i.audioEncryptionScheme||null}})),videoCapabilities:r.map((function(t){return{contentType:'video/mp4; codecs="'+t+'"',robustness:i.videoRobustness||"",encryptionScheme:i.videoEncryptionScheme||null}}))}]}(n,e,r,i)}(t,e,r,this.config.drmSystemOptions),a=this.keySystemAccessPromises[t],s=null==a?void 0:a.keySystemAccess;if(!s){this.log('Requesting encrypted media "'+t+'" key-system access with config: '+JSON.stringify(n)),s=this.requestMediaKeySystemAccess(t,n);var o=this.keySystemAccessPromises[t]={keySystemAccess:s};return s.catch((function(e){i.log('Failed to obtain access to key-system "'+t+'": '+e)})),s.then((function(e){i.log('Access for key-system "'+e.keySystem+'" obtained');var r=i.fetchServerCertificate(t);return i.log('Create media-keys for "'+t+'"'),o.mediaKeys=e.createMediaKeys().then((function(e){return i.log('Media-keys created for "'+t+'"'),r.then((function(r){return r?i.setMediaKeysServerCertificate(e,t,r):e}))})),o.mediaKeys.catch((function(e){i.error('Failed to create media-keys for "'+t+'"}: '+e)})),o.mediaKeys}))}return s.then((function(){return a.mediaKeys}))},e.createMediaKeySessionContext=function(t){var e=t.decryptdata,r=t.keySystem,i=t.mediaKeys;this.log('Creating key-system session "'+r+'" keyId: '+Lt(e.keyId||[]));var n=i.createSession(),a={decryptdata:e,keySystem:r,mediaKeys:i,mediaKeysSession:n,keyStatus:"status-pending"};return this.mediaKeySessions.push(a),a},e.renewKeySession=function(t){var e=t.decryptdata;if(e.pssh){var r=this.createMediaKeySessionContext(t),i=this.getKeyIdString(e);this.keyIdToKeySessionPromise[i]=this.generateRequestWithPreferredKeySession(r,"cenc",e.pssh,"expired")}else this.warn("Could not renew expired session. Missing pssh initData.");this.removeSession(t)},e.getKeyIdString=function(t){if(!t)throw new Error("Could not read keyId of undefined decryptdata");if(null===t.keyId)throw new Error("keyId is null");return Lt(t.keyId)},e.updateKeySession=function(t,e){var r,i=t.mediaKeysSession;return this.log('Updating key-session "'+i.sessionId+'" for keyID '+Lt((null==(r=t.decryptdata)?void 0:r.keyId)||[])+"\n } (data length: "+(e?e.byteLength:e)+")"),i.update(e)},e.selectKeySystemFormat=function(t){var e=Object.keys(t.levelkeys||{});return this.keyFormatPromise||(this.log("Selecting key-system from fragment (sn: "+t.sn+" "+t.type+": "+t.level+") key formats "+e.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(e)),this.keyFormatPromise},e.getKeyFormatPromise=function(t){var e=this;return new Promise((function(r,i){var n=et(e.config),a=t.map($).filter((function(t){return!!t&&-1!==n.indexOf(t)}));return e.getKeySystemSelectionPromise(a).then((function(t){var e=t.keySystem,n=tt(e);n?r(n):i(new Error('Unable to find format for key-system "'+e+'"'))})).catch(i)}))},e.loadKey=function(t){var e=this,r=t.keyInfo.decryptdata,i=this.getKeyIdString(r),n="(keyId: "+i+' format: "'+r.keyFormat+'" method: '+r.method+" uri: "+r.uri+")";this.log("Starting session for key "+n);var a=this.keyIdToKeySessionPromise[i];return a||(a=this.keyIdToKeySessionPromise[i]=this.getKeySystemForKeyPromise(r).then((function(i){var a=i.keySystem,s=i.mediaKeys;return e.throwIfDestroyed(),e.log("Handle encrypted media sn: "+t.frag.sn+" "+t.frag.type+": "+t.frag.level+" using key "+n),e.attemptSetMediaKeys(a,s).then((function(){e.throwIfDestroyed();var t=e.createMediaKeySessionContext({keySystem:a,mediaKeys:s,decryptdata:r});return e.generateRequestWithPreferredKeySession(t,"cenc",r.pssh,"playlist-key")}))}))).catch((function(t){return e.handleError(t)})),a},e.throwIfDestroyed=function(t){if(!this.hls)throw new Error("invalid state")},e.handleError=function(t){this.hls&&(this.error(t.message),t instanceof es?this.hls.trigger(S.ERROR,t.data):this.hls.trigger(S.ERROR,{type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_KEYS,error:t,fatal:!0}))},e.getKeySystemForKeyPromise=function(t){var e=this.getKeyIdString(t),r=this.keyIdToKeySessionPromise[e];if(!r){var i=$(t.keyFormat),n=i?[i]:et(this.config);return this.attemptKeySystemAccess(n)}return r},e.getKeySystemSelectionPromise=function(t){if(t.length||(t=et(this.config)),0===t.length)throw new es({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_NO_CONFIGURED_LICENSE,fatal:!0},"Missing key-system license configuration options "+JSON.stringify({drmSystems:this.config.drmSystems}));return this.attemptKeySystemAccess(t)},e._onMediaEncrypted=function(t){var e=this,r=t.initDataType,i=t.initData;if(this.debug('"'+t.type+'" event: init data type: "'+r+'"'),null!==i){var n,a;if("sinf"===r&&this.config.drmSystems[q.FAIRPLAY]){var s=bt(new Uint8Array(i));try{var o=V(JSON.parse(s).sinf),l=Ut(new Uint8Array(o));if(!l)return;n=l.subarray(8,24),a=q.FAIRPLAY}catch(t){return void this.warn('Failed to parse sinf "encrypted" event message initData')}}else{var u=function(t){if(!(t instanceof ArrayBuffer)||t.byteLength<32)return null;var e={version:0,systemId:"",kids:null,data:null},r=new DataView(t),i=r.getUint32(0);if(t.byteLength!==i&&i>44)return null;if(1886614376!==r.getUint32(4))return null;if(e.version=r.getUint32(8)>>>24,e.version>1)return null;e.systemId=Lt(new Uint8Array(t,12,16));var n=r.getUint32(28);if(0===e.version){if(i-320)for(var a,s=0,o=n.length;s in key message");return W(atob(f))},e.setupLicenseXHR=function(t,e,r,i){var n=this,a=this.config.licenseXhrSetup;return a?Promise.resolve().then((function(){if(!r.decryptdata)throw new Error("Key removed");return a.call(n.hls,t,e,r,i)})).catch((function(s){if(!r.decryptdata)throw s;return t.open("POST",e,!0),a.call(n.hls,t,e,r,i)})).then((function(r){return t.readyState||t.open("POST",e,!0),{xhr:t,licenseChallenge:r||i}})):(t.open("POST",e,!0),Promise.resolve({xhr:t,licenseChallenge:i}))},e.requestLicense=function(t,e){var r=this,i=this.config.keyLoadPolicy.default;return new Promise((function(n,a){var s=r.getLicenseServerUrl(t.keySystem);r.log("Sending license request to URL: "+s);var o=new XMLHttpRequest;o.responseType="arraybuffer",o.onreadystatechange=function(){if(!r.hls||!t.mediaKeysSession)return a(new Error("invalid state"));if(4===o.readyState)if(200===o.status){r._requestLicenseFailureCount=0;var l=o.response;r.log("License received "+(l instanceof ArrayBuffer?l.byteLength:l));var u=r.config.licenseResponseCallback;if(u)try{l=u.call(r.hls,o,s,t)}catch(t){r.error(t)}n(l)}else{var h=i.errorRetry,d=h?h.maxNumRetry:0;if(r._requestLicenseFailureCount++,r._requestLicenseFailureCount>d||o.status>=400&&o.status<500)a(new es({type:L.KEY_SYSTEM_ERROR,details:A.KEY_SYSTEM_LICENSE_REQUEST_FAILED,fatal:!0,networkDetails:o,response:{url:s,data:void 0,code:o.status,text:o.statusText}},"License Request XHR failed ("+s+"). Status: "+o.status+" ("+o.statusText+")"));else{var c=d-r._requestLicenseFailureCount+1;r.warn("Retrying license request, "+c+" attempts left"),r.requestLicense(t,e).then(n,a)}}},t.licenseXhr&&t.licenseXhr.readyState!==XMLHttpRequest.DONE&&t.licenseXhr.abort(),t.licenseXhr=o,r.setupLicenseXHR(o,s,t,e).then((function(e){var i=e.xhr,n=e.licenseChallenge;t.keySystem==q.PLAYREADY&&(n=r.unpackPlayReadyKeyMessage(i,n)),i.send(n)}))}))},e.onMediaAttached=function(t,e){if(this.config.emeEnabled){var r=e.media;this.media=r,r.addEventListener("encrypted",this.onMediaEncrypted),r.addEventListener("waitingforkey",this.onWaitingForKey)}},e.onMediaDetached=function(){var e=this,r=this.media,i=this.mediaKeySessions;r&&(r.removeEventListener("encrypted",this.onMediaEncrypted),r.removeEventListener("waitingforkey",this.onWaitingForKey),this.media=null),this._requestLicenseFailureCount=0,this.setMediaKeysQueue=[],this.mediaKeySessions=[],this.keyIdToKeySessionPromise={},qt.clearKeyUriToKeyIdMap();var n=i.length;t.CDMCleanupPromise=Promise.all(i.map((function(t){return e.removeSession(t)})).concat(null==r?void 0:r.setMediaKeys(null).catch((function(t){e.log("Could not clear media keys: "+t)})))).then((function(){n&&(e.log("finished closing key sessions and clearing media keys"),i.length=0)})).catch((function(t){e.log("Could not close sessions and clear media keys: "+t)}))},e.onManifestLoading=function(){this.keyFormatPromise=null},e.onManifestLoaded=function(t,e){var r=e.sessionKeys;if(r&&this.config.emeEnabled&&!this.keyFormatPromise){var i=r.reduce((function(t,e){return-1===t.indexOf(e.keyFormat)&&t.push(e.keyFormat),t}),[]);this.log("Selecting key-system from session-keys "+i.join(", ")),this.keyFormatPromise=this.getKeyFormatPromise(i)}},e.removeSession=function(t){var e=this,r=t.mediaKeysSession,i=t.licenseXhr;if(r){this.log("Remove licenses and keys and close session "+r.sessionId),t._onmessage&&(r.removeEventListener("message",t._onmessage),t._onmessage=void 0),t._onkeystatuseschange&&(r.removeEventListener("keystatuseschange",t._onkeystatuseschange),t._onkeystatuseschange=void 0),i&&i.readyState!==XMLHttpRequest.DONE&&i.abort(),t.mediaKeysSession=t.decryptdata=t.licenseXhr=void 0;var n=this.mediaKeySessions.indexOf(t);return n>-1&&this.mediaKeySessions.splice(n,1),r.remove().catch((function(t){e.log("Could not remove session: "+t)})).then((function(){return r.close()})).catch((function(t){e.log("Could not close session: "+t)}))}},t}();Qa.CDMCleanupPromise=void 0;var Ja,$a,Za,ts,es=function(t){function e(e,r){var i;return(i=t.call(this,r)||this).data=void 0,e.error||(e.error=new Error(r)),i.data=e,e.err=e.error,i}return l(e,t),e}(c(Error));!function(t){t.MANIFEST="m",t.AUDIO="a",t.VIDEO="v",t.MUXED="av",t.INIT="i",t.CAPTION="c",t.TIMED_TEXT="tt",t.KEY="k",t.OTHER="o"}(Ja||(Ja={})),function(t){t.DASH="d",t.HLS="h",t.SMOOTH="s",t.OTHER="o"}($a||($a={})),function(t){t.OBJECT="CMCD-Object",t.REQUEST="CMCD-Request",t.SESSION="CMCD-Session",t.STATUS="CMCD-Status"}(Za||(Za={}));var rs=((ts={})[Za.OBJECT]=["br","d","ot","tb"],ts[Za.REQUEST]=["bl","dl","mtp","nor","nrr","su"],ts[Za.SESSION]=["cid","pr","sf","sid","st","v"],ts[Za.STATUS]=["bs","rtp"],ts),is=function t(e,r){this.value=void 0,this.params=void 0,Array.isArray(e)&&(e=e.map((function(e){return e instanceof t?e:new t(e)}))),this.value=e,this.params=r},ns=function(t){this.description=void 0,this.description=t},as="Dict";function ss(t,e,r,i){return new Error("failed to "+t+' "'+(n=e,(Array.isArray(n)?JSON.stringify(n):n instanceof Map?"Map{}":n instanceof Set?"Set{}":"object"==typeof n?JSON.stringify(n):String(n))+'" as ')+r,{cause:i});var n}var os="Bare Item",ls="Boolean",us="Byte Sequence",hs="Decimal",ds="Integer",cs=/[\x00-\x1f\x7f]+/,fs="Token",gs="Key";function vs(t,e,r){return ss("serialize",t,e,r)}function ms(t){if(!1===ArrayBuffer.isView(t))throw vs(t,us);return":"+(e=t,btoa(String.fromCharCode.apply(String,e))+":");var e}function ps(t){if(function(t){return t<-999999999999999||99999999999999912)throw vs(t,hs);var r=e.toString();return r.includes(".")?r:r+".0"}var Ts="String";function Ss(t){var e,r=(e=t).description||e.toString().slice(7,-1);if(!1===/^([a-zA-Z*])([!#$%&'*+\-.^_`|~\w:/]*)$/.test(r))throw vs(r,fs);return r}function Ls(t){switch(typeof t){case"number":if(!y(t))throw vs(t,os);return Number.isInteger(t)?ps(t):Es(t);case"string":return function(t){if(cs.test(t))throw vs(t,Ts);return'"'+t.replace(/\\/g,"\\\\").replace(/"/g,'\\"')+'"'}(t);case"symbol":return Ss(t);case"boolean":return function(t){if("boolean"!=typeof t)throw vs(t,ls);return t?"?1":"?0"}(t);case"object":if(t instanceof Date)return function(t){return"@"+ps(t.getTime()/1e3)}(t);if(t instanceof Uint8Array)return ms(t);if(t instanceof ns)return Ss(t);default:throw vs(t,os)}}function As(t){if(!1===/^[a-z*][a-z0-9\-_.*]*$/.test(t))throw vs(t,gs);return t}function Rs(t){return null==t?"":Object.entries(t).map((function(t){var e=t[0],r=t[1];return!0===r?";"+As(e):";"+As(e)+"="+Ls(r)})).join("")}function ks(t){return t instanceof is?""+Ls(t.value)+Rs(t.params):Ls(t)}function bs(t,e){var r;if(void 0===e&&(e={whitespace:!0}),"object"!=typeof t)throw vs(t,as);var i=t instanceof Map?t.entries():Object.entries(t),n=null!=(r=e)&&r.whitespace?" ":"";return Array.from(i).map((function(t){var e=t[0],r=t[1];r instanceof is==0&&(r=new is(r));var i,n=As(e);return!0===r.value?n+=Rs(r.params):(n+="=",Array.isArray(r.value)?n+="("+(i=r).value.map(ks).join(" ")+")"+Rs(i.params):n+=ks(r)),n})).join(","+n)}var Ds=function(t){return"ot"===t||"sf"===t||"st"===t},Is=function(t){return"number"==typeof t?y(t):null!=t&&""!==t&&!1!==t},ws=function(t){return Math.round(t)},Cs=function(t){return 100*ws(t/100)},_s={br:ws,d:ws,bl:Cs,dl:Cs,mtp:Cs,nor:function(t,e){return null!=e&&e.baseUrl&&(t=function(t,e){var r=new URL(t),i=new URL(e);if(r.origin!==i.origin)return t;for(var n=r.pathname.split("/").slice(1),a=i.pathname.split("/").slice(1,-1);n[0]===a[0];)n.shift(),a.shift();for(;a.length;)a.shift(),n.unshift("..");return n.join("/")}(t,e.baseUrl)),encodeURIComponent(t)},rtp:Cs,tb:ws};function xs(t,e){return void 0===e&&(e={}),t?function(t,e){return bs(t,e)}(function(t,e){var r={};if(null==t||"object"!=typeof t)return r;var i=Object.keys(t).sort(),n=o({},_s,null==e?void 0:e.formatters),a=null==e?void 0:e.filter;return i.forEach((function(i){if(null==a||!a(i)){var s=t[i],o=n[i];o&&(s=o(s,e)),"v"===i&&1===s||"pr"==i&&1===s||Is(s)&&(Ds(i)&&"string"==typeof s&&(s=new ns(s)),r[i]=s)}})),r}(t,e),o({whitespace:!1},e)):""}function Ps(t,e,r){return o(t,function(t,e){var r;if(void 0===e&&(e={}),!t)return{};var i=Object.entries(t),n=Object.entries(rs).concat(Object.entries((null==(r=e)?void 0:r.customHeaderMap)||{})),a=i.reduce((function(t,e){var r,i=e[0],a=e[1],s=(null==(r=n.find((function(t){return t[1].includes(i)})))?void 0:r[0])||Za.REQUEST;return null!=t[s]||(t[s]={}),t[s][i]=a,t}),{});return Object.entries(a).reduce((function(t,r){var i=r[0],n=r[1];return t[i]=xs(n,e),t}),{})}(e,r))}var Fs="CMCD",Ms=/CMCD=[^&#]+/;function Os(t,e,r){var i=function(t,e){if(void 0===e&&(e={}),!t)return"";var r=xs(t,e);return Fs+"="+encodeURIComponent(r)}(e,r);if(!i)return t;if(Ms.test(t))return t.replace(Ms,i);var n=t.includes("?")?"&":"?";return""+t+n+i}var Ns=function(){function t(t){var e=this;this.hls=void 0,this.config=void 0,this.media=void 0,this.sid=void 0,this.cid=void 0,this.useHeaders=!1,this.includeKeys=void 0,this.initialized=!1,this.starved=!1,this.buffering=!0,this.audioBuffer=void 0,this.videoBuffer=void 0,this.onWaiting=function(){e.initialized&&(e.starved=!0),e.buffering=!0},this.onPlaying=function(){e.initialized||(e.initialized=!0),e.buffering=!1},this.applyPlaylistData=function(t){try{e.apply(t,{ot:Ja.MANIFEST,su:!e.initialized})}catch(t){w.warn("Could not generate manifest CMCD data.",t)}},this.applyFragmentData=function(t){try{var r=t.frag,i=e.hls.levels[r.level],n=e.getObjectType(r),a={d:1e3*r.duration,ot:n};n!==Ja.VIDEO&&n!==Ja.AUDIO&&n!=Ja.MUXED||(a.br=i.bitrate/1e3,a.tb=e.getTopBandwidth(n)/1e3,a.bl=e.getBufferLength(n)),e.apply(t,a)}catch(t){w.warn("Could not generate segment CMCD data.",t)}},this.hls=t;var r=this.config=t.config,i=r.cmcd;null!=i&&(r.pLoader=this.createPlaylistLoader(),r.fLoader=this.createFragmentLoader(),this.sid=i.sessionId||function(){try{return crypto.randomUUID()}catch(i){try{var t=URL.createObjectURL(new Blob),e=t.toString();return URL.revokeObjectURL(t),e.slice(e.lastIndexOf("/")+1)}catch(t){var r=(new Date).getTime();return"xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g,(function(t){var e=(r+16*Math.random())%16|0;return r=Math.floor(r/16),("x"==t?e:3&e|8).toString(16)}))}}}(),this.cid=i.contentId,this.useHeaders=!0===i.useHeaders,this.includeKeys=i.includeKeys,this.registerListeners())}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.on(S.MEDIA_DETACHED,this.onMediaDetached,this),t.on(S.BUFFER_CREATED,this.onBufferCreated,this)},e.unregisterListeners=function(){var t=this.hls;t.off(S.MEDIA_ATTACHED,this.onMediaAttached,this),t.off(S.MEDIA_DETACHED,this.onMediaDetached,this),t.off(S.BUFFER_CREATED,this.onBufferCreated,this)},e.destroy=function(){this.unregisterListeners(),this.onMediaDetached(),this.hls=this.config=this.audioBuffer=this.videoBuffer=null,this.onWaiting=this.onPlaying=null},e.onMediaAttached=function(t,e){this.media=e.media,this.media.addEventListener("waiting",this.onWaiting),this.media.addEventListener("playing",this.onPlaying)},e.onMediaDetached=function(){this.media&&(this.media.removeEventListener("waiting",this.onWaiting),this.media.removeEventListener("playing",this.onPlaying),this.media=null)},e.onBufferCreated=function(t,e){var r,i;this.audioBuffer=null==(r=e.tracks.audio)?void 0:r.buffer,this.videoBuffer=null==(i=e.tracks.video)?void 0:i.buffer},e.createData=function(){var t;return{v:1,sf:$a.HLS,sid:this.sid,cid:this.cid,pr:null==(t=this.media)?void 0:t.playbackRate,mtp:this.hls.bandwidthEstimate/1e3}},e.apply=function(t,e){void 0===e&&(e={}),o(e,this.createData());var r=e.ot===Ja.INIT||e.ot===Ja.VIDEO||e.ot===Ja.MUXED;this.starved&&r&&(e.bs=!0,e.su=!0,this.starved=!1),null==e.su&&(e.su=this.buffering);var i=this.includeKeys;i&&(e=Object.keys(e).reduce((function(t,r){return i.includes(r)&&(t[r]=e[r]),t}),{})),this.useHeaders?(t.headers||(t.headers={}),Ps(t.headers,e)):t.url=Os(t.url,e)},e.getObjectType=function(t){var e=t.type;return"subtitle"===e?Ja.TIMED_TEXT:"initSegment"===t.sn?Ja.INIT:"audio"===e?Ja.AUDIO:"main"===e?this.hls.audioTracks.length?Ja.VIDEO:Ja.MUXED:void 0},e.getTopBandwidth=function(t){var e,r=0,i=this.hls;if(t===Ja.AUDIO)e=i.audioTracks;else{var n=i.maxAutoLevel,a=n>-1?n+1:i.levels.length;e=i.levels.slice(0,a)}for(var s,o=g(e);!(s=o()).done;){var l=s.value;l.bitrate>r&&(r=l.bitrate)}return r>0?r:NaN},e.getBufferLength=function(t){var e=this.hls.media,r=t===Ja.AUDIO?this.audioBuffer:this.videoBuffer;return r&&e?1e3*zr.bufferInfo(r,e.currentTime,this.config.maxBufferHole).len:NaN},e.createPlaylistLoader=function(){var t=this.config.pLoader,e=this.applyPlaylistData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},e.createFragmentLoader=function(){var t=this.config.fLoader,e=this.applyFragmentData,r=t||this.config.loader;return function(){function t(t){this.loader=void 0,this.loader=new r(t)}var i=t.prototype;return i.destroy=function(){this.loader.destroy()},i.abort=function(){this.loader.abort()},i.load=function(t,r,i){e(t),this.loader.load(t,r,i)},s(t,[{key:"stats",get:function(){return this.loader.stats}},{key:"context",get:function(){return this.loader.context}}]),t}()},t}(),Us=function(){function t(t){this.hls=void 0,this.log=void 0,this.loader=null,this.uri=null,this.pathwayId=".",this.pathwayPriority=null,this.timeToLoad=300,this.reloadTimer=-1,this.updated=0,this.started=!1,this.enabled=!0,this.levels=null,this.audioTracks=null,this.subtitleTracks=null,this.penalizedPathways={},this.hls=t,this.log=w.log.bind(w,"[content-steering]:"),this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.MANIFEST_PARSED,this.onManifestParsed,this),t.on(S.ERROR,this.onError,this)},e.unregisterListeners=function(){var t=this.hls;t&&(t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.MANIFEST_PARSED,this.onManifestParsed,this),t.off(S.ERROR,this.onError,this))},e.startLoad=function(){if(this.started=!0,this.clearTimeout(),this.enabled&&this.uri){if(this.updated){var t=1e3*this.timeToLoad-(performance.now()-this.updated);if(t>0)return void this.scheduleRefresh(this.uri,t)}this.loadSteeringManifest(this.uri)}},e.stopLoad=function(){this.started=!1,this.loader&&(this.loader.destroy(),this.loader=null),this.clearTimeout()},e.clearTimeout=function(){-1!==this.reloadTimer&&(self.clearTimeout(this.reloadTimer),this.reloadTimer=-1)},e.destroy=function(){this.unregisterListeners(),this.stopLoad(),this.hls=null,this.levels=this.audioTracks=this.subtitleTracks=null},e.removeLevel=function(t){var e=this.levels;e&&(this.levels=e.filter((function(e){return e!==t})))},e.onManifestLoading=function(){this.stopLoad(),this.enabled=!0,this.timeToLoad=300,this.updated=0,this.uri=null,this.pathwayId=".",this.levels=this.audioTracks=this.subtitleTracks=null},e.onManifestLoaded=function(t,e){var r=e.contentSteering;null!==r&&(this.pathwayId=r.pathwayId,this.uri=r.uri,this.started&&this.startLoad())},e.onManifestParsed=function(t,e){this.audioTracks=e.audioTracks,this.subtitleTracks=e.subtitleTracks},e.onError=function(t,e){var r=e.errorAction;if((null==r?void 0:r.action)===Tr&&r.flags===Rr){var i=this.levels,n=this.pathwayPriority,a=this.pathwayId;if(e.context){var s=e.context,o=s.groupId,l=s.pathwayId,u=s.type;o&&i?a=this.getPathwayForGroupId(o,u,a):l&&(a=l)}a in this.penalizedPathways||(this.penalizedPathways[a]=performance.now()),!n&&i&&(n=i.reduce((function(t,e){return-1===t.indexOf(e.pathwayId)&&t.push(e.pathwayId),t}),[])),n&&n.length>1&&(this.updatePathwayPriority(n),r.resolved=this.pathwayId!==a),r.resolved||w.warn("Could not resolve "+e.details+' ("'+e.error.message+'") with content-steering for Pathway: '+a+" levels: "+(i?i.length:i)+" priorities: "+JSON.stringify(n)+" penalized: "+JSON.stringify(this.penalizedPathways))}},e.filterParsedLevels=function(t){this.levels=t;var e=this.getLevelsForPathway(this.pathwayId);if(0===e.length){var r=t[0].pathwayId;this.log("No levels found in Pathway "+this.pathwayId+'. Setting initial Pathway to "'+r+'"'),e=this.getLevelsForPathway(r),this.pathwayId=r}return e.length!==t.length?(this.log("Found "+e.length+"/"+t.length+' levels in Pathway "'+this.pathwayId+'"'),e):t},e.getLevelsForPathway=function(t){return null===this.levels?[]:this.levels.filter((function(e){return t===e.pathwayId}))},e.updatePathwayPriority=function(t){var e;this.pathwayPriority=t;var r=this.penalizedPathways,i=performance.now();Object.keys(r).forEach((function(t){i-r[t]>3e5&&delete r[t]}));for(var n=0;n0){this.log('Setting Pathway to "'+a+'"'),this.pathwayId=a,ur(e),this.hls.trigger(S.LEVELS_UPDATED,{levels:e});var l=this.hls.levels[s];o&&l&&this.levels&&(l.attrs["STABLE-VARIANT-ID"]!==o.attrs["STABLE-VARIANT-ID"]&&l.bitrate!==o.bitrate&&this.log("Unstable Pathways change from bitrate "+o.bitrate+" to "+l.bitrate),this.hls.nextLoadLevel=s);break}}}},e.getPathwayForGroupId=function(t,e,r){for(var i=this.getLevelsForPathway(r).concat(this.levels||[]),n=0;n=2&&(0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start),n.timeout!==n.loadPolicy.maxLoadTimeMs&&(self.clearTimeout(this.requestTimeout),n.timeout=n.loadPolicy.maxLoadTimeMs,this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),n.loadPolicy.maxLoadTimeMs-(r.loading.first-r.loading.start)))),4===i)){self.clearTimeout(this.requestTimeout),e.onreadystatechange=null,e.onprogress=null;var a=e.status,s="text"!==e.responseType;if(a>=200&&a<300&&(s&&e.response||null!==e.responseText)){r.loading.end=Math.max(self.performance.now(),r.loading.first);var o=s?e.response:e.responseText,l="arraybuffer"===e.responseType?o.byteLength:o.length;if(r.loaded=r.total=l,r.bwEstimate=8e3*r.total/(r.loading.end-r.loading.first),!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,o,e),!this.callbacks)return;var h={url:e.responseURL,data:o,code:a};this.callbacks.onSuccess(h,r,t,e)}else{var d=n.loadPolicy.errorRetry;gr(d,r.retry,!1,{url:t.url,data:void 0,code:a})?this.retry(d):(w.error(a+" while loading "+t.url),this.callbacks.onError({code:a,text:e.statusText},t,e,r))}}}},e.loadtimeout=function(){var t,e=null==(t=this.config)?void 0:t.loadPolicy.timeoutRetry;if(gr(e,this.stats.retry,!0))this.retry(e);else{var r;w.warn("timeout while loading "+(null==(r=this.context)?void 0:r.url));var i=this.callbacks;i&&(this.abortInternal(),i.onTimeout(this.stats,this.context,this.loader))}},e.retry=function(t){var e=this.context,r=this.stats;this.retryDelay=cr(t,r.retry),r.retry++,w.warn((status?"HTTP Status "+status:"Timeout")+" while loading "+(null==e?void 0:e.url)+", retrying "+r.retry+"/"+t.maxNumRetry+" in "+this.retryDelay+"ms"),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay)},e.loadprogress=function(t){var e=this.stats;e.loaded=t.loaded,t.lengthComputable&&(e.total=t.total)},e.getCacheAge=function(){var t=null;if(this.loader&&Ks.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.loader&&new RegExp("^"+t+":\\s*[\\d.]+\\s*$","im").test(this.loader.getAllResponseHeaders())?this.loader.getResponseHeader(t):null},t}(),Vs=/(\d+)-(\d+)\/(\d+)/,Ys=function(){function t(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=null,this.response=null,this.controller=void 0,this.context=null,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||Ws,this.controller=new self.AbortController,this.stats=new M}var e=t.prototype;return e.destroy=function(){this.loader=this.callbacks=this.context=this.config=this.request=null,this.abortInternal(),this.response=null,this.fetchSetup=this.controller=this.stats=null},e.abortInternal=function(){this.controller&&!this.stats.loading.end&&(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!=(t=this.callbacks)&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.response)},e.load=function(t,e,r){var i=this,n=this.stats;if(n.loading.start)throw new Error("Loader can only be used once.");n.loading.start=self.performance.now();var a=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e,headers:new self.Headers(o({},t.headers))};return t.rangeEnd&&r.headers.set("Range","bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)),r}(t,this.controller.signal),s=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length",h=e.loadPolicy,d=h.maxTimeToFirstByteMs,c=h.maxLoadTimeMs;this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,a),self.clearTimeout(this.requestTimeout),e.timeout=d&&y(d)?d:c,this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(a){i.response=i.loader=a;var o=Math.max(self.performance.now(),n.loading.start);if(self.clearTimeout(i.requestTimeout),e.timeout=c,i.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),c-(o-n.loading.start)),!a.ok){var u=a.status,h=a.statusText;throw new qs(h||"fetch, bad network response",u,a)}return n.loading.first=o,n.total=function(t){var e=t.get("Content-Range");if(e){var r=function(t){var e=Vs.exec(t);if(e)return parseInt(e[2])-parseInt(e[1])+1}(e);if(y(r))return r}var i=t.get("Content-Length");if(i)return parseInt(i)}(a.headers)||n.total,s&&y(e.highWaterMark)?i.loadProgressively(a,n,t,e.highWaterMark,s):l?a.arrayBuffer():"json"===t.responseType?a.json():a.text()})).then((function(a){var o=i.response;if(!o)throw new Error("loader destroyed");self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first);var l=a[u];l&&(n.loaded=n.total=l);var h={url:o.url,data:a,code:o.status};s&&!y(e.highWaterMark)&&s(n,t,a,o),r.onSuccess(h,n,t,o)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e&&e.code||0,s=e?e.message:null;r.onError({code:a,text:s},t,e?e.details:null,n)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.getResponseHeader=function(t){return this.response?this.response.headers.get(t):null},e.loadProgressively=function(t,e,r,i,n){void 0===i&&(i=0);var a=new ki,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return a.dataLength&&n(e,r,a.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&n(e,r,a.flush(),t)):n(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function Ws(t,e){return new self.Request(t.url,e)}var js,qs=function(t){function e(e,r,i){var n;return(n=t.call(this,e)||this).code=void 0,n.details=void 0,n.code=r,n.details=i,n}return l(e,t),e}(c(Error)),Xs=/\s/,zs=i(i({autoStartLoad:!0,startPosition:-1,defaultAudioCodec:void 0,debug:!1,capLevelOnFPSDrop:!1,capLevelToPlayerSize:!1,ignoreDevicePixelRatio:!1,preferManagedMediaSource:!0,initialLiveManifestSize:1,maxBufferLength:30,backBufferLength:1/0,frontBufferFlushThreshold:1/0,maxBufferSize:6e7,maxBufferHole:.1,highBufferWatchdogPeriod:2,nudgeOffset:.1,nudgeMaxRetry:3,maxFragLookUpTolerance:.25,liveSyncDurationCount:3,liveMaxLatencyDurationCount:1/0,liveSyncDuration:void 0,liveMaxLatencyDuration:void 0,maxLiveSyncPlaybackRate:1,liveDurationInfinity:!1,liveBackBufferLength:null,maxMaxBufferLength:600,enableWorker:!0,workerPath:null,enableSoftwareAES:!0,startLevel:void 0,startFragPrefetch:!1,fpsDroppedMonitoringPeriod:5e3,fpsDroppedMonitoringThreshold:.2,appendErrorMaxRetry:3,loader:Hs,fLoader:void 0,pLoader:void 0,xhrSetup:void 0,licenseXhrSetup:void 0,licenseResponseCallback:void 0,abrController:Br,bufferController:Qn,capLevelController:qa,errorController:br,fpsController:Xa,stretchShortVideoTrack:!1,maxAudioFramesDrift:1,forceKeyFrameOnDiscontinuity:!0,abrEwmaFastLive:3,abrEwmaSlowLive:9,abrEwmaFastVoD:3,abrEwmaSlowVoD:9,abrEwmaDefaultEstimate:5e5,abrEwmaDefaultEstimateMax:5e6,abrBandWidthFactor:.95,abrBandWidthUpFactor:.7,abrMaxWithRealBitrate:!1,maxStarvationDelay:4,maxLoadingDelay:4,minAutoBitrate:0,emeEnabled:!1,widevineLicenseUrl:void 0,drmSystems:{},drmSystemOptions:{},requestMediaKeySystemAccessFunc:it,testBandwidth:!0,progressive:!1,lowLatencyMode:!0,cmcd:void 0,enableDateRangeMetadataCues:!0,enableEmsgMetadataCues:!0,enableID3MetadataCues:!0,useMediaCapabilities:!0,certLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:null,errorRetry:null}},keyLoadPolicy:{default:{maxTimeToFirstByteMs:8e3,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"},errorRetry:{maxNumRetry:8,retryDelayMs:1e3,maxRetryDelayMs:2e4,backoff:"linear"}}},manifestLoadPolicy:{default:{maxTimeToFirstByteMs:1/0,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},playlistLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:2,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},fragLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:12e4,timeoutRetry:{maxNumRetry:4,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:6,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},steeringManifestLoadPolicy:{default:{maxTimeToFirstByteMs:1e4,maxLoadTimeMs:2e4,timeoutRetry:{maxNumRetry:2,retryDelayMs:0,maxRetryDelayMs:0},errorRetry:{maxNumRetry:1,retryDelayMs:1e3,maxRetryDelayMs:8e3}}},manifestLoadingTimeOut:1e4,manifestLoadingMaxRetry:1,manifestLoadingRetryDelay:1e3,manifestLoadingMaxRetryTimeout:64e3,levelLoadingTimeOut:1e4,levelLoadingMaxRetry:4,levelLoadingRetryDelay:1e3,levelLoadingMaxRetryTimeout:64e3,fragLoadingTimeOut:2e4,fragLoadingMaxRetry:6,fragLoadingRetryDelay:1e3,fragLoadingMaxRetryTimeout:64e3},{cueHandler:{newCue:function(t,e,r,i){for(var n,a,s,o,l,u=[],h=self.VTTCue||self.TextTrackCue,d=0;d=16?o--:o++;var g=ba(l.trim()),v=_a(e,r,g);null!=t&&null!=(c=t.cues)&&c.getCueById(v)||((a=new h(e,r,g)).id=v,a.line=d+1,a.align="left",a.position=10+Math.min(80,10*Math.floor(8*o/32)),u.push(a))}return t&&u.length&&(u.sort((function(t,e){return"auto"===t.line||"auto"===e.line?0:t.line>8&&e.line>8?e.line-t.line:t.line-e.line})),u.forEach((function(e){return Me(t,e)}))),u}},enableWebVTT:!0,enableIMSC1:!0,enableCEA708Captions:!0,captionsTextTrack1Label:"English",captionsTextTrack1LanguageCode:"en",captionsTextTrack2Label:"Spanish",captionsTextTrack2LanguageCode:"es",captionsTextTrack3Label:"Unknown CC",captionsTextTrack3LanguageCode:"",captionsTextTrack4Label:"Unknown CC",captionsTextTrack4LanguageCode:"",renderTextTracksNatively:!0}),{},{subtitleStreamController:Wn,subtitleTrackController:qn,timelineController:Ya,audioStreamController:Vn,audioTrackController:Yn,emeController:Qa,cmcdController:Ns,contentSteeringController:Us});function Qs(t){return t&&"object"==typeof t?Array.isArray(t)?t.map(Qs):Object.keys(t).reduce((function(e,r){return e[r]=Qs(t[r]),e}),{}):t}function Js(t){var e=t.loader;e!==Ys&&e!==Hs?(w.log("[config]: Custom loader detected, cannot enable progressive streaming"),t.progressive=!1):function(){if(self.fetch&&self.AbortController&&self.ReadableStream&&self.Request)try{return new self.ReadableStream({}),!0}catch(t){}return!1}()&&(t.loader=Ys,t.progressive=!0,t.enableSoftwareAES=!0,w.log("[config]: Progressive streaming enabled, using FetchLoader"))}var $s=function(t){function e(e,r){var i;return(i=t.call(this,e,"[level-controller]")||this)._levels=[],i._firstLevel=-1,i._maxAutoLevel=-1,i._startLevel=void 0,i.currentLevel=null,i.currentLevelIndex=-1,i.manualLevelIndex=-1,i.steering=void 0,i.onParsedComplete=void 0,i.steering=r,i._registerListeners(),i}l(e,t);var r=e.prototype;return r._registerListeners=function(){var t=this.hls;t.on(S.MANIFEST_LOADING,this.onManifestLoading,this),t.on(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.on(S.LEVEL_LOADED,this.onLevelLoaded,this),t.on(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.on(S.FRAG_BUFFERED,this.onFragBuffered,this),t.on(S.ERROR,this.onError,this)},r._unregisterListeners=function(){var t=this.hls;t.off(S.MANIFEST_LOADING,this.onManifestLoading,this),t.off(S.MANIFEST_LOADED,this.onManifestLoaded,this),t.off(S.LEVEL_LOADED,this.onLevelLoaded,this),t.off(S.LEVELS_UPDATED,this.onLevelsUpdated,this),t.off(S.FRAG_BUFFERED,this.onFragBuffered,this),t.off(S.ERROR,this.onError,this)},r.destroy=function(){this._unregisterListeners(),this.steering=null,this.resetLevels(),t.prototype.destroy.call(this)},r.stopLoad=function(){this._levels.forEach((function(t){t.loadError=0,t.fragmentError=0})),t.prototype.stopLoad.call(this)},r.resetLevels=function(){this._startLevel=void 0,this.manualLevelIndex=-1,this.currentLevelIndex=-1,this.currentLevel=null,this._levels=[],this._maxAutoLevel=-1},r.onManifestLoading=function(t,e){this.resetLevels()},r.onManifestLoaded=function(t,e){var r=this.hls.config.preferManagedMediaSource,i=[],n={},a={},s=!1,o=!1,l=!1;e.levels.forEach((function(t){var e,u,h=t.attrs,d=t.audioCodec,c=t.videoCodec;-1!==(null==(e=d)?void 0:e.indexOf("mp4a.40.34"))&&(js||(js=/chrome|firefox/i.test(navigator.userAgent)),js&&(t.audioCodec=d=void 0)),d&&(t.audioCodec=d=ue(d,r)),0===(null==(u=c)?void 0:u.indexOf("avc1"))&&(c=t.videoCodec=function(t){var e=t.split(".");if(e.length>2){var r=e.shift()+".";return(r+=parseInt(e.shift()).toString(16))+("000"+parseInt(e.shift()).toString(16)).slice(-4)}return t}(c));var f=t.width,g=t.height,v=t.unknownCodecs;if(s||(s=!(!f||!g)),o||(o=!!c),l||(l=!!d),!(null!=v&&v.length||d&&!re(d,"audio",r)||c&&!re(c,"video",r))){var m=h.CODECS,p=h["FRAME-RATE"],y=h["HDCP-LEVEL"],E=h["PATHWAY-ID"],T=h.RESOLUTION,S=h["VIDEO-RANGE"],L=(E||".")+"-"+t.bitrate+"-"+T+"-"+p+"-"+m+"-"+S+"-"+y;if(n[L])if(n[L].uri===t.url||t.attrs["PATHWAY-ID"])n[L].addGroupId("audio",h.AUDIO),n[L].addGroupId("text",h.SUBTITLES);else{var A=a[L]+=1;t.attrs["PATHWAY-ID"]=new Array(A+1).join(".");var R=new tr(t);n[L]=R,i.push(R)}else{var k=new tr(t);n[L]=k,a[L]=1,i.push(k)}}})),this.filterAndSortMediaOptions(i,e,s,o,l)},r.filterAndSortMediaOptions=function(t,e,r,i,n){var a=this,s=[],o=[],l=t;if((r||i)&&n&&(l=l.filter((function(t){var e,r=t.videoCodec,i=t.videoRange,n=t.width,a=t.height;return(!!r||!(!n||!a))&&!!(e=i)&&ze.indexOf(e)>-1}))),0!==l.length){if(e.audioTracks){var u=this.hls.config.preferManagedMediaSource;Zs(s=e.audioTracks.filter((function(t){return!t.audioCodec||re(t.audioCodec,"audio",u)})))}e.subtitles&&Zs(o=e.subtitles);var h=l.slice(0);l.sort((function(t,e){if(t.attrs["HDCP-LEVEL"]!==e.attrs["HDCP-LEVEL"])return(t.attrs["HDCP-LEVEL"]||"")>(e.attrs["HDCP-LEVEL"]||"")?1:-1;if(r&&t.height!==e.height)return t.height-e.height;if(t.frameRate!==e.frameRate)return t.frameRate-e.frameRate;if(t.videoRange!==e.videoRange)return ze.indexOf(t.videoRange)-ze.indexOf(e.videoRange);if(t.videoCodec!==e.videoCodec){var i=ae(t.videoCodec),n=ae(e.videoCodec);if(i!==n)return n-i}if(t.uri===e.uri&&t.codecSet!==e.codecSet){var a=se(t.codecSet),s=se(e.codecSet);if(a!==s)return s-a}return t.bitrate!==e.bitrate?t.bitrate-e.bitrate:0}));var d=h[0];if(this.steering&&(l=this.steering.filterParsedLevels(l)).length!==h.length)for(var c=0;cm&&m===zs.abrEwmaDefaultEstimate&&(this.hls.bandwidthEstimate=p)}break}var y=n&&!i,E={levels:l,audioTracks:s,subtitleTracks:o,sessionData:e.sessionData,sessionKeys:e.sessionKeys,firstLevel:this._firstLevel,stats:e.stats,audio:n,video:i,altAudio:!y&&s.some((function(t){return!!t.url}))};this.hls.trigger(S.MANIFEST_PARSED,E),(this.hls.config.autoStartLoad||this.hls.forceStartLoad)&&this.hls.startLoad(this.hls.config.startPosition)}else Promise.resolve().then((function(){if(a.hls){e.levels.length&&a.warn("One or more CODECS in variant not supported: "+JSON.stringify(e.levels[0].attrs));var t=new Error("no level with compatible codecs found in manifest");a.hls.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.MANIFEST_INCOMPATIBLE_CODECS_ERROR,fatal:!0,url:e.url,error:t,reason:t.message})}}))},r.onError=function(t,e){!e.fatal&&e.context&&e.context.type===ke&&e.context.level===this.level&&this.checkRetry(e)},r.onFragBuffered=function(t,e){var r=e.frag;if(void 0!==r&&r.type===Ie){var i=r.elementaryStreams;if(!Object.keys(i).some((function(t){return!!i[t]})))return;var n=this._levels[r.level];null!=n&&n.loadError&&(this.log("Resetting level error count of "+n.loadError+" on frag buffered"),n.loadError=0)}},r.onLevelLoaded=function(t,e){var r,i,n=e.level,a=e.details,s=this._levels[n];if(!s)return this.warn("Invalid level index "+n),void(null!=(i=e.deliveryDirectives)&&i.skip&&(a.deltaUpdateFailed=!0));n===this.currentLevelIndex?(0===s.fragmentError&&(s.loadError=0),this.playlistLoaded(n,e,s.details)):null!=(r=e.deliveryDirectives)&&r.skip&&(a.deltaUpdateFailed=!0)},r.loadPlaylist=function(e){t.prototype.loadPlaylist.call(this);var r=this.currentLevelIndex,i=this.currentLevel;if(i&&this.shouldLoadPlaylist(i)){var n=i.uri;if(e)try{n=e.addDirectives(n)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}var a=i.attrs["PATHWAY-ID"];this.log("Loading level index "+r+(void 0!==(null==e?void 0:e.msn)?" at sn "+e.msn+" part "+e.part:"")+" with"+(a?" Pathway "+a:"")+" "+n),this.clearTimer(),this.hls.trigger(S.LEVEL_LOADING,{url:n,level:r,pathwayId:i.attrs["PATHWAY-ID"],id:0,deliveryDirectives:e||null})}},r.removeLevel=function(t){var e,r=this,i=this._levels.filter((function(e,i){return i!==t||(r.steering&&r.steering.removeLevel(e),e===r.currentLevel&&(r.currentLevel=null,r.currentLevelIndex=-1,e.details&&e.details.fragments.forEach((function(t){return t.level=-1}))),!1)}));ur(i),this._levels=i,this.currentLevelIndex>-1&&null!=(e=this.currentLevel)&&e.details&&(this.currentLevelIndex=this.currentLevel.details.fragments[0].level),this.hls.trigger(S.LEVELS_UPDATED,{levels:i})},r.onLevelsUpdated=function(t,e){var r=e.levels;this._levels=r},r.checkMaxAutoUpdated=function(){var t=this.hls,e=t.autoLevelCapping,r=t.maxAutoLevel,i=t.maxHdcpLevel;this._maxAutoLevel!==r&&(this._maxAutoLevel=r,this.hls.trigger(S.MAX_AUTO_LEVEL_UPDATED,{autoLevelCapping:e,levels:this.levels,maxAutoLevel:r,minAutoLevel:this.hls.minAutoLevel,maxHdcpLevel:i}))},s(e,[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e=this._levels;if(0!==e.length){if(t<0||t>=e.length){var r=new Error("invalid level idx"),i=t<0;if(this.hls.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.LEVEL_SWITCH_ERROR,level:t,fatal:i,error:r,reason:r.message}),i)return;t=Math.min(t,e.length-1)}var n=this.currentLevelIndex,a=this.currentLevel,s=a?a.attrs["PATHWAY-ID"]:void 0,o=e[t],l=o.attrs["PATHWAY-ID"];if(this.currentLevelIndex=t,this.currentLevel=o,n!==t||!o.details||!a||s!==l){this.log("Switching to level "+t+" ("+(o.height?o.height+"p ":"")+(o.videoRange?o.videoRange+" ":"")+(o.codecSet?o.codecSet+" ":"")+"@"+o.bitrate+")"+(l?" with Pathway "+l:"")+" from level "+n+(s?" with Pathway "+s:""));var u={level:t,attrs:o.attrs,details:o.details,bitrate:o.bitrate,averageBitrate:o.averageBitrate,maxBitrate:o.maxBitrate,realBitrate:o.realBitrate,width:o.width,height:o.height,codecSet:o.codecSet,audioCodec:o.audioCodec,videoCodec:o.videoCodec,audioGroups:o.audioGroups,subtitleGroups:o.subtitleGroups,loaded:o.loaded,loadError:o.loadError,fragmentError:o.fragmentError,name:o.name,id:o.id,uri:o.uri,url:o.url,urlId:0,audioGroupIds:o.audioGroupIds,textGroupIds:o.textGroupIds};this.hls.trigger(S.LEVEL_SWITCHING,u);var h=o.details;if(!h||h.live){var d=this.switchParams(o.uri,null==a?void 0:a.details);this.loadPlaylist(d)}}}}},{key:"manualLevel",get:function(){return this.manualLevelIndex},set:function(t){this.manualLevelIndex=t,void 0===this._startLevel&&(this._startLevel=t),-1!==t&&(this.level=t)}},{key:"firstLevel",get:function(){return this._firstLevel},set:function(t){this._firstLevel=t}},{key:"startLevel",get:function(){if(void 0===this._startLevel){var t=this.hls.config.startLevel;return void 0!==t?t:this.hls.firstAutoLevel}return this._startLevel},set:function(t){this._startLevel=t}},{key:"nextLoadLevel",get:function(){return-1!==this.manualLevelIndex?this.manualLevelIndex:this.hls.nextAutoLevel},set:function(t){this.level=t,-1===this.manualLevelIndex&&(this.hls.nextAutoLevel=t)}}]),e}(Dr);function Zs(t){var e={};t.forEach((function(t){var r=t.groupId||"";t.id=e[r]=e[r]||0,e[r]++}))}var to=function(){function t(t){this.config=void 0,this.keyUriToKeyInfo={},this.emeController=null,this.config=t}var e=t.prototype;return e.abort=function(t){for(var e in this.keyUriToKeyInfo){var r=this.keyUriToKeyInfo[e].loader;if(r){var i;if(t&&t!==(null==(i=r.context)?void 0:i.frag.type))return;r.abort()}}},e.detach=function(){for(var t in this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t];(e.mediaKeySessionContext||e.decryptdata.isCommonEncryption)&&delete this.keyUriToKeyInfo[t]}},e.destroy=function(){for(var t in this.detach(),this.keyUriToKeyInfo){var e=this.keyUriToKeyInfo[t].loader;e&&e.destroy()}this.keyUriToKeyInfo={}},e.createKeyLoadError=function(t,e,r,i,n){return void 0===e&&(e=A.KEY_LOAD_ERROR),new si({type:L.NETWORK_ERROR,details:e,fatal:!1,frag:t,response:n,error:r,networkDetails:i})},e.loadClear=function(t,e){var r=this;if(this.emeController&&this.config.emeEnabled)for(var i=t.sn,n=t.cc,a=function(){var t=e[s];if(n<=t.cc&&("initSegment"===i||"initSegment"===t.sn||i2,c=!h||e&&e.start<=a||h-a>2&&!this.fragmentTracker.getPartialFragment(a);if(d||c)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var f;if(!(u.len>0||h))return;var g=Math.max(h,u.start||0)-a,v=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,m=(null==v||null==(f=v.details)?void 0:f.live)?2*v.details.targetduration:2,p=this.fragmentTracker.getPartialFragment(a);if(g>0&&(g<=m||p))return void(i.paused||this._trySkipBufferHole(p))}var y=self.performance.now();if(null!==n){var E=y-n;if(s||!(E>=250)||(this._reportStall(u),this.media)){var T=zr.bufferInfo(i,a,r.maxBufferHole);this._tryFixBufferStall(T,E)}}else this.stalled=y}else if(this.moved=!0,s||(this.nudgeRetry=0),null!==n){if(this.stallReported){var S=self.performance.now()-n;w.warn("playback not stuck anymore @"+a+", after "+Math.round(S)+"ms"),this.stallReported=!1}this.stalled=null}}},e._tryFixBufferStall=function(t,e){var r=this.config,i=this.fragmentTracker,n=this.media;if(null!==n){var a=n.currentTime,s=i.getPartialFragment(a);if(s&&(this._trySkipBufferHole(s)||!this.media))return;(t.len>r.maxBufferHole||t.nextStart&&t.nextStart-a1e3*r.highBufferWatchdogPeriod&&(w.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())}},e._reportStall=function(t){var e=this.hls,r=this.media;if(!this.stallReported&&r){this.stallReported=!0;var i=new Error("Playback stalling at @"+r.currentTime+" due to low buffer ("+JSON.stringify(t)+")");w.warn(i.message),e.trigger(S.ERROR,{type:L.MEDIA_ERROR,details:A.BUFFER_STALLED_ERROR,fatal:!1,error:i,buffer:t.len})}},e._trySkipBufferHole=function(t){var e=this.config,r=this.hls,i=this.media;if(null===i)return 0;var n=i.currentTime,a=zr.bufferInfo(i,n,0),s=n0&&a.len<1&&i.readyState<3,u=s-n;if(u>0&&(o||l)){if(u>e.maxBufferHole){var h=this.fragmentTracker,d=!1;if(0===n){var c=h.getAppendedFrag(0,Ie);c&&s1?(i=0,this.bitrateTest=!0):i=r.firstAutoLevel),this.level=r.nextLoadLevel=i,this.loadedmetadata=!1}e>0&&-1===t&&(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e),this.state=fi,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=ci},r.stopLoad=function(){this._forceStartLoad=!1,t.prototype.stopLoad.call(this)},r.doTick=function(){switch(this.state){case Ai:var t=this.levels,e=this.level,r=null==t?void 0:t[e],i=null==r?void 0:r.details;if(i&&(!i.live||this.levelLastLoaded===r)){if(this.waitForCdnTuneIn(i))break;this.state=fi;break}if(this.hls.nextLoadLevel!==this.level){this.state=fi;break}break;case mi:var n,a=self.performance.now(),s=this.retryDate;if(!s||a>=s||null!=(n=this.media)&&n.seeking){var o=this.levels,l=this.level,u=null==o?void 0:o[l];this.resetStartWhenNotLoaded(u||null),this.state=fi}}this.state===fi&&this.doTickIdle(),this.onTickEnd()},r.onTickEnd=function(){t.prototype.onTickEnd.call(this),this.checkBuffer(),this.checkFragmentChanged()},r.doTickIdle=function(){var t=this.hls,e=this.levelLastLoaded,r=this.levels,i=this.media,n=t.config,a=t.nextLoadLevel;if(null!==e&&(i||!this.startFragRequested&&n.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)&&null!=r&&r[a]){var s=r[a],o=this.getMainFwdBufferInfo();if(null!==o){var l=this.getLevelDetails();if(l&&this._streamEnded(o,l)){var u={};return this.altAudio&&(u.type="video"),this.hls.trigger(S.BUFFER_EOS,u),void(this.state=Ti)}t.loadLevel!==a&&-1===t.manualLevel&&this.log("Adapting to level "+a+" from level "+this.level),this.level=t.nextLoadLevel=a;var h=s.details;if(!h||this.state===Ai||h.live&&this.levelLastLoaded!==s)return this.level=a,void(this.state=Ai);var d=o.len,c=this.getMaxBufferLength(s.maxBitrate);if(!(d>=c)){this.backtrackFragment&&this.backtrackFragment.start>o.end&&(this.backtrackFragment=null);var f=this.backtrackFragment?this.backtrackFragment.start:o.end,g=this.getNextFragment(f,h);if(this.couldBacktrack&&!this.fragPrevious&&g&&"initSegment"!==g.sn&&this.fragmentTracker.getState(g)!==Yr){var v,m=(null!=(v=this.backtrackFragment)?v:g).sn-h.startSN,p=h.fragments[m-1];p&&g.cc===p.cc&&(g=p,this.fragmentTracker.removeFragment(p))}else this.backtrackFragment&&o.len&&(this.backtrackFragment=null);if(g&&this.isLoopLoading(g,f)){if(!g.gap){var y=this.audioOnly&&!this.altAudio?O:N,E=(y===N?this.videoBuffer:this.mediaBuffer)||this.media;E&&this.afterBufferFlushed(E,y,Ie)}g=this.getNextFragmentLoopLoading(g,h,o,Ie,c)}g&&(!g.initSegment||g.initSegment.data||this.bitrateTest||(g=g.initSegment),this.loadFragment(g,s,f))}}}},r.loadFragment=function(e,r,i){var n=this.fragmentTracker.getState(e);this.fragCurrent=e,n===Kr||n===Vr?"initSegment"===e.sn?this._loadInitSegment(e,r):this.bitrateTest?(this.log("Fragment "+e.sn+" of level "+e.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(e,r)):(this.startFragRequested=!0,t.prototype.loadFragment.call(this,e,r,i)):this.clearTrackerIfNeeded(e)},r.getBufferedFrag=function(t){return this.fragmentTracker.getBufferedFrag(t,Ie)},r.followingBufferedFrag=function(t){return t?this.getBufferedFrag(t.end+.5):null},r.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},r.nextLevelSwitch=function(){var t=this.levels,e=this.media;if(null!=e&&e.readyState){var r,i=this.getAppendedFrag(e.currentTime);i&&i.start>1&&this.flushMainBuffer(0,i.start-1);var n=this.getLevelDetails();if(null!=n&&n.live){var a=this.getMainFwdBufferInfo();if(!a||a.len<2*n.targetduration)return}if(!e.paused&&t){var s=t[this.hls.nextLoadLevel],o=this.fragLastKbps;r=o&&this.fragCurrent?this.fragCurrent.duration*s.maxBitrate/(1e3*o)+1:0}else r=0;var l=this.getBufferedFrag(e.currentTime+r);if(l){var u=this.followingBufferedFrag(l);if(u){this.abortCurrentFrag();var h=u.maxStartPTS?u.maxStartPTS:u.start,d=u.duration,c=Math.max(l.end,h+Math.min(Math.max(d-this.config.maxFragLookUpTolerance,d*(this.couldBacktrack?.5:.125)),d*(this.couldBacktrack?.75:.25)));this.flushMainBuffer(c,Number.POSITIVE_INFINITY)}}}},r.abortCurrentFrag=function(){var t=this.fragCurrent;switch(this.fragCurrent=null,this.backtrackFragment=null,t&&(t.abortRequests(),this.fragmentTracker.removeFragment(t)),this.state){case gi:case vi:case mi:case yi:case Ei:this.state=fi}this.nextLoadPosition=this.getLoadPosition()},r.flushMainBuffer=function(e,r){t.prototype.flushMainBuffer.call(this,e,r,this.altAudio?"video":null)},r.onMediaAttached=function(e,r){t.prototype.onMediaAttached.call(this,e,r);var i=r.media;this.onvplaying=this.onMediaPlaying.bind(this),this.onvseeked=this.onMediaSeeked.bind(this),i.addEventListener("playing",this.onvplaying),i.addEventListener("seeked",this.onvseeked),this.gapController=new io(this.config,i,this.fragmentTracker,this.hls)},r.onMediaDetaching=function(){var e=this.media;e&&this.onvplaying&&this.onvseeked&&(e.removeEventListener("playing",this.onvplaying),e.removeEventListener("seeked",this.onvseeked),this.onvplaying=this.onvseeked=null,this.videoBuffer=null),this.fragPlaying=null,this.gapController&&(this.gapController.destroy(),this.gapController=null),t.prototype.onMediaDetaching.call(this)},r.onMediaPlaying=function(){this.tick()},r.onMediaSeeked=function(){var t=this.media,e=t?t.currentTime:null;y(e)&&this.log("Media seeked to "+e.toFixed(3));var r=this.getMainFwdBufferInfo();null!==r&&0!==r.len?this.tick():this.warn('Main forward buffer length on "seeked" event '+(r?r.len:"empty")+")")},r.onManifestLoading=function(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(S.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=!1,this.startPosition=this.lastCurrentTime=this.fragLastKbps=0,this.levels=this.fragPlaying=this.backtrackFragment=this.levelLastLoaded=null,this.altAudio=this.audioOnly=this.startFragRequested=!1},r.onManifestParsed=function(t,e){var r,i,n=!1,a=!1;e.levels.forEach((function(t){var e=t.audioCodec;e&&(n=n||-1!==e.indexOf("mp4a.40.2"),a=a||-1!==e.indexOf("mp4a.40.5"))})),this.audioCodecSwitch=n&&a&&!("function"==typeof(null==(i=eo())||null==(r=i.prototype)?void 0:r.changeType)),this.audioCodecSwitch&&this.log("Both AAC/HE-AAC audio found in levels; declaring level codec as HE-AAC"),this.levels=e.levels,this.startFragRequested=!1},r.onLevelLoading=function(t,e){var r=this.levels;if(r&&this.state===fi){var i=r[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==i||this.waitForCdnTuneIn(i.details))&&(this.state=Ai)}},r.onLevelLoaded=function(t,e){var r,i=this.levels,n=e.level,a=e.details,s=a.totalduration;if(i){this.log("Level "+n+" loaded ["+a.startSN+","+a.endSN+"]"+(a.lastPartSn?"[part-"+a.lastPartSn+"-"+a.lastPartIndex+"]":"")+", cc ["+a.startCC+", "+a.endCC+"] duration:"+s);var o=i[n],l=this.fragCurrent;!l||this.state!==vi&&this.state!==mi||l.level!==e.level&&l.loader&&this.abortCurrentFrag();var u=0;if(a.live||null!=(r=o.details)&&r.live){var h;if(this.checkLiveUpdate(a),a.deltaUpdateFailed)return;u=this.alignPlaylists(a,o.details,null==(h=this.levelLastLoaded)?void 0:h.details)}if(o.details=a,this.levelLastLoaded=o,this.hls.trigger(S.LEVEL_UPDATED,{details:a,level:n}),this.state===Ai){if(this.waitForCdnTuneIn(a))return;this.state=fi}this.startFragRequested?a.live&&this.synchronizeToLiveEdge(a):this.setStartPosition(a,u),this.tick()}else this.warn("Levels were reset while loading level "+n)},r._handleFragmentLoadProgress=function(t){var e,r=t.frag,i=t.part,n=t.payload,a=this.levels;if(a){var s=a[r.level],o=s.details;if(!o)return this.warn("Dropping fragment "+r.sn+" of level "+r.level+" after level details were reset"),void this.fragmentTracker.removeFragment(r);var l=s.videoCodec,u=o.PTSKnown||!o.live,h=null==(e=r.initSegment)?void 0:e.data,d=this._getAudioCodec(s),c=this.transmuxer=this.transmuxer||new Bn(this.hls,Ie,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),f=i?i.index:-1,g=-1!==f,v=new Qr(r.level,r.sn,r.stats.chunkCount,n.byteLength,f,g),m=this.initPTS[r.cc];c.push(n,h,d,l,r,i,o.totalduration,u,v,m)}else this.warn("Levels were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},r.onAudioTrackSwitching=function(t,e){var r=this.altAudio;if(!e.url){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;var i=this.fragCurrent;i&&(this.log("Switching to main audio track, cancel main fragment load"),i.abortRequests(),this.fragmentTracker.removeFragment(i)),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();var n=this.hls;r&&(n.trigger(S.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:null}),this.fragmentTracker.removeAllFragments()),n.trigger(S.AUDIO_TRACK_SWITCHED,e)}},r.onAudioTrackSwitched=function(t,e){var r=e.id,i=!!this.hls.audioTracks[r].url;if(i){var n=this.videoBuffer;n&&this.mediaBuffer!==n&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=n)}this.altAudio=i,this.tick()},r.onBufferCreated=function(t,e){var r,i,n=e.tracks,a=!1;for(var s in n){var o=n[s];if("main"===o.id){if(i=s,r=o,"video"===s){var l=n[s];l&&(this.videoBuffer=l.buffer)}}else a=!0}a&&r?(this.log("Alternate track found, use "+i+".buffered to schedule main fragment loading"),this.mediaBuffer=r.buffer):this.mediaBuffer=this.media},r.onFragBuffered=function(t,e){var r=e.frag,i=e.part;if(!r||r.type===Ie){if(this.fragContextChanged(r))return this.warn("Fragment "+r.sn+(i?" p: "+i.index:"")+" of level "+r.level+" finished buffering, but was aborted. state: "+this.state),void(this.state===Ei&&(this.state=fi));var n=i?i.stats:r.stats;this.fragLastKbps=Math.round(8*n.total/(n.buffering.end-n.loading.first)),"initSegment"!==r.sn&&(this.fragPrevious=r),this.fragBufferedComplete(r,i)}},r.onError=function(t,e){var r;if(e.fatal)this.state=Si;else switch(e.details){case A.FRAG_GAP:case A.FRAG_PARSING_ERROR:case A.FRAG_DECRYPT_ERROR:case A.FRAG_LOAD_ERROR:case A.FRAG_LOAD_TIMEOUT:case A.KEY_LOAD_ERROR:case A.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(Ie,e);break;case A.LEVEL_LOAD_ERROR:case A.LEVEL_LOAD_TIMEOUT:case A.LEVEL_PARSING_ERROR:e.levelRetry||this.state!==Ai||(null==(r=e.context)?void 0:r.type)!==ke||(this.state=fi);break;case A.BUFFER_APPEND_ERROR:case A.BUFFER_FULL_ERROR:if(!e.parent||"main"!==e.parent)return;if(e.details===A.BUFFER_APPEND_ERROR)return void this.resetLoadingState();this.reduceLengthAndFlushBuffer(e)&&this.flushMainBuffer(0,Number.POSITIVE_INFINITY);break;case A.INTERNAL_EXCEPTION:this.recoverWorkerError(e)}},r.checkBuffer=function(){var t=this.media,e=this.gapController;if(t&&e&&t.readyState){if(this.loadedmetadata||!zr.getBuffered(t).length){var r=this.state!==fi?this.fragCurrent:null;e.poll(this.lastCurrentTime,r)}this.lastCurrentTime=t.currentTime}},r.onFragLoadEmergencyAborted=function(){this.state=fi,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()},r.onBufferFlushed=function(t,e){var r=e.type;if(r!==O||this.audioOnly&&!this.altAudio){var i=(r===N?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(i,r,Ie),this.tick()}},r.onLevelsUpdated=function(t,e){this.level>-1&&this.fragCurrent&&(this.level=this.fragCurrent.level),this.levels=e.levels},r.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},r.seekToStartPos=function(){var t=this.media;if(t){var e=t.currentTime,r=this.startPosition;if(r>=0&&e0&&(nT.cc;if(!1!==n.independent){var R=h.startPTS,k=h.endPTS,b=h.startDTS,D=h.endDTS;if(l)l.elementaryStreams[h.type]={startPTS:R,endPTS:k,startDTS:b,endDTS:D};else if(h.firstKeyFrame&&h.independent&&1===a.id&&!A&&(this.couldBacktrack=!0),h.dropped&&h.independent){var I=this.getMainFwdBufferInfo(),w=(I?I.end:this.getLoadPosition())+this.config.maxBufferHole,C=h.firstKeyFramePTS?h.firstKeyFramePTS:R;if(!L&&w2&&(o.gap=!0);o.setElementaryStreamInfo(h.type,R,k,b,D),this.backtrackFragment&&(this.backtrackFragment=o),this.bufferFragmentData(h,o,l,a,L||A)}else{if(!L&&!A)return void this.backtrack(o);o.gap=!0}}if(v){var _=v.startPTS,x=v.endPTS,P=v.startDTS,F=v.endDTS;l&&(l.elementaryStreams[O]={startPTS:_,endPTS:x,startDTS:P,endDTS:F}),o.setElementaryStreamInfo(O,_,x,P,F),this.bufferFragmentData(v,o,l,a)}if(g&&null!=c&&null!=(e=c.samples)&&e.length){var M={id:r,frag:o,details:g,samples:c.samples};i.trigger(S.FRAG_PARSING_METADATA,M)}if(g&&d){var N={id:r,frag:o,details:g,samples:d.samples};i.trigger(S.FRAG_PARSING_USERDATA,N)}}}else this.resetWhenMissingContext(a)},r._bufferInitSegment=function(t,e,r,i){var n=this;if(this.state===yi){this.audioOnly=!!e.audio&&!e.video,this.altAudio&&!this.audioOnly&&delete e.audio;var a=e.audio,s=e.video,o=e.audiovideo;if(a){var l=t.audioCodec,u=navigator.userAgent.toLowerCase();this.audioCodecSwitch&&(l&&(l=-1!==l.indexOf("mp4a.40.5")?"mp4a.40.2":"mp4a.40.5"),1!==a.metadata.channelCount&&-1===u.indexOf("firefox")&&(l="mp4a.40.5")),l&&-1!==l.indexOf("mp4a.40.5")&&-1!==u.indexOf("android")&&"audio/mpeg"!==a.container&&(l="mp4a.40.2",this.log("Android: force audio codec to "+l)),t.audioCodec&&t.audioCodec!==l&&this.log('Swapping manifest audio codec "'+t.audioCodec+'" for "'+l+'"'),a.levelCodec=l,a.id="main",this.log("Init audio buffer, container:"+a.container+", codecs[selected/level/parsed]=["+(l||"")+"/"+(t.audioCodec||"")+"/"+a.codec+"]")}s&&(s.levelCodec=t.videoCodec,s.id="main",this.log("Init video buffer, container:"+s.container+", codecs[level/parsed]=["+(t.videoCodec||"")+"/"+s.codec+"]")),o&&this.log("Init audiovideo buffer, container:"+o.container+", codecs[level/parsed]=["+t.codecs+"/"+o.codec+"]"),this.hls.trigger(S.BUFFER_CODECS,e),Object.keys(e).forEach((function(t){var a=e[t].initSegment;null!=a&&a.byteLength&&n.hls.trigger(S.BUFFER_APPENDING,{type:t,data:a,frag:r,part:null,chunkMeta:i,parent:r.type})})),this.tickImmediate()}},r.getMainFwdBufferInfo=function(){return this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:this.media,Ie)},r.backtrack=function(t){this.couldBacktrack=!0,this.backtrackFragment=t,this.resetTransmuxer(),this.flushBufferGap(t),this.fragmentTracker.removeFragment(t),this.fragPrevious=null,this.nextLoadPosition=t.start,this.state=fi},r.checkFragmentChanged=function(){var t=this.media,e=null;if(t&&t.readyState>1&&!1===t.seeking){var r=t.currentTime;if(zr.isBuffered(t,r)?e=this.getAppendedFrag(r):zr.isBuffered(t,r+.1)&&(e=this.getAppendedFrag(r+.1)),e){this.backtrackFragment=null;var i=this.fragPlaying,n=e.level;i&&e.sn===i.sn&&i.level===n||(this.fragPlaying=e,this.hls.trigger(S.FRAG_CHANGED,{frag:e}),i&&i.level===n||this.hls.trigger(S.LEVEL_SWITCHED,{level:n}))}}},s(e,[{key:"nextLevel",get:function(){var t=this.nextBufferedFrag;return t?t.level:-1}},{key:"currentFrag",get:function(){var t=this.media;return t?this.fragPlaying||this.getAppendedFrag(t.currentTime):null}},{key:"currentProgramDateTime",get:function(){var t=this.media;if(t){var e=t.currentTime,r=this.currentFrag;if(r&&y(e)&&y(r.programDateTime)){var i=r.programDateTime+1e3*(e-r.start);return new Date(i)}}return null}},{key:"currentLevel",get:function(){var t=this.currentFrag;return t?t.level:-1}},{key:"nextBufferedFrag",get:function(){var t=this.currentFrag;return t?this.followingBufferedFrag(t):null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}]),e}(Ri),ao=function(){function t(e){void 0===e&&(e={}),this.config=void 0,this.userConfig=void 0,this.coreComponents=void 0,this.networkControllers=void 0,this.started=!1,this._emitter=new Mn,this._autoLevelCapping=-1,this._maxHdcpLevel=null,this.abrController=void 0,this.bufferController=void 0,this.capLevelController=void 0,this.latencyController=void 0,this.levelController=void 0,this.streamController=void 0,this.audioTrackController=void 0,this.subtitleTrackController=void 0,this.emeController=void 0,this.cmcdController=void 0,this._media=null,this.url=null,this.triggeringException=void 0,I(e.debug||!1,"Hls instance");var r=this.config=function(t,e){if((e.liveSyncDurationCount||e.liveMaxLatencyDurationCount)&&(e.liveSyncDuration||e.liveMaxLatencyDuration))throw new Error("Illegal hls.js config: don't mix up liveSyncDurationCount/liveMaxLatencyDurationCount and liveSyncDuration/liveMaxLatencyDuration");if(void 0!==e.liveMaxLatencyDurationCount&&(void 0===e.liveSyncDurationCount||e.liveMaxLatencyDurationCount<=e.liveSyncDurationCount))throw new Error('Illegal hls.js config: "liveMaxLatencyDurationCount" must be greater than "liveSyncDurationCount"');if(void 0!==e.liveMaxLatencyDuration&&(void 0===e.liveSyncDuration||e.liveMaxLatencyDuration<=e.liveSyncDuration))throw new Error('Illegal hls.js config: "liveMaxLatencyDuration" must be greater than "liveSyncDuration"');var r=Qs(t),n=["TimeOut","MaxRetry","RetryDelay","MaxRetryTimeout"];return["manifest","level","frag"].forEach((function(t){var i=("level"===t?"playlist":t)+"LoadPolicy",a=void 0===e[i],s=[];n.forEach((function(n){var o=t+"Loading"+n,l=e[o];if(void 0!==l&&a){s.push(o);var u=r[i].default;switch(e[i]={default:u},n){case"TimeOut":u.maxLoadTimeMs=l,u.maxTimeToFirstByteMs=l;break;case"MaxRetry":u.errorRetry.maxNumRetry=l,u.timeoutRetry.maxNumRetry=l;break;case"RetryDelay":u.errorRetry.retryDelayMs=l,u.timeoutRetry.retryDelayMs=l;break;case"MaxRetryTimeout":u.errorRetry.maxRetryDelayMs=l,u.timeoutRetry.maxRetryDelayMs=l}}})),s.length&&w.warn('hls.js config: "'+s.join('", "')+'" setting(s) are deprecated, use "'+i+'": '+JSON.stringify(e[i]))})),i(i({},r),e)}(t.DefaultConfig,e);this.userConfig=e,r.progressive&&Js(r);var n=r.abrController,a=r.bufferController,s=r.capLevelController,o=r.errorController,l=r.fpsController,u=new o(this),h=this.abrController=new n(this),d=this.bufferController=new a(this),c=this.capLevelController=new s(this),f=new l(this),g=new Pe(this),v=new je(this),m=r.contentSteeringController,p=m?new m(this):null,y=this.levelController=new $s(this,p),E=new Wr(this),T=new to(this.config),L=this.streamController=new no(this,E,T);c.setStreamController(L),f.setStreamController(L);var A=[g,y,L];p&&A.splice(1,0,p),this.networkControllers=A;var R=[h,d,c,f,v,E];this.audioTrackController=this.createController(r.audioTrackController,A);var k=r.audioStreamController;k&&A.push(new k(this,E,T)),this.subtitleTrackController=this.createController(r.subtitleTrackController,A);var b=r.subtitleStreamController;b&&A.push(new b(this,E,T)),this.createController(r.timelineController,R),T.emeController=this.emeController=this.createController(r.emeController,R),this.cmcdController=this.createController(r.cmcdController,R),this.latencyController=this.createController(qe,R),this.coreComponents=R,A.push(u);var D=u.onErrorOut;"function"==typeof D&&this.on(S.ERROR,D,u)}t.isMSESupported=function(){return ro()},t.isSupported=function(){return function(){if(!ro())return!1;var t=te();return"function"==typeof(null==t?void 0:t.isTypeSupported)&&(["avc1.42E01E,mp4a.40.2","av01.0.01M.08","vp09.00.50.08"].some((function(e){return t.isTypeSupported(ne(e,"video"))}))||["mp4a.40.2","fLaC"].some((function(e){return t.isTypeSupported(ne(e,"audio"))})))}()},t.getMediaSource=function(){return te()};var e=t.prototype;return e.createController=function(t,e){if(t){var r=new t(this);return e&&e.push(r),r}return null},e.on=function(t,e,r){void 0===r&&(r=this),this._emitter.on(t,e,r)},e.once=function(t,e,r){void 0===r&&(r=this),this._emitter.once(t,e,r)},e.removeAllListeners=function(t){this._emitter.removeAllListeners(t)},e.off=function(t,e,r,i){void 0===r&&(r=this),this._emitter.off(t,e,r,i)},e.listeners=function(t){return this._emitter.listeners(t)},e.emit=function(t,e,r){return this._emitter.emit(t,e,r)},e.trigger=function(t,e){if(this.config.debug)return this.emit(t,t,e);try{return this.emit(t,t,e)}catch(e){if(w.error("An internal error happened while handling event "+t+'. Error message: "'+e.message+'". Here is a stacktrace:',e),!this.triggeringException){this.triggeringException=!0;var r=t===S.ERROR;this.trigger(S.ERROR,{type:L.OTHER_ERROR,details:A.INTERNAL_EXCEPTION,fatal:r,event:t,error:e}),this.triggeringException=!1}}return!1},e.listenerCount=function(t){return this._emitter.listenerCount(t)},e.destroy=function(){w.log("destroy"),this.trigger(S.DESTROYING,void 0),this.detachMedia(),this.removeAllListeners(),this._autoLevelCapping=-1,this.url=null,this.networkControllers.forEach((function(t){return t.destroy()})),this.networkControllers.length=0,this.coreComponents.forEach((function(t){return t.destroy()})),this.coreComponents.length=0;var t=this.config;t.xhrSetup=t.fetchSetup=void 0,this.userConfig=null},e.attachMedia=function(t){w.log("attachMedia"),this._media=t,this.trigger(S.MEDIA_ATTACHING,{media:t})},e.detachMedia=function(){w.log("detachMedia"),this.trigger(S.MEDIA_DETACHING,void 0),this._media=null},e.loadSource=function(t){this.stopLoad();var e=this.media,r=this.url,i=this.url=p.buildAbsoluteURL(self.location.href,t,{alwaysNormalize:!0});this._autoLevelCapping=-1,this._maxHdcpLevel=null,w.log("loadSource:"+i),e&&r&&(r!==i||this.bufferController.hasSourceTypes())&&(this.detachMedia(),this.attachMedia(e)),this.trigger(S.MANIFEST_LOADING,{url:t})},e.startLoad=function(t){void 0===t&&(t=-1),w.log("startLoad("+t+")"),this.started=!0,this.networkControllers.forEach((function(e){e.startLoad(t)}))},e.stopLoad=function(){w.log("stopLoad"),this.started=!1,this.networkControllers.forEach((function(t){t.stopLoad()}))},e.resumeBuffering=function(){this.started&&this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.startLoad(-1)}))},e.pauseBuffering=function(){this.networkControllers.forEach((function(t){"fragmentLoader"in t&&t.stopLoad()}))},e.swapAudioCodec=function(){w.log("swapAudioCodec"),this.streamController.swapAudioCodec()},e.recoverMediaError=function(){w.log("recoverMediaError");var t=this._media;this.detachMedia(),t&&this.attachMedia(t)},e.removeLevel=function(t){this.levelController.removeLevel(t)},e.setAudioOption=function(t){var e;return null==(e=this.audioTrackController)?void 0:e.setAudioOption(t)},e.setSubtitleOption=function(t){var e;return null==(e=this.subtitleTrackController)||e.setSubtitleOption(t),null},s(t,[{key:"levels",get:function(){var t=this.levelController.levels;return t||[]}},{key:"currentLevel",get:function(){return this.streamController.currentLevel},set:function(t){w.log("set currentLevel:"+t),this.levelController.manualLevel=t,this.streamController.immediateLevelSwitch()}},{key:"nextLevel",get:function(){return this.streamController.nextLevel},set:function(t){w.log("set nextLevel:"+t),this.levelController.manualLevel=t,this.streamController.nextLevelSwitch()}},{key:"loadLevel",get:function(){return this.levelController.level},set:function(t){w.log("set loadLevel:"+t),this.levelController.manualLevel=t}},{key:"nextLoadLevel",get:function(){return this.levelController.nextLoadLevel},set:function(t){this.levelController.nextLoadLevel=t}},{key:"firstLevel",get:function(){return Math.max(this.levelController.firstLevel,this.minAutoLevel)},set:function(t){w.log("set firstLevel:"+t),this.levelController.firstLevel=t}},{key:"startLevel",get:function(){var t=this.levelController.startLevel;return-1===t&&this.abrController.forcedAutoLevel>-1?this.abrController.forcedAutoLevel:t},set:function(t){w.log("set startLevel:"+t),-1!==t&&(t=Math.max(t,this.minAutoLevel)),this.levelController.startLevel=t}},{key:"capLevelToPlayerSize",get:function(){return this.config.capLevelToPlayerSize},set:function(t){var e=!!t;e!==this.config.capLevelToPlayerSize&&(e?this.capLevelController.startCapping():(this.capLevelController.stopCapping(),this.autoLevelCapping=-1,this.streamController.nextLevelSwitch()),this.config.capLevelToPlayerSize=e)}},{key:"autoLevelCapping",get:function(){return this._autoLevelCapping},set:function(t){this._autoLevelCapping!==t&&(w.log("set autoLevelCapping:"+t),this._autoLevelCapping=t,this.levelController.checkMaxAutoUpdated())}},{key:"bandwidthEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimate():NaN},set:function(t){this.abrController.resetEstimator(t)}},{key:"ttfbEstimate",get:function(){var t=this.abrController.bwEstimator;return t?t.getEstimateTTFB():NaN}},{key:"maxHdcpLevel",get:function(){return this._maxHdcpLevel},set:function(t){(function(t){return Xe.indexOf(t)>-1})(t)&&this._maxHdcpLevel!==t&&(this._maxHdcpLevel=t,this.levelController.checkMaxAutoUpdated())}},{key:"autoLevelEnabled",get:function(){return-1===this.levelController.manualLevel}},{key:"manualLevel",get:function(){return this.levelController.manualLevel}},{key:"minAutoLevel",get:function(){var t=this.levels,e=this.config.minAutoBitrate;if(!t)return 0;for(var r=t.length,i=0;i=e)return i;return 0}},{key:"maxAutoLevel",get:function(){var t,e=this.levels,r=this.autoLevelCapping,i=this.maxHdcpLevel;if(t=-1===r&&null!=e&&e.length?e.length-1:r,i)for(var n=t;n--;){var a=e[n].attrs["HDCP-LEVEL"];if(a&&a<=i)return n}return t}},{key:"firstAutoLevel",get:function(){return this.abrController.firstAutoLevel}},{key:"nextAutoLevel",get:function(){return this.abrController.nextAutoLevel},set:function(t){this.abrController.nextAutoLevel=t}},{key:"playingDate",get:function(){return this.streamController.currentProgramDateTime}},{key:"mainForwardBufferInfo",get:function(){return this.streamController.getMainFwdBufferInfo()}},{key:"allAudioTracks",get:function(){var t=this.audioTrackController;return t?t.allAudioTracks:[]}},{key:"audioTracks",get:function(){var t=this.audioTrackController;return t?t.audioTracks:[]}},{key:"audioTrack",get:function(){var t=this.audioTrackController;return t?t.audioTrack:-1},set:function(t){var e=this.audioTrackController;e&&(e.audioTrack=t)}},{key:"allSubtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.allSubtitleTracks:[]}},{key:"subtitleTracks",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTracks:[]}},{key:"subtitleTrack",get:function(){var t=this.subtitleTrackController;return t?t.subtitleTrack:-1},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleTrack=t)}},{key:"media",get:function(){return this._media}},{key:"subtitleDisplay",get:function(){var t=this.subtitleTrackController;return!!t&&t.subtitleDisplay},set:function(t){var e=this.subtitleTrackController;e&&(e.subtitleDisplay=t)}},{key:"lowLatencyMode",get:function(){return this.config.lowLatencyMode},set:function(t){this.config.lowLatencyMode=t}},{key:"liveSyncPosition",get:function(){return this.latencyController.liveSyncPosition}},{key:"latency",get:function(){return this.latencyController.latency}},{key:"maxLatency",get:function(){return this.latencyController.maxLatency}},{key:"targetLatency",get:function(){return this.latencyController.targetLatency}},{key:"drift",get:function(){return this.latencyController.drift}},{key:"forceStartLoad",get:function(){return this.streamController.forceStartLoad}}],[{key:"version",get:function(){return"1.5.1"}},{key:"Events",get:function(){return S}},{key:"ErrorTypes",get:function(){return L}},{key:"ErrorDetails",get:function(){return A}},{key:"DefaultConfig",get:function(){return t.defaultConfig?t.defaultConfig:zs},set:function(e){t.defaultConfig=e}}]),t}();return ao.defaultConfig=void 0,ao},"object"==typeof exports&&"undefined"!=typeof module?module.exports=i():"function"==typeof define&&define.amd?define(i):(r="undefined"!=typeof globalThis?globalThis:r||self).Hls=i()}(!1); +// @license-end diff --git a/public/js/hlsPlayback.js b/public/js/hlsPlayback.js new file mode 100644 index 0000000..5cd46a6 --- /dev/null +++ b/public/js/hlsPlayback.js @@ -0,0 +1,25 @@ +// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 +// SPDX-License-Identifier: AGPL-3.0-only +function playVideo(overlay) { + const video = overlay.parentElement.querySelector('video'); + const url = video.getAttribute("data-url"); + video.setAttribute("controls", ""); + overlay.style.display = "none"; + + if (Hls.isSupported()) { + var hls = new Hls({autoStartLoad: false}); + hls.loadSource(url); + hls.attachMedia(video); + hls.on(Hls.Events.MANIFEST_PARSED, function () { + hls.loadLevel = hls.levels.length - 1; + hls.startLoad(); + video.play(); + }); + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = url; + video.addEventListener('canplay', function() { + video.play(); + }); + } +} +// @license-end diff --git a/public/js/infiniteScroll.js b/public/js/infiniteScroll.js new file mode 100644 index 0000000..1e7d05d --- /dev/null +++ b/public/js/infiniteScroll.js @@ -0,0 +1,77 @@ +// @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 +// SPDX-License-Identifier: AGPL-3.0-only +function insertBeforeLast(node, elem) { + node.insertBefore(elem, node.childNodes[node.childNodes.length - 2]); +} + +function getLoadMore(doc) { + return doc.querySelector(".show-more:not(.timeline-item)"); +} + +function isDuplicate(item, itemClass) { + const tweet = item.querySelector(".tweet-link"); + if (tweet == null) return false; + const href = tweet.getAttribute("href"); + return document.querySelector(itemClass + " .tweet-link[href='" + href + "']") != null; +} + +window.onload = function () { + const url = window.location.pathname; + const isTweet = url.indexOf("/status/") !== -1; + const containerClass = isTweet ? ".replies" : ".timeline"; + const itemClass = containerClass + " > div:not(.top-ref)"; + + var html = document.querySelector("html"); + var container = document.querySelector(containerClass); + var loading = false; + + function handleScroll(failed) { + if (loading) return; + + if (html.scrollTop + html.clientHeight >= html.scrollHeight - 3000) { + loading = true; + var loadMore = getLoadMore(document); + if (loadMore == null) return; + + loadMore.children[0].text = "Loading..."; + + var url = new URL(loadMore.children[0].href); + url.searchParams.append("scroll", "true"); + + fetch(url.toString()).then(function (response) { + if (response.status === 404) throw "error"; + + return response.text(); + }).then(function (html) { + var parser = new DOMParser(); + var doc = parser.parseFromString(html, "text/html"); + loadMore.remove(); + + for (var item of doc.querySelectorAll(itemClass)) { + if (item.className == "timeline-item show-more") continue; + if (isDuplicate(item, itemClass)) continue; + if (isTweet || isEngagement) container.appendChild(item); + else insertBeforeLast(container, item); + } + + loading = false; + const newLoadMore = getLoadMore(doc); + if (newLoadMore == null) return; + if (isTweet || isEngagement) container.appendChild(newLoadMore); + else insertBeforeLast(container, newLoadMore); + }).catch(function (err) { + console.warn("Something went wrong.", err); + if (failed > 3) { + loadMore.children[0].text = "Error"; + return; + } + + loading = false; + handleScroll((failed || 0) + 1); + }); + } + } + + window.addEventListener("scroll", () => handleScroll()); +}; +// @license-end diff --git a/public/logo.png b/public/logo.png new file mode 100644 index 0000000000000000000000000000000000000000..24caa2714eb1b215d3c9188f2696406430cffb2d GIT binary patch literal 1865 zcmb_ddsGu=77x-&YZl_^3CwA6gS)oM%ClXIRwhAt1_%sV6c9qNb<5^Ni+jvM7$Iq~ ziklQuYulu-1i~Zf`XGnW6pe;>%E_kf9ycU&7AmdDai}u0G^4CbgEzo2|= zad(%uU!L3C`eUpq`%aBsB#KPTaRg#L~9l_OYFa+stORSGyXVb0NU3=1j+FZ4Oyhp@0a z_GqFdp4IO)H?QHAK@igKt%N&t28pU@>fRr8yAf>I{zqb*&C+{Eo1fweB4p8_oB?kI ze3D~en%>ecs2o!dB=b|4w1(PXxh7lBEvt%W4O~!daoHtXT$57Q%N5ciR5Gd^xe}vF zPtz5!uos}WT_4&fiLcEVA-43(o)cEXXLJUq%HjH}eUbRoyht1j_+Q#(IWODD74&&d z@u7HRNHu+C$ViUiXQgG70PW^&)fH>ltYIU04%bTys7gzNuM*Yrc0`r-LF(a7vZJbG zLwg;p;ninrb%qvI3}&>SBgN>L?4|Z&GHe~Td4f_;t*;ScF;);hS)lP-_4UR`<;yo7$U79I1)0!uc#4mJ7XY0qrS z6h`l*vWZK$23RFjDbb6M11p!RCA#pLsRfBBkA*qa&rdn`*Q9fHIrE&n00r$=#kg`D zBH8Cl^O&=xrfy}9JgLy%t+!m3v4yp;vc7snMfM$$im6nK)OVE_)sCcc3ab*3L1;Ss zTEP5ndMc-ACf@e3JZ*46Iq&+D1c7VXTkd?Ne3@Ux{Zx>_*9Ru4&( zjc%qyv^!E-2cHh-{Ds;Oh{vIw&v2dO70pPpE+2x_1IyDB9UM14O1Fa0pUD!02S7v9 zLF@_hByq!R>Nk$aGTRTpU5_(B46IcUn|!e4hiK-u_WiIrJ%z)s;!Z@7=N;R3wC{z_ zrla~z?i5?}H7zfhQ~W3sj#n%FYA#Xd$#{%uigF(N)}BWy$D6kb@B0gfnH+D{RFD02 za`90rDTEcjLQ}Qb8eaJNYcbY+u$Gs`5%lpU&XSXk@fYZF>QLbR$+)r%cHeQb3wO~q z)ZqX@b(JM?CwzL1suSB{p9Ek-t_yaIJTcz9v;DsRlyN!9o8?mdjCV72fiMHymugqm zpv%!JAwZT%+`uE@)c)n^a3q$#JNIoCst3UT{Zg->DSWLpF@!z~YF_15kIpa-XjKHl zh*6_#wentGb#^buuwCiERzQq{s8zPiDo{@W7afi*7M5g@+3B5WKEwj z8p#JZMa!u+w7WiChcrR9w>(0CjJBS8INDkhTnjAt$dlHp;5uMszX@0oj6)^9Qqay} z|0*SFfu;AS(S?+Rcwjc^JIVQ?{{b?1!APo|b@`?Q+v$4Y%4W1%ULD*FFxW?L3ta@~ z0qQCt4(tKvFs^Z>NJND!42km;X0UBt97BZO^l6gIsS*|Ch*o%Lci4e1yVNQXAHgD( z!Q6nDN+dfaUOo$p91WBNYN!-Yqaw+6PF@)-M}r^9ceY@*mJ^Lri&k3pBDYNOJ8r!I z!9;vDgaKD(sEZB;V$ai;k4(r7}urcYbLBJ>E*Q Z4`ZM_q9GwGGZpMQMsY!T{zz^^$4{-3>C6BC literal 0 HcmV?d00001 diff --git a/public/lp.svg b/public/lp.svg new file mode 100644 index 0000000..bdae6cc --- /dev/null +++ b/public/lp.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/public/opensearch.xml b/public/opensearch.xml new file mode 100644 index 0000000..f345209 --- /dev/null +++ b/public/opensearch.xml @@ -0,0 +1,9 @@ + + + teapot + Search Twitter via teapot + UTF-8 + /public/favicon.ico + + + diff --git a/public/robots.txt b/public/robots.txt new file mode 100644 index 0000000..812656a --- /dev/null +++ b/public/robots.txt @@ -0,0 +1,5 @@ +User-agent: * +Disallow: / +Crawl-delay: 1 +User-agent: Twitterbot +Disallow: diff --git a/public/safari-pinned-tab.svg b/public/safari-pinned-tab.svg new file mode 100644 index 0000000..2fdb49d --- /dev/null +++ b/public/safari-pinned-tab.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..db53872 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,24 @@ +{ + "name": "teapot", + "short_name": "teapot", + "icons": [ + { + "src": "/android-chrome-192x192.png", + "sizes": "192x192", + "type": "image/png" + }, + { + "src": "/android-chrome-384x384.png", + "sizes": "384x384", + "type": "image/png" + }, + { + "src": "/android-chrome-512x512.png", + "sizes": "512x512", + "type": "image/png" + } + ], + "theme_color": "#333333", + "background_color": "#333333", + "display": "standalone" +} diff --git a/sass/general.scss b/sass/general.scss new file mode 100644 index 0000000..e6247d6 --- /dev/null +++ b/sass/general.scss @@ -0,0 +1,40 @@ +@import "_variables"; +@import "_mixins"; + +.panel-container { + margin: auto; + font-size: 130%; +} + +.error-panel { + @include center-panel(var(--error_red)); + text-align: center; +} + +.search-bar > form { + @include center-panel(var(--darkest_grey)); + + button { + background: var(--bg_elements); + color: var(--fg_color); + border: 0; + border-radius: 3px; + cursor: pointer; + font-weight: bold; + width: 30px; + height: 30px; + padding: 0px 5px 1px 8px; + } + + input { + font-size: 16px; + width: 100%; + background: var(--bg_elements); + color: var(--fg_color); + border: 0; + border-radius: 4px; + padding: 4px; + margin-right: 8px; + height: unset; + } +} diff --git a/sass/include/_mixins.scss b/sass/include/_mixins.scss new file mode 100644 index 0000000..5fde51a --- /dev/null +++ b/sass/include/_mixins.scss @@ -0,0 +1,86 @@ +@import '_variables'; + +@mixin panel($width, $max-width) { + max-width: $max-width; + margin: 0 auto; + float: none; + border-radius: 0; + position: relative; + width: $width; +} + +@mixin play-button { + position: absolute; + width: 100%; + height: 100%; + top: 0; + left: 0; + z-index: 1; + + &:hover { + .overlay-circle { + border-color: var(--play_button_hover); + } + + .overlay-triangle { + border-color: transparent transparent transparent var(--play_button_hover); + } + } +} + +@mixin breakable { + overflow: hidden; + overflow-wrap: break-word; +} + +@mixin ellipsis { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; +} + +@mixin center-panel($bg) { + padding: 12px; + border-radius: 4px; + display: flex; + background: $bg; + box-shadow: 0 0 15px $shadow_dark; + margin: auto; + margin-top: -50px; +} + +@mixin input-colors { + &:hover { + border-color: var(--accent); + } + + &:active { + border-color: var(--accent_light); + } +} + +@mixin search-resize($width, $rows) { + @media(max-width: $width) { + .search-toggles { + grid-template-columns: repeat($rows, auto); + } + + #search-panel-toggle:checked ~ .search-panel { + max-height: 380px !important; + } + } +} + +@mixin create-toggle($elem, $height) { + ##{$elem}-toggle { + display: none; + + &:checked ~ .#{$elem} { + max-height: $height; + } + + &:checked ~ label .icon-down:before { + transform: rotate(180deg) translateY(-1px); + } + } +} diff --git a/sass/include/_variables.scss b/sass/include/_variables.scss new file mode 100644 index 0000000..127cccb --- /dev/null +++ b/sass/include/_variables.scss @@ -0,0 +1,43 @@ +// colors +$bg_color: #0f0f0f; +$fg_color: #f8f8f2; +$fg_faded: #f8f8f2cf; +$fg_dark: #ff6c60; +$fg_nav: #ff6c60; + +$bg_panel: #161616; +$bg_elements: #121212; +$bg_overlays: #1f1f1f; +$bg_hover: #1a1a1a; + +$grey: #888889; +$dark_grey: #404040; +$darker_grey: #282828; +$darkest_grey: #222222; +$border_grey: #3e3e35; + +$accent: #ff6c60; +$accent_light: #ffaca0; +$accent_dark: #8a3731; +$accent_border: #ff6c6091; + +$play_button: #d8574d; +$play_button_hover: #ff6c60; + +$more_replies_dots: #ad433b; +$error_red: #420a05; + +$verified_blue: #1da1f2; +$verified_business: #fac82b; +$verified_government: #c1b6a4; +$icon_text: $fg_color; + +$tab: $fg_color; +$tab_selected: $accent; + +$shadow: rgba(0, 0, 0, 0.6); +$shadow_dark: rgba(0, 0, 0, 0.2); + +//fonts +$font_0: sans-serif; +$font_1: fontello; diff --git a/sass/index.scss b/sass/index.scss new file mode 100644 index 0000000..3f4b123 --- /dev/null +++ b/sass/index.scss @@ -0,0 +1,202 @@ +@import "_variables"; + +@import "tweet/_base"; +@import "profile/_base"; +@import "general"; +@import "navbar"; +@import "inputs"; +@import "timeline"; +@import "search"; + +body { + // colors + --bg_color: #{$bg_color}; + --fg_color: #{$fg_color}; + --fg_faded: #{$fg_faded}; + --fg_dark: #{$fg_dark}; + --fg_nav: #{$fg_nav}; + + --bg_panel: #{$bg_panel}; + --bg_elements: #{$bg_elements}; + --bg_overlays: #{$bg_overlays}; + --bg_hover: #{$bg_hover}; + + --grey: #{$grey}; + --dark_grey: #{$dark_grey}; + --darker_grey: #{$darker_grey}; + --darkest_grey: #{$darkest_grey}; + --border_grey: #{$border_grey}; + + --accent: #{$accent}; + --accent_light: #{$accent_light}; + --accent_dark: #{$accent_dark}; + --accent_border: #{$accent_border}; + + --play_button: #{$play_button}; + --play_button_hover: #{$play_button_hover}; + + --more_replies_dots: #{$more_replies_dots}; + --error_red: #{$error_red}; + + --verified_blue: #{$verified_blue}; + --verified_business: #{$verified_business}; + --verified_government: #{$verified_government}; + --icon_text: #{$icon_text}; + + --tab: #{$fg_color}; + --tab_selected: #{$accent}; + + --profile_stat: #{$fg_color}; + + background-color: var(--bg_color); + color: var(--fg_color); + font-family: $font_0, $font_1; + font-size: 15px; + line-height: 1.3; + margin: 0; +} + +* { + outline: unset; + margin: 0; + text-decoration: none; +} + +h1 { + display: inline; +} + +h2, +h3 { + font-weight: normal; +} + +p { + margin: 14px 0; +} + +a { + color: var(--accent); + + &:hover { + text-decoration: underline; + } +} + +fieldset { + border: 0; + padding: 0; + margin-top: -0.6em; +} + +legend { + width: 100%; + padding: 0.6em 0 0.3em 0; + border: 0; + font-size: 16px; + font-weight: 600; + border-bottom: 1px solid var(--border_grey); + margin-bottom: 8px; +} + +.preferences .note { + border-top: 1px solid var(--border_grey); + border-bottom: 1px solid var(--border_grey); + padding: 6px 0 8px 0; + margin-bottom: 8px; + margin-top: 16px; +} + +ul { + padding-left: 1.3em; +} + +.container { + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + padding-top: 50px; + margin: auto; + min-height: 100vh; +} + +.icon-container { + display: inline; +} + +.overlay-panel { + max-width: 600px; + width: 100%; + margin: 0 auto; + margin-top: 10px; + background-color: var(--bg_overlays); + padding: 10px 15px; + align-self: start; + + ul { + margin-bottom: 14px; + } + + p { + word-break: break-word; + } +} + +.verified-icon { + display: inline-block; + width: 14px; + height: 14px; + margin-left: 2px; + + .verified-icon-circle { + position: absolute; + font-size: 15px; + } + + .verified-icon-check { + position: absolute; + font-size: 9px; + margin: 5px 3px; + } + + &.blue { + .verified-icon-circle { + color: var(--verified_blue); + } + + .verified-icon-check { + color: var(--icon_text); + } + } + + &.business { + .verified-icon-circle { + color: var(--verified_business); + } + + .verified-icon-check { + color: var(--bg_panel); + } + } + + &.government { + .verified-icon-circle { + color: var(--verified_government); + } + + .verified-icon-check { + color: var(--bg_panel); + } + } +} + +@media (max-width: 600px) { + .preferences-container { + max-width: 95vw; + } + + .nav-item, + .nav-item .icon-container { + font-size: 16px; + } +} diff --git a/sass/inputs.scss b/sass/inputs.scss new file mode 100644 index 0000000..aafa5b8 --- /dev/null +++ b/sass/inputs.scss @@ -0,0 +1,203 @@ +@import "_variables"; +@import "_mixins"; + +button { + @include input-colors; + background-color: var(--bg_elements); + color: var(--fg_color); + border: 1px solid var(--accent_border); + padding: 3px 6px; + font-size: 14px; + cursor: pointer; + float: right; +} + +input[type="text"], +input[type="date"], +input[type="number"], +select { + @include input-colors; + background-color: var(--bg_elements); + padding: 1px 4px; + color: var(--fg_color); + border: 1px solid var(--accent_border); + border-radius: 0; + font-size: 14px; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +input[type="text"], +input[type="number"] { + height: 16px; +} + +select { + height: 20px; + padding: 0 2px; + line-height: 1; +} + +input[type="date"]::-webkit-inner-spin-button { + display: none; +} + +input[type="number"] { + -moz-appearance: textfield; +} + +input[type="number"]::-webkit-inner-spin-button, +input[type="number"]::-webkit-outer-spin-button { + display: none; + -webkit-appearance: none; + margin: 0; +} + +input[type="date"]::-webkit-clear-button { + margin-left: 17px; + filter: grayscale(100%); + filter: hue-rotate(120deg); +} + +input::-webkit-calendar-picker-indicator { + opacity: 0; +} + +input::-webkit-datetime-edit-day-field:focus, +input::-webkit-datetime-edit-month-field:focus, +input::-webkit-datetime-edit-year-field:focus { + background-color: var(--accent); + color: var(--fg_color); + outline: none; +} + +.date-range { + .date-input { + display: inline-block; + position: relative; + } + + .icon-container { + pointer-events: none; + position: absolute; + top: 2px; + right: 5px; + } + + .search-title { + margin: 0 2px; + } +} + +.icon-button button { + color: var(--accent); + text-decoration: none; + background: none; + border: none; + float: none; + padding: unset; + padding-left: 4px; + + &:hover { + color: var(--accent_light); + } +} + +.checkbox { + position: absolute; + top: 1px; + right: 0; + height: 17px; + width: 17px; + background-color: var(--bg_elements); + border: 1px solid var(--accent_border); + + &:after { + content: ""; + position: absolute; + display: none; + } +} + +.checkbox-container { + display: block; + position: relative; + margin-bottom: 5px; + cursor: pointer; + user-select: none; + padding-right: 22px; + + input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + + &:checked ~ .checkbox:after { + display: block; + } + } + + &:hover input ~ .checkbox { + border-color: var(--accent); + } + + &:active input ~ .checkbox { + border-color: var(--accent_light); + } + + .checkbox:after { + left: 2px; + bottom: 0; + font-size: 13px; + font-family: $font_1; + content: "\e811"; + } +} + +.pref-group { + display: inline; +} + +.preferences { + button { + margin: 6px 0 3px 0; + } + + label { + padding-right: 150px; + } + + select { + position: absolute; + top: 0; + right: 0; + display: block; + -moz-appearance: none; + -webkit-appearance: none; + appearance: none; + } + + input[type="text"], + input[type="number"] { + position: absolute; + right: 0; + max-width: 140px; + } + + .pref-group { + display: block; + } + + .pref-input { + position: relative; + margin-bottom: 6px; + } + + .pref-reset { + float: left; + } +} diff --git a/sass/navbar.scss b/sass/navbar.scss new file mode 100644 index 0000000..86bfbe7 --- /dev/null +++ b/sass/navbar.scss @@ -0,0 +1,87 @@ +@import "_variables"; + +nav { + display: flex; + align-items: center; + position: fixed; + background-color: var(--bg_overlays); + box-shadow: 0 0 4px $shadow; + padding: 0; + width: 100%; + height: 50px; + z-index: 1000; + font-size: 16px; + + a, + .icon-button button { + color: var(--fg_nav); + } +} + +.inner-nav { + margin: auto; + box-sizing: border-box; + padding: 0 10px; + display: flex; + align-items: center; + flex-basis: 920px; + height: 50px; +} + +.site-name { + font-size: 15px; + font-weight: 600; + line-height: 1; + + &:hover { + color: var(--accent_light); + text-decoration: unset; + } +} + +.site-logo { + display: block; + width: 35px; + height: 35px; +} + +.nav-item { + display: flex; + flex: 1; + line-height: 50px; + height: 50px; + overflow: hidden; + flex-wrap: wrap; + align-items: center; + + &.right { + text-align: right; + justify-content: flex-end; + } + + &.right a:hover { + color: var(--accent_light); + text-decoration: unset; + } +} + +.lp { + height: 14px; + display: inline-block; + position: relative; + top: 2px; + fill: var(--fg_nav); + + &:hover { + fill: var(--accent_light); + } +} + +.icon-info { + margin: 0 -3px; +} + +.icon-cog { + font-size: 15px; + padding-left: 0 !important; +} diff --git a/sass/profile/_base.scss b/sass/profile/_base.scss new file mode 100644 index 0000000..b7f33e6 --- /dev/null +++ b/sass/profile/_base.scss @@ -0,0 +1,83 @@ +@import '_variables'; +@import '_mixins'; + +@import 'card'; +@import 'photo-rail'; + +.profile-tabs { + @include panel(auto, 900px); + + .timeline-container { + float: right; + width: 68% !important; + max-width: unset; + } +} + +.profile-banner { + margin-bottom: 4px; + background-color: var(--bg_panel); + + a { + display: block; + position: relative; + padding: 33.34% 0 0 0; + } + + img { + max-width: 100%; + position: absolute; + top: 0; + } +} + +.profile-tab { + padding: 0 4px 0 0; + box-sizing: border-box; + display: inline-block; + font-size: 14px; + text-align: left; + vertical-align: top; + max-width: 32%; + top: 50px; +} + +.profile-result { + min-height: 54px; + + .username { + margin: 0 !important; + } + + .tweet-header { + margin-bottom: unset; + } +} + +@media(max-width: 700px) { + .profile-tabs { + width: 100vw; + max-width: 600px; + + .timeline-container { + width: 100% !important; + + .tab-item wide { + flex-grow: 1.4; + } + } + } + + .profile-tab { + width: 100%; + max-width: unset; + position: initial !important; + padding: 0; + } +} + +@media (min-height: 900px) { + .profile-tab.sticky { + position: sticky; + } +} diff --git a/sass/profile/card.scss b/sass/profile/card.scss new file mode 100644 index 0000000..46a9679 --- /dev/null +++ b/sass/profile/card.scss @@ -0,0 +1,130 @@ +@import '_variables'; +@import '_mixins'; + +.profile-card { + flex-wrap: wrap; + background: var(--bg_panel); + padding: 12px; + display: flex; +} + +.profile-card-info { + @include breakable; + width: 100%; +} + +.profile-card-tabs-name { + @include breakable; + max-width: 100%; +} + +.profile-card-username { + @include breakable; + color: var(--fg_color); + font-size: 14px; + display: block; +} + +.profile-card-fullname { + @include breakable; + color: var(--fg_color); + font-size: 16px; + font-weight: bold; + text-shadow: none; + max-width: 100%; +} + +.profile-card-avatar { + display: inline-block; + position: relative; + width: 100%; + margin-right: 4px; + margin-bottom: 6px; + + &:after { + content: ''; + display: block; + margin-top: 100%; + } + + img { + box-sizing: border-box; + position: absolute; + width: 100%; + height: 100%; + border: 4px solid var(--darker_grey); + background: var(--bg_panel); + } +} + +.profile-card-extra { + display: contents; + flex: 100%; + margin-top: 7px; + + .profile-bio { + @include breakable; + width: 100%; + margin: 4px -6px 6px 0; + white-space: pre-wrap; + + p { + margin: 0; + } + } + + .profile-joindate, .profile-location, .profile-website { + color: var(--fg_faded); + margin: 1px 0; + width: 100%; + } +} + +.profile-card-extra-links { + margin-top: 8px; + font-size: 14px; + width: 100%; +} + +.profile-statlist { + display: flex; + flex-wrap: wrap; + padding: 0; + width: 100%; + justify-content: space-between; + + li { + display: table-cell; + text-align: center; + } +} + +.profile-stat-header { + font-weight: bold; + color: var(--profile_stat); +} + +.profile-stat-num { + display: block; + color: var(--profile_stat); +} + +@media(max-width: 700px) { + .profile-card-info { + display: flex; + } + + .profile-card-tabs-name { + flex-shrink: 100; + } + + .profile-card-avatar { + width: 80px; + height: 80px; + + img { + border-width: 2px; + width: unset; + } + } +} diff --git a/sass/profile/photo-rail.scss b/sass/profile/photo-rail.scss new file mode 100644 index 0000000..f32d22a --- /dev/null +++ b/sass/profile/photo-rail.scss @@ -0,0 +1,96 @@ +@import '_variables'; + +.photo-rail { + &-card { + float: left; + background: var(--bg_panel); + border-radius: 0 0 4px 4px; + width: 100%; + margin: 5px 0; + } + + &-header { + padding: 5px 12px 0; + } + + &-header-mobile { + display: none; + box-sizing: border-box; + padding: 5px 12px 0; + width: 100%; + float: unset; + color: var(--accent); + justify-content: space-between; + } + + &-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + grid-gap: 3px 3px; + padding: 5px 12px 12px; + + a { + position: relative; + border-radius: 5px; + background-color: var(--darker_grey); + + &:before { + content: ""; + display: block; + padding-top: 100%; + } + } + + img { + height: 100%; + width: 100%; + border-radius: 4px; + object-fit: cover; + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + } + } +} + +@include create-toggle(photo-rail-grid, 640px); +#photo-rail-grid-toggle:checked ~ .photo-rail-grid { + padding-bottom: 12px; +} + +@media(max-width: 700px) { + .photo-rail-header { + display: none; + } + + .photo-rail-header-mobile { + display: flex; + } + + .photo-rail-grid { + max-height: 0; + padding-bottom: 0; + overflow: hidden; + transition: max-height 0.4s; + } + + .photo-rail-grid { + grid-template-columns: repeat(6, 1fr); + } + + #photo-rail-grid-toggle:checked ~ .photo-rail-grid { + max-height: 300px; + } +} + +@media(max-width: 450px) { + .photo-rail-grid { + grid-template-columns: repeat(4, 1fr); + } + + #photo-rail-grid-toggle:checked ~ .photo-rail-grid { + max-height: 450px; + } +} diff --git a/sass/search.scss b/sass/search.scss new file mode 100644 index 0000000..c2adaf7 --- /dev/null +++ b/sass/search.scss @@ -0,0 +1,120 @@ +@import "_variables"; +@import "_mixins"; + +.search-title { + font-weight: bold; + display: inline-block; + margin-top: 4px; +} + +.search-field { + display: flex; + flex-wrap: wrap; + + button { + margin: 0 2px 0 0; + padding: 0px 1px 1px 4px; + height: 23px; + display: flex; + align-items: center; + } + + .pref-input { + margin: 0 4px 0 0; + flex-grow: 1; + height: 23px; + } + + input[type="text"], + input[type="number"] { + height: calc(100% - 4px); + width: calc(100% - 8px); + } + + > label { + display: inline; + background-color: var(--bg_elements); + color: var(--fg_color); + border: 1px solid var(--accent_border); + padding: 1px 1px 2px 4px; + font-size: 14px; + cursor: pointer; + margin-bottom: 2px; + + @include input-colors; + } + + @include create-toggle(search-panel, 380px); +} + +.search-panel { + width: 100%; + max-height: 0; + overflow: hidden; + transition: max-height 0.4s; + + flex-grow: 1; + font-weight: initial; + text-align: left; + + .checkbox-container { + display: inline; + padding-right: unset; + margin-bottom: 5px; + margin-left: 23px; + } + + .checkbox { + right: unset; + left: -22px; + line-height: 1.6em; + } + + .checkbox-container .checkbox:after { + top: -4px; + } +} + +.search-row { + display: flex; + flex-wrap: wrap; + line-height: unset; + + > div { + flex-grow: 1; + flex-shrink: 1; + } + + input { + height: 21px; + } + + .pref-input { + display: block; + padding-bottom: 5px; + + input { + height: 21px; + margin-top: 1px; + } + } +} + +.search-toggles { + flex-grow: 1; + display: grid; + grid-template-columns: repeat(5, auto); + grid-column-gap: 10px; +} + +.profile-tabs { + @include search-resize(820px, 5); + @include search-resize(715px, 4); + @include search-resize(700px, 5); + @include search-resize(485px, 4); + @include search-resize(410px, 3); +} + +@include search-resize(700px, 5); +@include search-resize(485px, 4); +@include search-resize(410px, 3); diff --git a/sass/timeline.scss b/sass/timeline.scss new file mode 100644 index 0000000..40882b2 --- /dev/null +++ b/sass/timeline.scss @@ -0,0 +1,162 @@ +@import "_variables"; + +.timeline-container { + @include panel(100%, 600px); +} + +.timeline { + background-color: var(--bg_panel); + + > div:not(:first-child) { + border-top: 1px solid var(--border_grey); + } +} + +.timeline-header { + width: 100%; + background-color: var(--bg_panel); + text-align: center; + padding: 8px; + display: block; + font-weight: bold; + margin-bottom: 5px; + box-sizing: border-box; + + button { + float: unset; + } +} + +.timeline-banner img { + width: 100%; +} + +.timeline-description { + font-weight: normal; +} + +.tab { + align-items: center; + display: flex; + flex-wrap: wrap; + list-style: none; + margin: 0 0 5px 0; + background-color: var(--bg_panel); + padding: 0; +} + +.tab-item { + flex: 1 1 0; + text-align: center; + margin-top: 0; + + a { + border-bottom: 0.1rem solid transparent; + color: var(--tab); + display: block; + padding: 8px 0; + text-decoration: none; + font-weight: bold; + + &:hover { + text-decoration: none; + } + + &.active { + border-bottom-color: var(--tab_selected); + color: var(--tab_selected); + } + } + + &.active a { + border-bottom-color: var(--tab_selected); + color: var(--tab_selected); + } + + &.wide { + flex-grow: 1.2; + flex-basis: 50px; + } +} + +.timeline-footer { + background-color: var(--bg_panel); + padding: 6px 0; +} + +.timeline-protected { + text-align: center; + + p { + margin: 8px 0; + } + + h2 { + color: var(--accent); + font-size: 20px; + font-weight: 600; + } +} + +.timeline-none { + color: var(--accent); + font-size: 20px; + font-weight: 600; + text-align: center; +} + +.timeline-end { + background-color: var(--bg_panel); + color: var(--accent); + font-size: 16px; + font-weight: 600; + text-align: center; +} + +.show-more { + background-color: var(--bg_panel); + text-align: center; + padding: 0.75em 0; + display: block !important; + + a { + background-color: var(--darkest_grey); + display: inline-block; + height: 2em; + padding: 0 2em; + line-height: 2em; + + &:hover { + background-color: var(--darker_grey); + } + } +} + +.top-ref { + background-color: var(--bg_color); + border-top: none !important; + + .icon-down { + font-size: 20px; + display: flex; + justify-content: center; + text-decoration: none; + + &:hover { + color: var(--accent_light); + } + + &::before { + transform: rotate(180deg) translateY(-1px); + } + } +} + +.timeline-item { + overflow-wrap: break-word; + border-left-width: 0; + min-width: 0; + padding: 0.75em; + display: flex; + position: relative; +} diff --git a/sass/tweet/_base.scss b/sass/tweet/_base.scss new file mode 100644 index 0000000..7f2d931 --- /dev/null +++ b/sass/tweet/_base.scss @@ -0,0 +1,244 @@ +@import "_variables"; +@import "_mixins"; +@import "thread"; +@import "media"; +@import "video"; +@import "embed"; +@import "card"; +@import "poll"; +@import "quote"; + +.tweet-body { + flex: 1; + min-width: 0; + margin-left: 58px; + pointer-events: none; + z-index: 1; +} + +.tweet-content { + line-height: 1.3em; + pointer-events: all; + display: inline; +} + +.tweet-bidi { + display: block !important; +} + +.tweet-header { + padding: 0; + vertical-align: bottom; + flex-basis: 100%; + margin-bottom: 0.2em; + + a { + display: inline-block; + word-break: break-all; + max-width: 100%; + pointer-events: all; + } +} + +.tweet-name-row { + padding: 0; + display: flex; + justify-content: space-between; +} + +.fullname-and-username { + display: flex; + min-width: 0; +} + +.fullname { + @include ellipsis; + flex-shrink: 2; + max-width: 80%; + font-size: 14px; + font-weight: 700; + color: var(--fg_color); +} + +.username { + @include ellipsis; + min-width: 1.6em; + margin-left: 0.4em; + word-wrap: normal; +} + +.tweet-date { + display: flex; + flex-shrink: 0; + margin-left: 4px; +} + +.tweet-date a, +.username, +.show-more a { + color: var(--fg_dark); +} + +.tweet-published { + margin: 0; + margin-top: 5px; + color: var(--grey); + pointer-events: all; +} + +.tweet-avatar { + display: contents !important; + + img { + float: left; + margin-top: 3px; + margin-left: -58px; + width: 48px; + height: 48px; + } +} + +.avatar { + &.round { + border-radius: 50%; + -webkit-user-select: none; + } + + &.mini { + position: unset; + margin-right: 5px; + margin-top: -1px; + width: 20px; + height: 20px; + } +} + +.tweet-embed { + display: flex; + flex-direction: column; + justify-content: center; + height: 100%; + background-color: var(--bg_panel); + + .tweet-content { + font-size: 18px; + } + + .tweet-body { + display: flex; + flex-direction: column; + max-height: calc(100vh - 0.75em * 2); + } + + .card-image img { + height: auto; + } + + .avatar { + position: absolute; + } +} + +.attribution { + display: flex; + pointer-events: all; + margin: 5px 0; + + strong { + color: var(--fg_color); + } +} + +.media-tag-block { + padding-top: 5px; + pointer-events: all; + color: var(--fg_faded); + + .icon-container { + padding-right: 2px; + } + + .media-tag, + .icon-container { + color: var(--fg_faded); + } +} + +.timeline-container .media-tag-block { + font-size: 13px; +} + +.tweet-geo { + color: var(--fg_faded); +} + +.replying-to { + color: var(--fg_faded); + margin: -2px 0 4px; + + a { + pointer-events: all; + } +} + +.retweet-header, +.pinned, +.tweet-stats { + align-content: center; + color: var(--grey); + display: flex; + flex-shrink: 0; + flex-wrap: wrap; + font-size: 14px; + font-weight: 600; + line-height: 22px; + + span { + @include ellipsis; + } +} + +.retweet-header { + margin-top: -5px !important; +} + +.tweet-stats { + margin-bottom: -3px; + -webkit-user-select: none; +} + +.tweet-stat { + padding-top: 5px; + min-width: 1em; + margin-right: 0.8em; +} + +.show-thread { + display: block; + pointer-events: all; + padding-top: 2px; +} + +.unavailable-box { + width: 100%; + height: 100%; + padding: 12px; + border: solid 1px var(--dark_grey); + box-sizing: border-box; + border-radius: 10px; + background-color: var(--bg_color); + z-index: 2; +} + +.tweet-link { + height: 100%; + width: 100%; + left: 0; + top: 0; + position: absolute; + -webkit-user-select: none; + + &:hover { + background-color: var(--bg_hover); + } +} diff --git a/sass/tweet/card.scss b/sass/tweet/card.scss new file mode 100644 index 0000000..5575191 --- /dev/null +++ b/sass/tweet/card.scss @@ -0,0 +1,119 @@ +@import '_variables'; +@import '_mixins'; + +.card { + margin: 5px 0; + pointer-events: all; + max-height: unset; +} + +.card-container { + border-radius: 10px; + border-width: 1px; + border-style: solid; + border-color: var(--dark_grey); + background-color: var(--bg_elements); + overflow: hidden; + color: inherit; + display: flex; + flex-direction: row; + text-decoration: none !important; + + &:hover { + border-color: var(--grey); + } + + .attachments { + margin: 0; + border-radius: 0; + } +} + +.card-content { + padding: 0.5em; +} + +.card-title { + @include ellipsis; + white-space: unset; + font-weight: bold; + font-size: 1.1em; +} + +.card-description { + margin: 0.3em 0; + white-space: pre-wrap; +} + +.card-destination { + @include ellipsis; + color: var(--grey); + display: block; +} + +.card-content-container { + color: unset; + overflow: auto; + &:hover { + text-decoration: none; + } +} + +.card-image-container { + width: 98px; + flex-shrink: 0; + position: relative; + overflow: hidden; + &:before { + content: ""; + display: block; + padding-top: 100%; + } +} + +.card-image { + position: absolute; + top: 0; + left: 0; + bottom: 0; + right: 0; + background-color: var(--bg_overlays); + + img { + width: 100%; + height: 100%; + max-height: 400px; + display: block; + object-fit: cover; + } +} + +.card-overlay { + @include play-button; + opacity: 0.8; + display: flex; + justify-content: center; + align-items: center; +} + +.large { + .card-container { + display: block; + } + + .card-image-container { + width: unset; + + &:before { + display: none; + } + } + + .card-image { + position: unset; + border-style: solid; + border-color: var(--dark_grey); + border-width: 0; + border-bottom-width: 1px; + } +} diff --git a/sass/tweet/embed.scss b/sass/tweet/embed.scss new file mode 100644 index 0000000..fbdbd41 --- /dev/null +++ b/sass/tweet/embed.scss @@ -0,0 +1,17 @@ +@import "_variables"; +@import "_mixins"; + +.embed-video { + .gallery-video { + width: 100%; + height: 100%; + position: absolute; + background-color: black; + top: 0%; + left: 0%; + } + + .video-container { + max-height: unset; + } +} diff --git a/sass/tweet/media.scss b/sass/tweet/media.scss new file mode 100644 index 0000000..66a300f --- /dev/null +++ b/sass/tweet/media.scss @@ -0,0 +1,119 @@ +@import "_variables"; + +.gallery-row { + display: flex; + flex-direction: row; + flex-wrap: nowrap; + align-items: center; + overflow: hidden; + flex-grow: 1; + max-height: 379.5px; + max-width: 533px; + pointer-events: all; + + .still-image { + width: 100%; + display: flex; + } +} + +.attachments { + margin-top: 0.35em; + display: flex; + flex-direction: row; + width: 100%; + max-height: 600px; + border-radius: 7px; + overflow: hidden; + flex-flow: column; + background-color: var(--bg_color); + align-items: center; + pointer-events: all; + + .image-attachment { + width: 100%; + } +} + +.attachment { + position: relative; + line-height: 0; + overflow: hidden; + margin: 0 0.25em 0 0; + flex-grow: 1; + box-sizing: border-box; + min-width: 2em; + + &:last-child { + margin: 0; + max-height: 530px; + } +} + +.gallery-gif video { + max-height: 530px; + background-color: #101010; +} + +.still-image { + max-height: 379.5px; + max-width: 533px; + justify-content: center; + + img { + object-fit: cover; + max-width: 100%; + max-height: 379.5px; + flex-basis: 300px; + flex-grow: 1; + } +} + +.image { + display: inline-block; +} + +// .single-image { +// display: inline-block; +// width: 100%; +// max-height: 600px; + +// .attachments { +// width: unset; +// max-height: unset; +// display: inherit; +// } +// } + +.overlay-circle { + border-radius: 50%; + background-color: var(--dark_grey); + width: 40px; + height: 40px; + align-items: center; + display: flex; + border-width: 5px; + border-color: var(--play_button); + border-style: solid; +} + +.overlay-triangle { + width: 0; + height: 0; + border-style: solid; + border-width: 12px 0 12px 17px; + border-color: transparent transparent transparent var(--play_button); + margin-left: 14px; +} + +.media-gif { + display: table; + background-color: unset; + width: unset; +} + +.media-body { + flex: 1; + padding: 0; + white-space: pre-wrap; +} diff --git a/sass/tweet/poll.scss b/sass/tweet/poll.scss new file mode 100644 index 0000000..6d54e00 --- /dev/null +++ b/sass/tweet/poll.scss @@ -0,0 +1,42 @@ +@import "_variables"; + +.poll-meter { + overflow: hidden; + position: relative; + margin: 6px 0; + height: 26px; + background: var(--bg_color); + border-radius: 5px; + display: flex; + align-items: center; +} + +.poll-choice-bar { + height: 100%; + position: absolute; + background: var(--dark_grey); +} + +.poll-choice-value { + position: relative; + font-weight: bold; + margin-left: 5px; + margin-right: 6px; + min-width: 30px; + text-align: right; + pointer-events: all; +} + +.poll-choice-option { + position: relative; + pointer-events: all; +} + +.poll-info { + color: var(--grey); + pointer-events: all; +} + +.leader .poll-choice-bar { + background: var(--accent_dark); +} diff --git a/sass/tweet/quote.scss b/sass/tweet/quote.scss new file mode 100644 index 0000000..1db4f7e --- /dev/null +++ b/sass/tweet/quote.scss @@ -0,0 +1,95 @@ +@import "_variables"; + +.quote { + margin-top: 10px; + border: solid 1px var(--dark_grey); + border-radius: 10px; + background-color: var(--bg_elements); + overflow: hidden; + pointer-events: all; + position: relative; + width: 100%; + + &:hover { + border-color: var(--grey); + } + + &.unavailable:hover { + border-color: var(--dark_grey); + } + + .tweet-name-row { + padding: 6px 8px; + margin-top: 1px; + } + + .quote-text { + overflow: hidden; + white-space: pre-wrap; + word-wrap: break-word; + padding: 0px 8px 8px 8px; + } + + .show-thread { + padding: 0px 8px 6px 8px; + margin-top: -6px; + } + + .replying-to { + padding: 0px 8px; + margin: unset; + } +} + +.unavailable-quote { + padding: 12px; +} + +.quote-link { + width: 100%; + height: 100%; + left: 0; + top: 0; + position: absolute; +} + +.quote-media-container { + max-height: 300px; + display: flex; + + .card { + margin: unset; + } + + .attachments { + border-radius: 0; + } + + .media-gif { + width: 100%; + display: flex; + justify-content: center; + } + + .gallery-gif .attachment { + display: flex; + justify-content: center; + background-color: var(--bg_color); + + video { + height: unset; + width: unset; + max-height: 100%; + max-width: 100%; + } + } + + .gallery-video, + .gallery-gif { + max-height: 300px; + } + + .still-image img { + max-height: 250px; + } +} diff --git a/sass/tweet/thread.scss b/sass/tweet/thread.scss new file mode 100644 index 0000000..9d2fb64 --- /dev/null +++ b/sass/tweet/thread.scss @@ -0,0 +1,139 @@ +@import "_variables"; +@import "_mixins"; + +.conversation { + @include panel(100%, 600px); + + .show-more { + margin-bottom: 10px; + } +} + +.main-thread { + margin-bottom: 20px; + background-color: var(--bg_panel); +} + +.main-tweet, +.replies { + padding-top: 50px; + margin-top: -50px; +} + +.main-tweet .tweet-content { + font-size: 18px; +} + +@media (max-width: 600px) { + .main-tweet .tweet-content { + font-size: 16px; + } +} + +.reply { + background-color: var(--bg_panel); + margin-bottom: 10px; +} + +.thread-line { + .timeline-item::before, + &.timeline-item::before { + background: var(--accent_dark); + content: ""; + position: relative; + min-width: 3px; + width: 3px; + left: 26px; + border-radius: 2px; + margin-left: -3px; + margin-bottom: 37px; + top: 56px; + z-index: 1; + pointer-events: none; + } + + .with-header:not(:first-child)::after { + background: var(--accent_dark); + content: ""; + position: relative; + float: left; + min-width: 3px; + width: 3px; + right: calc(100% - 26px); + border-radius: 2px; + margin-left: -3px; + margin-bottom: 37px; + bottom: 10px; + height: 30px; + z-index: 1; + pointer-events: none; + } + + .unavailable::before { + top: 48px; + margin-bottom: 28px; + } + + .more-replies::before { + content: "..."; + background: unset; + color: var(--more_replies_dots); + font-weight: bold; + font-size: 20px; + line-height: 0.25em; + left: 1.2em; + width: 5px; + top: 2px; + margin-bottom: 0; + margin-left: -2.5px; + } + + .earlier-replies { + padding-bottom: 0; + margin-bottom: -5px; + } +} + +.timeline-item.thread-last::before { + background: unset; + min-width: unset; + width: 0; + margin: 0; +} + +.more-replies { + padding-top: 0.3em !important; +} + +.more-replies-text { + @include ellipsis; + display: block; + margin-left: 58px; + padding: 7px 0; +} + +.timeline-item.thread.more-replies-thread { + padding: 0 0.75em; + + &::before { + top: 40px; + margin-bottom: 31px; + } + + .more-replies { + display: flex; + padding-top: unset !important; + margin-top: 8px; + + &::before { + display: inline-block; + position: relative; + top: -1px; + line-height: 0.4em; + } + + .more-replies-text { + display: inline; + } + } +} diff --git a/sass/tweet/video.scss b/sass/tweet/video.scss new file mode 100644 index 0000000..ba77b14 --- /dev/null +++ b/sass/tweet/video.scss @@ -0,0 +1,77 @@ +@import "_variables"; +@import "_mixins"; + +video { + height: 100%; + width: 100%; +} + +.gallery-video { + display: flex; + overflow: hidden; +} + +.gallery-video.card-container { + flex-direction: column; + width: 100%; +} + +.video-container { + min-height: 80px; + min-width: 200px; + max-height: 530px; + margin: 0; + + img { + max-height: 100%; + max-width: 100%; + } +} + +.video-overlay { + @include play-button; + background-color: $shadow; + + p { + position: relative; + z-index: 0; + text-align: center; + top: calc(50% - 20px); + font-size: 20px; + line-height: 1.3; + margin: 0 20px; + } + + .overlay-circle { + position: relative; + z-index: 0; + top: calc(50% - 20px); + margin: 0 auto; + width: 40px; + height: 40px; + } + + .overlay-duration { + position: absolute; + bottom: 8px; + left: 8px; + background-color: #0000007a; + line-height: 1em; + padding: 4px 6px 4px 6px; + border-radius: 5px; + font-weight: bold; + } + + form { + width: 100%; + height: 100%; + align-items: center; + justify-content: center; + display: flex; + } + + button { + padding: 5px 8px; + font-size: 16px; + } +} diff --git a/sessions.example.jsonl b/sessions.example.jsonl new file mode 100644 index 0000000..c3bbbc1 --- /dev/null +++ b/sessions.example.jsonl @@ -0,0 +1 @@ +{"id":1,"username":"example","kind":"cookie","auth_token":"YOUR_AUTH_TOKEN_HERE","ct0":"YOUR_CT0_TOKEN_HERE"} diff --git a/src/api/auth.rs b/src/api/auth.rs new file mode 100644 index 0000000..7357fdb --- /dev/null +++ b/src/api/auth.rs @@ -0,0 +1,343 @@ +use std::{ + collections::HashMap, + path::Path, + sync::Arc, + time::{ + SystemTime, + UNIX_EPOCH, + }, +}; + +use data_encoding::BASE64; +use ring::hmac; +use serde::Serialize; +use time::format_description::well_known::Rfc3339; +use tokio::{ + fs, + sync::RwLock, +}; + +use crate::{ + error::{ + Error, + Result, + }, + types::{ + RateLimit, + Session, + SessionCredentials, + SessionKind, + SessionLimits, + }, +}; + +#[derive(Serialize)] +pub struct HealthResponse { + pub sessions: SessionStats, + pub requests: RequestStats, + pub timestamp: String, +} + +#[derive(Serialize)] +pub struct SessionStats { + pub total: usize, + pub limited: usize, + pub available: usize, +} + +#[derive(Serialize)] +pub struct RequestStats { + pub total: i32, + pub by_api: HashMap, +} + +#[derive(Serialize)] +pub struct DebugResponse { + pub sessions: Vec, + pub count: usize, + pub timestamp: String, +} + +#[derive(Serialize)] +pub struct SessionDetail { + pub id: i64, + pub username: String, + pub limited: bool, + pub limited_at: i64, + pub pending: i32, + pub apis: HashMap, +} + +/// Pool of authentication sessions for Twitter API. +#[derive(Clone)] +pub struct SessionPool { + sessions: Vec>, + limits: Arc>>, +} + +impl SessionPool { + /// Load sessions from a JSONL file. + #[expect( + clippy::cognitive_complexity, + reason = "session loading has inherent branching" + )] + pub async fn load(path: &str) -> Result { + let parsed = if Path::new(path).exists() { + let content = fs::read_to_string(path).await?; + let mut parsed = Vec::new(); + + for line in content.lines() { + if line.trim().is_empty() { + continue; + } + match serde_json::from_str::(line) { + Ok(session) => parsed.push(session), + Err(err) => { + tracing::warn!("Failed to parse session: {err}"); + }, + } + } + + tracing::info!("Loaded {} sessions", parsed.len()); + parsed + } else { + tracing::warn!("Sessions file not found: {path}"); + Vec::new() + }; + + let mut sessions = Vec::with_capacity(parsed.len()); + let mut limits = HashMap::with_capacity(parsed.len()); + + for session in parsed { + let (creds, lims) = session.into_credentials_and_limits(); + let id = creds.id; + sessions.push(Arc::new(creds)); + limits.insert(id, lims); + } + + Ok(Self { + sessions, + limits: Arc::new(RwLock::new(limits)), + }) + } + + /// Get an available session for making API requests. + pub async fn get_session(&self, api: &str) -> Result> { + if self.sessions.is_empty() { + return Err(Error::NoSessions); + } + + let limits = self.limits.read().await; + + // Find a session that isn't rate limited for this API + for session in &self.sessions { + if let Some(lim) = limits.get(&session.id) { + if !lim.is_limited(api) { + return Ok(Arc::clone(session)); + } + } else { + // No limits recorded yet — session is available + return Ok(Arc::clone(session)); + } + } + + // If all sessions are limited, return the one with the earliest reset time + let best = self + .sessions + .iter() + .min_by_key(|sess| { + limits + .get(&sess.id) + .and_then(|lim| lim.apis.get(api)) + .map_or(i64::MAX, |rate| rate.reset) + }) + .ok_or(Error::NoSessions)?; + + Ok(Arc::clone(best)) + } + + /// Update rate limit info for a session. + pub async fn update_session_limit( + &self, + session_id: i64, + api: &str, + limit: i32, + remaining: i32, + reset: i64, + ) { + let mut limits = self.limits.write().await; + + if let Some(lim) = limits.get_mut(&session_id) { + lim.update_limit(api, limit, remaining, reset); + } + } + + /// Mark a session as globally rate limited. + pub async fn mark_limited(&self, session_id: i64) { + let mut limits = self.limits.write().await; + + if let Some(lim) = limits.get_mut(&session_id) { + lim.limited = true; + lim.limited_at = time::OffsetDateTime::now_utc().unix_timestamp(); + } + } + + /// Get the session kind that would be used for a given API. + pub async fn get_session_kind(&self, api: &str) -> SessionKind { + self + .get_session(api) + .await + .map_or_else(|_| SessionKind::default(), |session| session.kind) + } + + /// Get session count. + pub const fn len(&self) -> usize { + self.sessions.len() + } + + /// Check if pool is empty. + pub const fn is_empty(&self) -> bool { + self.sessions.is_empty() + } + + /// Get health statistics about the session pool. + #[expect( + clippy::iter_over_hash_type, + reason = "iteration order irrelevant for aggregation" + )] + pub async fn get_health(&self) -> HealthResponse { + let limits = self.limits.read().await; + + let mut limited_count = 0; + let mut total_requests = 0; + let mut by_api = HashMap::::new(); + + for lim in limits.values() { + if lim.limited { + limited_count += 1; + } + + for (api, limit_info) in &lim.apis { + let used = limit_info.limit - limit_info.remaining; + total_requests += used; + *by_api.entry(api.clone()).or_default() += used; + } + } + drop(limits); + + HealthResponse { + sessions: SessionStats { + total: self.sessions.len(), + limited: limited_count, + available: self.sessions.len() - limited_count, + }, + requests: RequestStats { + total: total_requests, + by_api, + }, + timestamp: time::OffsetDateTime::now_utc().format(&Rfc3339).unwrap(), + } + } + + /// Get detailed debug info about sessions. + pub async fn get_debug(&self) -> DebugResponse { + let limits = self.limits.read().await; + + let sessions = self + .sessions + .iter() + .map(|sess| { + let lim = limits.get(&sess.id); + SessionDetail { + id: sess.id, + username: sess.username.clone(), + limited: lim.is_some_and(|sl| sl.limited), + limited_at: lim.map_or(0, |sl| sl.limited_at), + pending: lim.map_or(0, |sl| sl.pending), + apis: lim.map(|sl| sl.apis.clone()).unwrap_or_default(), + } + }) + .collect(); + + DebugResponse { + sessions, + count: self.sessions.len(), + timestamp: time::OffsetDateTime::now_utc().format(&Rfc3339).unwrap(), + } + } +} + +/// Sign a request with `OAuth1`. +pub fn oauth1_sign( + method: &str, + url: &str, + params: &[(&str, &str)], + oauth_token: &str, + oauth_secret: &str, +) -> String { + // OAuth parameters + let timestamp = time::OffsetDateTime::now_utc().unix_timestamp().to_string(); + let nonce = format!( + "{:032x}", + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_nanos() + ); + + let mut oauth_params = vec![ + ("oauth_consumer_key", super::endpoints::CONSUMER_KEY), + ("oauth_nonce", &nonce), + ("oauth_signature_method", "HMAC-SHA1"), + ("oauth_timestamp", ×tamp), + ("oauth_token", oauth_token), + ("oauth_version", "1.0"), + ]; + + // Combine all parameters and sort + let mut all_params: Vec<(&str, &str)> = params.to_vec(); + all_params.extend(oauth_params.iter().copied()); + all_params.sort_by(|lhs, rhs| lhs.0.cmp(rhs.0)); + + // Create parameter string + let param_string = all_params + .iter() + .map(|&(key, val)| format!("{}={}", percent_encode(key), percent_encode(val))) + .collect::>() + .join("&"); + + // Create signature base string + let base_string = format!( + "{}&{}&{}", + method.to_uppercase(), + percent_encode(url), + percent_encode(¶m_string) + ); + + // Create signing key + let signing_key = format!( + "{}&{}", + percent_encode(super::endpoints::CONSUMER_SECRET), + percent_encode(oauth_secret) + ); + + // Generate signature + let key = hmac::Key::new(hmac::HMAC_SHA1_FOR_LEGACY_USE_ONLY, signing_key.as_bytes()); + let tag = hmac::sign(&key, base_string.as_bytes()); + let signature = BASE64.encode(tag.as_ref()); + + oauth_params.push(("oauth_signature", &signature)); + + // Build Authorization header + let auth_header = oauth_params + .iter() + .map(|&(param, val)| format!("{}=\"{}\"", param, percent_encode(val))) + .collect::>() + .join(", "); + + format!("OAuth {auth_header}") +} + +fn percent_encode(input: &str) -> String { + percent_encoding::utf8_percent_encode(input, percent_encoding::NON_ALPHANUMERIC).to_string() +} diff --git a/src/api/client.rs b/src/api/client.rs new file mode 100644 index 0000000..093b192 --- /dev/null +++ b/src/api/client.rs @@ -0,0 +1,620 @@ +use std::sync::Arc; + +use axum::http::header; +use serde::{ + Deserialize, + de::DeserializeOwned, +}; + +use super::{ + SessionPool, + TidClient, + endpoints, + http::HttpClient, + parser, +}; +use crate::{ + api::schema::{ + ConversationData, + EditHistoryData, + GqlResponse, + ListByIdData, + ListBySlugData, + ListMembersData, + ListTimelineData, + SearchTimelineData, + TweetResultData, + UserResultData, + UserTimelineData, + }, + config::Config, + error::{ + Error, + Result, + TwitterError, + }, + types::{ + Conversation, + EditHistory, + GalleryPhoto, + List, + PaginatedResult, + Profile, + SessionKind, + Timeline, + Tweet, + User, + }, +}; + +/// Twitter/X API client. +#[derive(Clone)] +pub struct ApiClient { + client: HttpClient, + sessions: SessionPool, + tid: TidClient, +} + +impl ApiClient { + pub fn new(_config: Arc, sessions: SessionPool) -> Self { + let mut headers = header::HeaderMap::new(); + headers.insert( + header::USER_AGENT, + header::HeaderValue::from_static(endpoints::USER_AGENT), + ); + headers.insert( + header::ACCEPT_LANGUAGE, + header::HeaderValue::from_static("en-US,en;q=0.9"), + ); + headers.insert( + header::ACCEPT_ENCODING, + header::HeaderValue::from_static("gzip"), + ); + headers.insert( + header::CONNECTION, + header::HeaderValue::from_static("keep-alive"), + ); + + let client = HttpClient::new().with_default_headers(headers); + + let tid = TidClient::new(client.clone()); + + Self { + client, + sessions, + tid, + } + } + + /// Check for API-level errors in the raw response bytes. + fn check_api_errors(bytes: &[u8]) -> Result<()> { + #[derive(Deserialize)] + struct ErrorCheck { + errors: Option>, + } + #[derive(Deserialize)] + struct ApiError { + #[serde(default)] + code: i64, + #[serde(default)] + message: String, + } + + let Ok(check) = serde_json::from_slice::(bytes) else { + return Ok(()); + }; + let Some(error) = check.errors.as_ref().and_then(|errs| errs.first()) else { + return Ok(()); + }; + + if let Some(twitter_err) = TwitterError::from_code(error.code) { + return match twitter_err { + TwitterError::UserNotFound | TwitterError::NoUserMatches => { + Err(Error::UserNotFound(error.message.clone())) + }, + TwitterError::ProtectedUser => Err(Error::ProtectedUser(error.message.clone())), + TwitterError::UserSuspended | TwitterError::Locked => { + Err(Error::UserSuspended(error.message.clone())) + }, + TwitterError::RateLimited => Err(Error::RateLimited), + TwitterError::TweetNotFound + | TwitterError::TweetUnavailable + | TwitterError::NoStatusFound + | TwitterError::TweetUnavailable421 + | TwitterError::TweetCensored => Err(Error::TweetNotFound(error.message.clone())), + TwitterError::InvalidToken | TwitterError::BadToken => { + Err(Error::TwitterApi(format!( + "Invalid token: {}", + error.message + ))) + }, + }; + } + + Err(Error::TwitterApi(format!( + "Error {}: {}", + error.code, error.message + ))) + } + + /// Make a GraphQL request to the Twitter API. + async fn graphql_request( + &self, + endpoint: &str, + variables: &str, + features: &str, + field_toggles: Option<&str>, + ) -> Result { + let session = self.sessions.get_session(endpoint).await?; + + let base_url = match session.kind { + SessionKind::OAuth => endpoints::API_URL, + SessionKind::Cookie => endpoints::GRAPHQL_URL, + }; + + // Build URL with query string (scoped to drop Serializer before await) + let url = { + let mut qs = form_urlencoded::Serializer::new(String::new()); + qs.append_pair("variables", variables); + qs.append_pair("features", features); + if let Some(toggles) = field_toggles { + qs.append_pair("fieldToggles", toggles); + } + format!("{base_url}/{endpoint}?{}", qs.finish()) + }; + + // Build auth + extra headers + let mut headers = header::HeaderMap::new(); + + match session.kind { + SessionKind::OAuth => { + let auth_url = format!("{base_url}/{endpoint}"); + let auth = super::oauth1_sign( + "GET", + &auth_url, + &[], + &session.oauth_token, + &session.oauth_secret, + ); + headers.insert(header::AUTHORIZATION, auth.parse().unwrap()); + }, + SessionKind::Cookie => { + let api_path = format!("/i/api/graphql/{endpoint}"); + let (bearer, tid) = self + .tid + .generate(&api_path) + .await + .map_or((endpoints::BEARER_TOKEN_NO_TID, None), |tid_val| { + (endpoints::BEARER_TOKEN, Some(tid_val)) + }); + + headers.insert( + header::AUTHORIZATION, + header::HeaderValue::from_str(bearer).unwrap(), + ); + headers.insert( + "x-twitter-auth-type", + header::HeaderValue::from_static("OAuth2Session"), + ); + headers.insert("x-csrf-token", session.ct0.parse().unwrap()); + headers.insert( + header::COOKIE, + format!("auth_token={}; ct0={}", session.auth_token, session.ct0) + .parse() + .unwrap(), + ); + headers.insert(header::ORIGIN, header::HeaderValue::from_static("https://x.com")); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + ); + headers.insert( + "sec-ch-ua", + header::HeaderValue::from_static( + r#""Google Chrome";v="142", "Chromium";v="142", "Not A(Brand";v="24""#, + ), + ); + headers.insert("sec-ch-ua-mobile", header::HeaderValue::from_static("?0")); + headers.insert( + "sec-ch-ua-platform", + header::HeaderValue::from_static("\"Windows\""), + ); + headers.insert("sec-fetch-dest", header::HeaderValue::from_static("empty")); + headers.insert("sec-fetch-mode", header::HeaderValue::from_static("cors")); + headers.insert( + "sec-fetch-site", + header::HeaderValue::from_static("same-site"), + ); + + if let Some(tid) = tid { + headers.insert("x-client-transaction-id", tid.parse().unwrap()); + } + }, + } + + // Common headers + headers.insert(header::ACCEPT, header::HeaderValue::from_static("*/*")); + headers.insert( + "x-twitter-active-user", + header::HeaderValue::from_static("yes"), + ); + headers.insert( + "x-twitter-client-language", + header::HeaderValue::from_static("en"), + ); + + let response = self.client.get_with_headers(&url, &headers).await?; + + // Check rate limit headers + if let Some(remaining) = response.headers().get("x-rate-limit-remaining") + && let Ok(remaining_str) = remaining.to_str() + && let Ok(remaining_val) = remaining_str.parse::() + { + let limit = response + .headers() + .get("x-rate-limit-limit") + .and_then(|hv| hv.to_str().ok()) + .and_then(|sv| sv.parse().ok()) + .unwrap_or(0); + let reset = response + .headers() + .get("x-rate-limit-reset") + .and_then(|hv| hv.to_str().ok()) + .and_then(|sv| sv.parse().ok()) + .unwrap_or(0); + + self + .sessions + .update_session_limit(session.id, endpoint, limit, remaining_val, reset) + .await; + } + + if !response.status().is_success() { + let status = response.status(); + let body = response.text().await.unwrap_or_default(); + tracing::error!("API request failed: {status} - {body}"); + + if status.as_u16() == 429 { + self.sessions.mark_limited(session.id).await; + return Err(Error::RateLimited); + } + + return Err(Error::TwitterApi(format!("Status {status}: {body}"))); + } + + let bytes = response.bytes().await?; + + // Check for API errors before full deserialization + Self::check_api_errors(&bytes)?; + + let resp = serde_json::from_slice::>(&bytes) + .map_err(|err| Error::Internal(format!("Response parse error: {err}")))?; + Ok(resp.data) + } + + /// Get user by screen name. + pub async fn get_user(&self, screen_name: &str) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_USER, + &endpoints::user_by_screen_name_vars(screen_name), + endpoints::GQL_FEATURES, + Some(endpoints::USER_FIELD_TOGGLES), + ) + .await?; + super::parse_user(&data) + } + + /// Get user by REST ID (numeric user ID). + pub async fn get_user_by_id(&self, user_id: &str) -> Result { + if user_id.is_empty() || !user_id.chars().all(|ch| ch.is_ascii_digit()) { + return Err(Error::UserNotFound("Invalid user ID format".to_owned())); + } + let data = self + .graphql_request::( + endpoints::GRAPH_USER_BY_ID, + &endpoints::user_by_id_vars(user_id), + endpoints::GQL_FEATURES, + Some(endpoints::USER_FIELD_TOGGLES), + ) + .await?; + super::parse_user(&data) + } + + /// Get edit history for a tweet. + pub async fn get_edit_history(&self, tweet_id: &str) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_TWEET_EDIT_HISTORY, + &endpoints::tweet_edit_history_vars(tweet_id), + endpoints::GQL_FEATURES, + None, + ) + .await?; + parser::edit_history::parse_edit_history(&data) + } + + /// Get tweet by ID. + pub async fn get_tweet(&self, tweet_id: &str) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_TWEET_RESULT, + &endpoints::tweet_vars(tweet_id, None), + endpoints::GQL_FEATURES, + None, + ) + .await?; + super::parse_tweet(&data) + } + + /// Get conversation/thread for a tweet. + pub async fn get_conversation( + &self, + tweet_id: &str, + cursor: Option<&str>, + ) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_TWEET_DETAIL, + &endpoints::tweet_detail_vars(tweet_id, cursor), + endpoints::GQL_FEATURES, + Some(endpoints::TWEET_DETAIL_FIELD_TOGGLES), + ) + .await?; + super::parse_conversation(&data, tweet_id) + } + + /// Get user's tweets timeline. + pub async fn get_user_tweets(&self, user_id: &str, cursor: Option<&str>) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_USER_TWEETS, + &endpoints::user_tweets_vars(user_id, cursor), + endpoints::GQL_FEATURES, + Some(endpoints::USER_TWEETS_FIELD_TOGGLES), + ) + .await?; + super::parse_timeline(&data) + } + + /// Get user's media timeline. + pub async fn get_user_media(&self, user_id: &str, cursor: Option<&str>) -> Result { + // Use different endpoint/variables based on session type + let session_kind = self + .sessions + .get_session_kind(endpoints::GRAPH_USER_MEDIA) + .await; + let (endpoint, variables) = match session_kind { + SessionKind::OAuth => { + ( + endpoints::GRAPH_USER_MEDIA_V2, + endpoints::user_media_v2_vars(user_id, cursor), + ) + }, + SessionKind::Cookie => { + ( + endpoints::GRAPH_USER_MEDIA, + endpoints::user_media_vars(user_id, cursor), + ) + }, + }; + + let data = self + .graphql_request::(endpoint, &variables, endpoints::GQL_FEATURES, None) + .await?; + + super::parse_timeline(&data) + } + + /// Get user's profile with tweets. + pub async fn get_profile(&self, screen_name: &str, cursor: Option<&str>) -> Result { + // First get user info + let user = self.get_user(screen_name).await?; + + // Fetch tweets and photo rail in parallel (only for first page) + let (tweets, photo_rail) = if cursor.is_none() { + let tweets_future = self.get_user_tweets(&user.id, None); + let photo_rail_future = self.get_photo_rail(&user.id); + let (tweets_result, photo_rail_result) = tokio::join!(tweets_future, photo_rail_future); + (tweets_result?, photo_rail_result.unwrap_or_default()) + } else { + (self.get_user_tweets(&user.id, cursor).await?, Vec::new()) + }; + + // Get pinned tweet if present + let pinned = if user.pinned_tweet > 0 { + tweets + .content + .iter() + .flatten() + .find(|tweet| tweet.id == user.pinned_tweet) + .cloned() + } else { + None + }; + + Ok(Profile { + user, + photo_rail, + pinned, + tweets, + }) + } + + /// Search tweets. + pub async fn search(&self, query: &str, cursor: Option<&str>) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_SEARCH_TIMELINE, + &endpoints::search_vars(query, cursor, "Latest"), + endpoints::GQL_FEATURES, + None, + ) + .await?; + Ok(super::parse_search_timeline(&data)) + } + + /// Search users. + pub async fn search_users( + &self, + query: &str, + cursor: Option<&str>, + ) -> Result> { + let data = self + .graphql_request::( + endpoints::GRAPH_SEARCH_TIMELINE, + &endpoints::search_vars(query, cursor, "People"), + endpoints::GQL_FEATURES, + None, + ) + .await?; + Ok(super::parse_user_search(&data)) + } + + /// Get list by ID. + pub async fn get_list(&self, list_id: &str) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_LIST_BY_ID, + &endpoints::list_by_id_vars(list_id), + endpoints::GQL_FEATURES, + None, + ) + .await?; + let wrapper = data + .list + .as_ref() + .ok_or_else(|| Error::NotFound("List not found".into()))?; + Ok(super::parse_list(wrapper.list_data())) + } + + /// Get list by owner username and slug. + pub async fn get_list_by_slug(&self, screen_name: &str, slug: &str) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_LIST_BY_SLUG, + &endpoints::list_by_slug_vars(screen_name, slug), + endpoints::GQL_FEATURES, + None, + ) + .await?; + data + .user_by_screen_name + .as_ref() + .and_then(|nested| nested.list.as_ref()) + .map(|ld| Ok(super::parse_list(ld))) + .ok_or_else(|| Error::NotFound("List not found".into()))? + } + + /// Get list tweets. + pub async fn get_list_tweets(&self, list_id: &str, cursor: Option<&str>) -> Result { + let data = self + .graphql_request::( + endpoints::GRAPH_LIST_TWEETS, + &endpoints::list_timeline_vars(list_id, cursor), + endpoints::GQL_FEATURES, + None, + ) + .await?; + super::parse_list_timeline(&data) + } + + /// Get list members. + pub async fn get_list_members( + &self, + list_id: &str, + cursor: Option<&str>, + ) -> Result> { + let data = self + .graphql_request::( + endpoints::GRAPH_LIST_MEMBERS, + &endpoints::list_members_vars(list_id, cursor), + endpoints::GQL_FEATURES, + None, + ) + .await?; + Ok(super::parse_list_members(&data)) + } + + /// Get user's tweets and replies timeline. + pub async fn get_user_tweets_and_replies( + &self, + user_id: &str, + cursor: Option<&str>, + ) -> Result { + let session_kind = self + .sessions + .get_session_kind(endpoints::GRAPH_USER_TWEETS_AND_REPLIES) + .await; + let (endpoint, variables, field_toggles) = match session_kind { + SessionKind::OAuth => { + ( + endpoints::GRAPH_USER_TWEETS_AND_REPLIES_V2, + endpoints::user_media_v2_vars(user_id, cursor), + None, + ) + }, + SessionKind::Cookie => { + ( + endpoints::GRAPH_USER_TWEETS_AND_REPLIES, + endpoints::user_tweets_and_replies_vars(user_id, cursor), + Some(endpoints::USER_TWEETS_FIELD_TOGGLES), + ) + }, + }; + let data = self + .graphql_request::( + endpoint, + &variables, + endpoints::GQL_FEATURES, + field_toggles, + ) + .await?; + super::parse_timeline(&data) + } + + /// Get session pool health statistics. + pub async fn get_session_health(&self) -> super::HealthResponse { + self.sessions.get_health().await + } + + /// Get detailed session debug info. + pub async fn get_session_debug(&self) -> super::DebugResponse { + self.sessions.get_debug().await + } + + /// Get photo rail (up to 16 recent photos) for a user. + pub async fn get_photo_rail(&self, user_id: &str) -> Result> { + let timeline = self.get_user_media(user_id, None).await?; + + let mut photos = Vec::new(); + for mut tweet in timeline.content.into_iter().flatten() { + // Extract ONE photo per tweet: + // first photo > video thumb > gif thumb > card image + let url = if !tweet.photos.is_empty() { + Some(tweet.photos.swap_remove(0).url) + } else if let Some(video) = tweet.video.take() { + (!video.thumb.is_empty()).then_some(video.thumb) + } else if let Some(gif) = tweet.gif.take() { + (!gif.thumb.is_empty()).then_some(gif.thumb) + } else if let Some(card) = tweet.card.take() { + (!card.image.is_empty()).then_some(card.image) + } else { + None + }; + + if let Some(url) = url { + photos.push(GalleryPhoto { + url, + tweet_id: tweet.id.to_string(), + color: String::new(), + }); + if photos.len() >= 16 { + return Ok(photos); + } + } + } + + Ok(photos) + } +} diff --git a/src/api/endpoints.rs b/src/api/endpoints.rs new file mode 100644 index 0000000..e945af6 --- /dev/null +++ b/src/api/endpoints.rs @@ -0,0 +1,142 @@ +/// Twitter/X API constants and endpoints. +use serde_json::json; + +pub const CONSUMER_KEY: &str = "3nVuSoBZnx6U4vzUxf5w"; +pub const CONSUMER_SECRET: &str = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys"; +/// Bearer token that requires x-client-transaction-id (used for cookie sessions +/// with TID). +pub const BEARER_TOKEN: &str = + "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%\ + 3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA"; + +/// Fallback bearer token that doesn't require x-client-transaction-id. +pub const BEARER_TOKEN_NO_TID: &str = + "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%\ + 3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F"; + +// GraphQL endpoints +pub const GRAPH_USER: &str = "-oaLodhGbbnzJBACb1kk2Q/UserByScreenName"; +pub const GRAPH_USER_BY_ID: &str = "VN33vKXrPT7p35DgNR27aw/UserResultByIdQuery"; +pub const GRAPH_USER_TWEETS: &str = "oRJs8SLCRNRbQzuZG93_oA/UserTweets"; +pub const GRAPH_USER_MEDIA: &str = "36oKqyQ7E_9CmtONGjJRsA/UserMedia"; +pub const GRAPH_USER_MEDIA_V2: &str = "bp0e_WdXqgNBIwlLukzyYA/MediaTimelineV2"; +pub const GRAPH_TWEET_DETAIL: &str = "YVyS4SfwYW7Uw5qwy0mQCA/TweetDetail"; +pub const GRAPH_TWEET_RESULT: &str = "nzme9KiYhfIOrrLrPP_XeQ/TweetResultByIdQuery"; +pub const GRAPH_SEARCH_TIMELINE: &str = "bshMIjqDk8LTXTq4w91WKw/SearchTimeline"; +pub const GRAPH_LIST_BY_ID: &str = "cIUpT1UjuGgl_oWiY7Snhg/ListByRestId"; +pub const GRAPH_LIST_BY_SLUG: &str = "K6wihoTiTrzNzSF8y1aeKQ/ListBySlug"; +pub const GRAPH_LIST_TWEETS: &str = "VQf8_XQynI3WzH6xopOMMQ/ListTimeline"; +pub const GRAPH_LIST_MEMBERS: &str = "BQp2IEYkgxuSxqbTAr1e1g/ListMembers"; +pub const GRAPH_USER_TWEETS_AND_REPLIES: &str = "kkaJ0Mf34PZVarrxzLihjg/UserTweetsAndReplies"; +pub const GRAPH_USER_TWEETS_AND_REPLIES_V2: &str = + "BDX77Xzqypdt11-mDfgdpQ/UserWithProfileTweetsAndRepliesQueryV2"; +pub const GRAPH_TWEET_EDIT_HISTORY: &str = "upS9teTSG45aljmP9oTuXA/TweetEditHistory"; + +pub const USER_AGENT: &str = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 \ + (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36"; + +// Base URLs +pub const GRAPHQL_URL: &str = "https://x.com/i/api/graphql"; +pub const API_URL: &str = "https://api.x.com/graphql"; + +/// GraphQL features to include in requests. +pub const GQL_FEATURES: &str = r#"{"android_ad_formats_media_component_render_overlay_enabled":false,"android_graphql_skip_api_media_color_palette":false,"android_professional_link_spotlight_display_enabled":false,"blue_business_profile_image_shape_enabled":false,"commerce_android_shop_module_enabled":false,"creator_subscriptions_subscription_count_enabled":false,"creator_subscriptions_tweet_preview_api_enabled":true,"freedom_of_speech_not_reach_fetch_enabled":true,"graphql_is_translatable_rweb_tweet_is_translatable_enabled":true,"hidden_profile_likes_enabled":false,"highlights_tweets_tab_ui_enabled":false,"interactive_text_enabled":false,"longform_notetweets_consumption_enabled":true,"longform_notetweets_inline_media_enabled":true,"longform_notetweets_rich_text_read_enabled":true,"longform_notetweets_richtext_consumption_enabled":true,"mobile_app_spotlight_module_enabled":false,"responsive_web_edit_tweet_api_enabled":true,"responsive_web_enhance_cards_enabled":false,"responsive_web_graphql_exclude_directive_enabled":true,"responsive_web_graphql_skip_user_profile_image_extensions_enabled":false,"responsive_web_graphql_timeline_navigation_enabled":true,"responsive_web_media_download_video_enabled":false,"responsive_web_text_conversations_enabled":false,"responsive_web_twitter_article_tweet_consumption_enabled":true,"unified_cards_destination_url_params_enabled":false,"responsive_web_twitter_blue_verified_badge_is_enabled":true,"rweb_lists_timeline_redesign_enabled":true,"spaces_2022_h2_clipping":true,"spaces_2022_h2_spaces_communities":true,"standardized_nudges_misinfo":true,"subscriptions_verification_info_enabled":true,"subscriptions_verification_info_reason_enabled":true,"subscriptions_verification_info_verified_since_enabled":true,"super_follow_badge_privacy_enabled":false,"super_follow_exclusive_tweet_notifications_enabled":false,"super_follow_tweet_api_enabled":false,"super_follow_user_api_enabled":false,"tweet_awards_web_tipping_enabled":false,"tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled":true,"tweetypie_unmention_optimization_enabled":false,"unified_cards_ad_metadata_container_dynamic_card_content_query_enabled":false,"verified_phone_label_enabled":false,"vibe_api_enabled":false,"view_counts_everywhere_api_enabled":true,"premium_content_api_read_enabled":false,"communities_web_enable_tweet_community_results_fetch":true,"responsive_web_jetfuel_frame":true,"responsive_web_grok_analyze_button_fetch_trends_enabled":false,"responsive_web_grok_image_annotation_enabled":true,"responsive_web_grok_imagine_annotation_enabled":true,"rweb_tipjar_consumption_enabled":true,"profile_label_improvements_pcf_label_in_post_enabled":true,"creator_subscriptions_quote_tweet_preview_enabled":false,"c9s_tweet_anatomy_moderator_badge_enabled":true,"responsive_web_grok_analyze_post_followups_enabled":true,"rweb_video_timestamps_enabled":false,"responsive_web_grok_share_attachment_enabled":true,"articles_preview_enabled":true,"immersive_video_status_linkable_timestamps":false,"articles_api_enabled":false,"responsive_web_grok_analysis_button_from_backend":true,"rweb_video_screen_enabled":false,"payments_enabled":false,"responsive_web_profile_redirect_enabled":false,"responsive_web_grok_show_grok_translated_post":false,"responsive_web_grok_community_note_auto_translation_is_enabled":false,"profile_label_improvements_pcf_label_in_profile_enabled":false,"grok_android_analyze_trend_fetch_enabled":false,"grok_translations_community_note_auto_translation_is_enabled":false,"grok_translations_post_auto_translation_is_enabled":false,"grok_translations_community_note_translation_is_enabled":false,"grok_translations_timeline_user_bio_auto_translation_is_enabled":false,"subscriptions_feature_can_gift_premium":false,"responsive_web_twitter_article_notes_tab_enabled":false,"subscriptions_verification_info_is_identity_verified_enabled":false,"hidden_profile_subscriptions_enabled":false}"#; + +pub const USER_FIELD_TOGGLES: &str = r#"{"withPayments":false,"withAuxiliaryUserLabels":true}"#; +pub const USER_TWEETS_FIELD_TOGGLES: &str = r#"{"withArticlePlainText":false}"#; +pub const TWEET_DETAIL_FIELD_TOGGLES: &str = r#"{"withArticleRichContentState":true,"withArticlePlainText":false,"withGrokAnalyze":false,"withDisallowedReplyControls":false}"#; + +// ── Helper ────────────────────────────────────────────────────────────── + +/// Serialize a JSON value to string, stripping null fields (for optional +/// cursor). +fn vars(mut value: serde_json::Value) -> String { + if let Some(obj) = value.as_object_mut() { + obj.retain(|_, val| !val.is_null()); + } + value.to_string() +} + +// ── Builder functions ─────────────────────────────────────────────────── + +pub fn tweet_vars(post_id: &str, cursor: Option<&str>) -> String { + vars(json!({ + "postId": post_id, "cursor": cursor, + "includeHasBirdwatchNotes": false, "includePromotedContent": false, + "withBirdwatchNotes": false, "withVoice": false, "withV2Timeline": true, + })) +} + +pub fn tweet_detail_vars(focal_tweet_id: &str, cursor: Option<&str>) -> String { + vars(json!({ + "focalTweetId": focal_tweet_id, "cursor": cursor, + "referrer": "profile", "withRuxInjections": false, "rankingMode": "Relevance", + "includePromotedContent": true, "withCommunity": true, + "withQuickPromoteEligibilityTweetFields": true, + "withBirdwatchNotes": true, "withVoice": true, + })) +} + +pub fn user_tweets_vars(user_id: &str, cursor: Option<&str>) -> String { + vars(json!({ + "userId": user_id, "cursor": cursor, "count": 20, + "includePromotedContent": false, + "withQuickPromoteEligibilityTweetFields": true, "withVoice": true, + })) +} + +pub fn user_media_vars(user_id: &str, cursor: Option<&str>) -> String { + vars(json!({ + "userId": user_id, "cursor": cursor, "count": 20, + "includePromotedContent": false, "withClientEventToken": false, + "withBirdwatchNotes": false, "withVoice": true, + })) +} + +pub fn user_media_v2_vars(user_id: &str, cursor: Option<&str>) -> String { + vars(json!({ "rest_id": user_id, "cursor": cursor, "count": 20 })) +} + +pub fn user_by_screen_name_vars(screen_name: &str) -> String { + vars(json!({ "screen_name": screen_name, "withSafetyModeUserFields": true })) +} + +pub fn user_by_id_vars(rest_id: &str) -> String { + json!({ "rest_id": rest_id }).to_string() +} + +pub fn list_members_vars(list_id: &str, cursor: Option<&str>) -> String { + vars(json!({ "listId": list_id, "cursor": cursor, "count": 20 })) +} + +pub fn user_tweets_and_replies_vars(user_id: &str, cursor: Option<&str>) -> String { + vars(json!({ + "userId": user_id, "cursor": cursor, "count": 20, + "includePromotedContent": false, "withCommunity": true, "withVoice": true, + })) +} + +pub fn list_by_slug_vars(screen_name: &str, list_slug: &str) -> String { + json!({ "screenName": screen_name, "listSlug": list_slug }).to_string() +} + +pub fn tweet_edit_history_vars(tweet_id: &str) -> String { + json!({ "tweetId": tweet_id, "withQuickPromoteEligibilityTweetFields": true }).to_string() +} + +pub fn search_vars(raw_query: &str, cursor: Option<&str>, product: &str) -> String { + vars(json!({ + "rawQuery": raw_query, "cursor": cursor, "count": 20, + "querySource": "typedQuery", "product": product, + "withDownvotePerspective": false, "withReactionsMetadata": false, + "withReactionsPerspective": false, + })) +} + +pub fn list_by_id_vars(list_id: &str) -> String { + json!({ "listId": list_id }).to_string() +} + +pub fn list_timeline_vars(rest_id: &str, cursor: Option<&str>) -> String { + vars(json!({ "rest_id": rest_id, "cursor": cursor, "count": 20 })) +} diff --git a/src/api/http.rs b/src/api/http.rs new file mode 100644 index 0000000..7d97002 --- /dev/null +++ b/src/api/http.rs @@ -0,0 +1,178 @@ +use std::{ + io::Read as _, + time::Duration, +}; + +use axum::http::{ + HeaderMap, + Method, + Uri, + header, +}; +use bytes::Bytes; +use flate2::read::GzDecoder; +use http_body_util::{ + BodyExt as _, + Empty, +}; +use hyper::{ + StatusCode, + body as hyper_body, +}; +use hyper_rustls::HttpsConnectorBuilder; +use hyper_util::{ + client::legacy::{ + Client, + connect::HttpConnector, + }, + rt::TokioExecutor, +}; +use serde::de::DeserializeOwned; + +use crate::error::{ + Error, + Result, +}; + +type Connector = hyper_rustls::HttpsConnector; + +/// Lightweight HTTP client wrapping hyper-util's connection-pooling client. +#[derive(Clone)] +#[expect( + clippy::module_name_repetitions, + reason = "HttpClient is clearer than Client" +)] +pub struct HttpClient { + inner: Client>, + default_headers: HeaderMap, +} + +/// Response wrapper providing convenience methods. +pub struct Response { + status: StatusCode, + headers: HeaderMap, + body: hyper_body::Incoming, +} + +impl HttpClient { + pub fn new() -> Self { + let connector = HttpsConnectorBuilder::new() + .with_native_roots() + .expect("native TLS roots") + .https_or_http() + .enable_http1() + .build(); + + let inner = Client::builder(TokioExecutor::new()) + .pool_idle_timeout(Duration::from_secs(90)) + .build(connector); + + Self { + inner, + default_headers: HeaderMap::new(), + } + } + + /// Create a client with default headers applied to every request. + pub fn with_default_headers(mut self, headers: HeaderMap) -> Self { + self.default_headers = headers; + self + } + + /// Send a request with a given method and optional extra headers. + async fn send(&self, method: Method, uri: &str, extra_headers: &HeaderMap) -> Result { + let parsed: Uri = uri + .parse() + .map_err(|err| Error::Internal(format!("invalid URI: {err}")))?; + + let mut builder = hyper::Request::builder().method(method).uri(parsed); + + for (key, value) in &self.default_headers { + builder = builder.header(key, value); + } + for (key, value) in extra_headers { + builder = builder.header(key, value); + } + + let request = builder + .body(Empty::new()) + .map_err(|err| Error::Internal(format!("build request: {err}")))?; + + let resp = self + .inner + .request(request) + .await + .map_err(|err| Error::Internal(format!("HTTP request failed: {err}")))?; + + let (parts, body) = resp.into_parts(); + Ok(Response { + status: parts.status, + headers: parts.headers, + body, + }) + } + + /// Send a GET request. + pub async fn get(&self, uri: &str) -> Result { + self.send(Method::GET, uri, &HeaderMap::new()).await + } + + /// Send a GET request with additional headers. + pub async fn get_with_headers(&self, uri: &str, extra_headers: &HeaderMap) -> Result { + self.send(Method::GET, uri, extra_headers).await + } + + /// Send a HEAD request. + pub async fn head(&self, uri: &str) -> Result { + self.send(Method::HEAD, uri, &HeaderMap::new()).await + } +} + +impl Response { + pub const fn status(&self) -> StatusCode { + self.status + } + + pub const fn headers(&self) -> &HeaderMap { + &self.headers + } + + /// Collect the response body as bytes, decompressing gzip if needed. + pub async fn bytes(self) -> Result { + let is_gzip = self + .headers + .get(header::CONTENT_ENCODING) + .and_then(|val| val.to_str().ok()) + .is_some_and(|val| val.contains("gzip")); + + let collected = self + .body + .collect() + .await + .map_err(|err| Error::Internal(format!("read body: {err}")))? + .to_bytes(); + + if is_gzip { + let mut gz = GzDecoder::new(&*collected); + let mut decoded = Vec::new(); + gz.read_to_end(&mut decoded) + .map_err(|err| Error::Internal(format!("gzip decode: {err}")))?; + Ok(Bytes::from(decoded)) + } else { + Ok(collected) + } + } + + /// Collect the response body as a UTF-8 string. + pub async fn text(self) -> Result { + let data = self.bytes().await?; + String::from_utf8(data.to_vec()) + .map_err(|err| Error::Internal(format!("invalid UTF-8: {err}"))) + } + + /// Deserialize the response body as JSON. + pub async fn json(self) -> Result { + let data = self.bytes().await?; + serde_json::from_slice(&data).map_err(Into::into) + } +} diff --git a/src/api/mod.rs b/src/api/mod.rs new file mode 100644 index 0000000..0595470 --- /dev/null +++ b/src/api/mod.rs @@ -0,0 +1,13 @@ +mod auth; +mod client; +mod endpoints; +pub mod http; +mod parser; +pub mod schema; +mod tid; + +pub use auth::*; +pub use client::*; +pub use http::HttpClient; +pub use parser::*; +pub use tid::*; diff --git a/src/api/parser/card.rs b/src/api/parser/card.rs new file mode 100644 index 0000000..1add613 --- /dev/null +++ b/src/api/parser/card.rs @@ -0,0 +1,464 @@ +use std::collections::HashMap; + +use time::format_description::well_known::Rfc3339; + +use crate::{ + api::schema::{ + BindingValues, + CardData, + Destination, + MediaType, + UnifiedCard, + }, + error::{ + Error, + Result, + }, + types::{ + Card, + CardKind, + Poll, + Video, + VideoType, + VideoVariant, + }, +}; + +/// Extract destination URL and vanity from typed destination objects. +fn extract_destination( + dest_id: &str, + destinations: &HashMap, +) -> (String, String) { + let Some(dest) = destinations.get(dest_id) else { + return (String::new(), String::new()); + }; + let Some(url_data) = dest + .data + .as_ref() + .and_then(|dest_data| dest_data.url_data.as_ref()) + else { + return (String::new(), String::new()); + }; + let url = url_data.url.as_deref().unwrap_or("").to_owned(); + let vanity = url_data.vanity.as_deref().unwrap_or("").to_owned(); + (url, vanity) +} + +impl TryFrom<&CardData> for Card { + type Error = Error; + + fn try_from(card: &CardData) -> Result { + let name = card.name(); + + // Strip "poll2choice_" etc. prefix + let kind_name = name.rsplit_once(':').map_or(name, |(_, suffix)| suffix); + let kind = CardKind::from_name(kind_name); + + // Handle unified cards specially + if kind == CardKind::Unified { + return Ok(parse_unified_card(card.binding_values())); + } + + let bv = card.binding_values(); + + let url = card.url().to_owned(); + + let title = bv.string("title").to_owned(); + let text = bv.string("description").to_owned(); + let dest = { + let vanity = bv.string("vanity_url"); + if vanity.is_empty() { + bv.string("domain").to_owned() + } else { + vanity.to_owned() + } + }; + + // Try multiple image keys in order of preference (with _large suffix) + let image = [ + "summary_photo_image_large", + "player_image_large", + "promo_image_large", + "photo_image_full_size_large", + "thumbnail_image_large", + "thumbnail_large", + "event_thumbnail_large", + "image_large", + // Fallbacks without _large suffix + "thumbnail_image", + "player_image", + "summary_photo_image", + "image", + ] + .iter() + .map(|key| bv.image(key)) + .find(|img| !img.is_empty()) + .unwrap_or_default() + .to_owned(); + + // Card-type-specific logic + let mut video = None; + let mut card_url = url; + let mut card_text = text; + + match kind { + CardKind::PromoVideo + | CardKind::PromoVideoConvo + | CardKind::AppPlayer + | CardKind::VideoDirectMessage => { + video = Some(parse_promo_video(bv)); + if kind == CardKind::AppPlayer { + let app_cat = bv.string("app_category"); + if !app_cat.is_empty() { + app_cat.clone_into(&mut card_text); + } + } + }, + CardKind::Broadcast => { + let broadcast_image = bv.image("broadcast_thumbnail_large").to_owned(); + return Ok(Self { + kind, + url: bv.string("broadcast_url").to_owned(), + title: bv.string("broadcaster_display_name").to_owned(), + dest, + text: bv.string("broadcast_title").to_owned(), + image: broadcast_image.clone(), + member_count: 0, + video: Some(Video { + thumb: broadcast_image, + available: true, + ..Default::default() + }), + }); + }, + CardKind::LiveEvent => { + bv.string("event_title").clone_into(&mut card_text); + }, + CardKind::Player => { + let player_url = bv.string("player_url"); + if !player_url.is_empty() { + card_url = player_url.replace("/embed/", "/watch?v="); + } + }, + _ => {}, + } + + // Clear URL for DM cards + if matches!( + kind, + CardKind::VideoDirectMessage | CardKind::ImageDirectMessage + ) { + card_url = String::new(); + } + + Ok(Self { + kind, + url: card_url, + title, + dest, + text: card_text, + image, + video, + member_count: 0, + }) + } +} + +/// Parse promo video from card binding values. +fn parse_promo_video(bv: &BindingValues) -> Video { + let thumb = bv.image("player_image_large").to_owned(); + let duration_secs = bv.string("content_duration_seconds").parse().unwrap_or(0); + + let stream_url = ["player_hls_url", "player_stream_url", "amplify_url_vmap"] + .iter() + .map(|k| bv.string(k)) + .find(|v| !v.is_empty()) + .unwrap_or_default() + .to_owned(); + + let (content_type, playback_type) = if stream_url.contains("m3u8") { + (VideoType::M3u8, VideoType::M3u8) + } else { + (VideoType::Vmap, VideoType::Vmap) + }; + + let variant = VideoVariant { + content_type, + url: stream_url.clone(), + bitrate: 0, + resolution: 0, + }; + + Video { + duration_ms: duration_secs * 1000, + url: stream_url, + thumb, + available: true, + playback_type, + variants: vec![variant], + ..Default::default() + } +} + +/// Parse unified card (complex card format). +#[expect( + clippy::too_many_lines, + reason = "unified card parsing has inherent branching" +)] +fn parse_unified_card(bv: &BindingValues) -> Card { + let raw_json = bv.string("unified_card"); + let unified_card_json = if raw_json.is_empty() { "{}" } else { raw_json }; + + let card = serde_json::from_str::(unified_card_json).unwrap_or_default(); + + let mut result = Card { + kind: CardKind::Summary, + url: String::new(), + title: String::new(), + dest: String::new(), + text: String::new(), + image: String::new(), + video: None, + member_count: 0, + }; + + let destinations = card.destination_objects.unwrap_or_default(); + let media_entities = card.media_entities.unwrap_or_default(); + let app_store_data = card.app_store_data.unwrap_or_default(); + + if let Some(components) = card.component_objects { + #[expect(clippy::iter_over_hash_type, reason = "component order is irrelevant")] + for component in components.values() { + let comp_type = component.comp_type.as_deref().unwrap_or(""); + let Some(data) = component.data.as_ref() else { + continue; + }; + + match comp_type { + "details" | "twitter_list_details" | "community_details" => { + if let Some(dest_id) = data.destination.as_deref() { + let (url, vanity) = extract_destination(dest_id, &destinations); + result.url = url; + result.dest = vanity; + } + data + .title + .as_deref() + .or(data.name.as_deref()) + .unwrap_or("") + .clone_into(&mut result.title); + match comp_type { + "twitter_list_details" => { + result.member_count = data.member_count; + "List".clone_into(&mut result.dest); + }, + "community_details" => { + result.member_count = data.member_count; + "Community".clone_into(&mut result.dest); + }, + _ => {}, + } + }, + "media" | "swipeable_media" => { + let media_id = if comp_type == "swipeable_media" { + data + .media_list + .as_ref() + .and_then(|list| list.first()) + .and_then(|media_ref| media_ref.id.as_deref()) + } else { + data.id.as_deref() + }; + + if let Some(mid) = media_id + && let Some(media) = media_entities.get(mid) + { + if let Some(img_url) = media.media_url_https.as_deref() { + img_url.clone_into(&mut result.image); + } + + match media.media_type.unwrap_or_default() { + MediaType::Photo => { + result.kind = CardKind::SummaryLarge; + }, + MediaType::Video => { + result.kind = CardKind::PromoVideo; + }, + MediaType::Model3d => { + "Unsupported 3D model".clone_into(&mut result.title); + }, + _ => {}, + } + } + }, + "app_store_details" => { + if let Some(app_id) = data.app_id.as_deref() + && let Some(app_entry) = app_store_data + .get(app_id) + .and_then(|entries| entries.first()) + { + let app_type = app_entry.app_type.as_deref().unwrap_or(""); + let app_store_id = app_entry.id.as_deref().unwrap_or(""); + + match app_type { + "android_app" => { + result.url = + format!("http://play.google.com/store/apps/details?id={app_store_id}"); + }, + "iphone_app" | "ipad_app" => { + result.url = format!("https://itunes.apple.com/app/id{app_store_id}"); + }, + _ => {}, + } + + app_entry + .title + .as_deref() + .unwrap_or("") + .clone_into(&mut result.title); + app_entry + .category + .as_deref() + .unwrap_or("") + .clone_into(&mut result.dest); + } + }, + "job_details" => { + result.kind = CardKind::JobDetails; + + if let Some(dest_id) = data.destination.as_deref() { + let (url, _) = extract_destination(dest_id, &destinations); + result.url = url; + } + + data + .title + .as_deref() + .unwrap_or("") + .clone_into(&mut result.title); + data + .short_description_text + .as_deref() + .unwrap_or("") + .clone_into(&mut result.text); + + let username = data + .profile_user + .as_ref() + .and_then(|profile| profile.username.as_deref()) + .unwrap_or(""); + let location = data.location.as_deref().unwrap_or(""); + result.dest = format!("@{username} · {location}"); + }, + "grok_share" => { + result.kind = CardKind::SummaryLarge; + + if let Some(dest_id) = data.destination.as_deref() { + let (url, _) = extract_destination(dest_id, &destinations); + result.url = url; + } + + "Answer by Grok".clone_into(&mut result.dest); + + let truncate = |text: &str, max: usize| { + let truncated: String = text.chars().take(max).collect(); + if truncated.len() < text.len() { + format!("{truncated}...") + } else { + truncated + } + }; + if let Some(conversation) = data.conversation_preview.as_ref() { + for msg in conversation { + match msg.sender.as_deref() { + Some("USER") => { + result.title = truncate(msg.message.as_deref().unwrap_or(""), 70); + }, + Some("AGENT") => { + result.text = truncate(msg.message.as_deref().unwrap_or(""), 500); + }, + _ => {}, + } + } + } + }, + "hidden" => { + result.kind = CardKind::Hidden; + }, + "button_group" => {}, + _ => { + tracing::debug!("Unknown unified card component type: {comp_type}"); + }, + } + } + } + + result +} + +impl TryFrom<&CardData> for Poll { + type Error = Error; + + fn try_from(card: &CardData) -> Result { + let name = card.name(); + + if !name.contains("poll") { + return Err(Error::Internal("Not a poll card".into())); + } + + let bv = card.binding_values(); + + let mut options = Vec::new(); + let mut values = Vec::new(); + + for idx in 1..=4 { + let label = bv.string(&format!("choice{idx}_label")); + if label.is_empty() { + break; + } + options.push(label.to_owned()); + + let count = bv + .string(&format!("choice{idx}_count")) + .parse() + .unwrap_or(0); + values.push(count); + } + + let votes = values.iter().sum(); + #[expect( + clippy::cast_possible_wrap, + reason = "poll option count is always tiny" + )] + let leader = values + .iter() + .enumerate() + .max_by_key(|&(_, val)| val) + .map_or(0, |(idx, _)| idx as i64); + + // Parse poll end time from end_datetime_utc (display formatting deferred + // to views via Poll::status_text) + let end_time = time::OffsetDateTime::parse(bv.string("end_datetime_utc"), &Rfc3339).ok(); + + // Extract poll image if the card name contains "image" + let image = if name.contains("image") { + let img = bv.image("image_large"); + if img.is_empty() { + None + } else { + Some(img.to_owned()) + } + } else { + None + }; + + Ok(Self { + options, + values, + votes, + leader, + end_time, + image, + }) + } +} diff --git a/src/api/parser/conversation.rs b/src/api/parser/conversation.rs new file mode 100644 index 0000000..bb1e096 --- /dev/null +++ b/src/api/parser/conversation.rs @@ -0,0 +1,144 @@ +use super::tweet::parse_tweet_object; +use crate::{ + api::schema::{ + ConversationData, + InstructionType, + }, + error::{ + Error, + Result, + }, + types::{ + Chain, + Conversation, + PaginatedResult, + Tweet, + }, +}; + +/// Parse a conversation from `TweetDetail` response. +pub fn parse_conversation(data: &ConversationData, tweet_id: &str) -> Result { + // Single tweet result path + if let Some(tweet_data) = data + .tweet_result + .as_ref() + .and_then(|nested| nested.result.as_deref()) + && tweet_data.__typename.is_some() + { + let tweet = parse_tweet_object(tweet_data)?; + return Ok(Conversation { + tweet, + ..Default::default() + }); + } + + // Instructions path + let raw_instructions = data + .threaded_conversation_with_injections_v2 + .as_ref() + .map(|conv| conv.instructions.as_slice()) + .filter(|instr| !instr.is_empty()) + .ok_or_else(|| Error::TweetNotFound("No instructions array".to_owned()))?; + + let mut main_tweet = Option::::None; + let mut before = Chain::default(); + let mut after = Chain::default(); + let mut replies = PaginatedResult::::default(); + + // Parse tweet_id for matching + let target_id = tweet_id.parse().unwrap_or(0); + + for instruction in raw_instructions { + if instruction.instruction_type != Some(InstructionType::TimelineAddEntries) { + continue; + } + + for entry in instruction.entries.as_deref().unwrap_or_default() { + let entry_id = entry.entry_id_str(); + + if entry_id.starts_with("tweet-") { + // Single tweet entry -- match by ID to determine main vs before + if let Some(tweet) = entry + .tweet_result() + .and_then(|result| parse_tweet_object(result).ok()) + { + if tweet.id == target_id { + main_tweet = Some(tweet); + } else { + before.content.push(tweet); + } + } + } else if entry_id.starts_with("tombstone") { + // Tombstone entry -- unavailable tweet in conversation + let tombstone_id = entry_id + .split('-') + .next_back() + .and_then(|id_str| id_str.parse().ok()) + .unwrap_or(0); + let tweet = Tweet { + id: tombstone_id, + available: false, + tombstone: "This tweet is unavailable".to_owned(), + ..Default::default() + }; + if tombstone_id == target_id { + main_tweet = Some(tweet); + } else { + before.content.push(tweet); + } + } else if entry_id.starts_with("conversationthread-") { + // Thread/replies -- check for self-thread (after) vs other replies + let mut chain = Chain::default(); + let mut is_self_thread = false; + + for item in entry.items() { + // Check for "Show more" cursor in thread + if item.entry_id_str().contains("cursor-showmore") { + let cursor = item.cursor_value().unwrap_or_default(); + chain.has_more = true; + chain.cursor = Some(cursor.to_owned()); + continue; + } + + if let Some(tweet_result) = item.tweet_result() + && let Ok(tweet) = parse_tweet_object(tweet_result) + { + chain.content.push(tweet); + } + + // Check if this is a self-thread + if item.display_type() == Some("SelfThread") { + is_self_thread = true; + } + } + + if !chain.content.is_empty() { + if is_self_thread { + after = chain; + } else { + replies.content.push(chain); + } + } + } else if entry_id.starts_with("cursor-") { + // Pagination cursor + if let Some(cursor_value) = entry.cursor_value() { + if entry_id.contains("bottom") { + replies.bottom = Some(cursor_value.to_owned()); + } else if entry_id.contains("top") { + replies.top = Some(cursor_value.to_owned()); + } + } + } + } + } + + let tweet = main_tweet + .ok_or_else(|| Error::TweetNotFound("Main tweet not found in conversation".to_owned()))?; + + Ok(Conversation { + tweet, + before, + after, + replies, + }) +} diff --git a/src/api/parser/edit_history.rs b/src/api/parser/edit_history.rs new file mode 100644 index 0000000..12ceb20 --- /dev/null +++ b/src/api/parser/edit_history.rs @@ -0,0 +1,62 @@ +use super::tweet::parse_tweet_object; +use crate::{ + api::schema::{ + EditHistoryData, + InstructionType, + }, + error::{ + Error, + Result, + }, + types::EditHistory, +}; + +/// Parse a `TweetEditHistory` response. +#[expect( + clippy::module_name_repetitions, + reason = "clear naming for public API" +)] +pub fn parse_edit_history(data: &EditHistoryData) -> Result { + let raw_instructions = data.instructions(); + + let mut result = EditHistory::default(); + + for instruction in raw_instructions { + if instruction.instruction_type != Some(InstructionType::TimelineAddEntries) { + continue; + } + + for entry in instruction.entries.as_deref().unwrap_or(&[]) { + let entry_id = entry.entry_id_str(); + + if entry_id == "latestTweet" { + // Latest version is in items[0] + if let Some(tweet) = entry + .items() + .first() + .and_then(|item| item.tweet_result()) + .map(parse_tweet_object) + .transpose()? + { + result.latest = tweet; + } + } else if entry_id == "staleTweets" { + // Previous versions + for item in entry.items() { + if let Some(tweet) = item + .tweet_result() + .and_then(|tw_result| parse_tweet_object(tw_result).ok()) + { + result.history.push(tweet); + } + } + } + } + } + + if result.latest.id == 0 { + return Err(Error::TweetNotFound("No edit history found".to_owned())); + } + + Ok(result) +} diff --git a/src/api/parser/entity.rs b/src/api/parser/entity.rs new file mode 100644 index 0000000..ce26818 --- /dev/null +++ b/src/api/parser/entity.rs @@ -0,0 +1,64 @@ +use crate::{ + api::schema::{ + Entities, + indices, + }, + types::{ + Entity, + EntityKind, + }, +}; + +/// Parse entities from a typed `Entities` struct. +/// +/// Stores only raw API data -- display formatting (`short_url`, hashtag +/// display) is deferred to `entity_expander::expand_entities()`. +pub fn parse_entities_from(raw: &Entities) -> Vec { + let mut entities = Vec::new(); + + for url_ent in &raw.urls { + let (start, end) = indices(&url_ent.indices); + let expanded = url_ent.expanded_url.as_deref().unwrap_or_default(); + entities.push(Entity { + indices: (start, end), + kind: EntityKind::Url, + url: expanded.to_owned(), + ..Default::default() + }); + } + + // Hashtag/symbol display and URLs are computed by the expander from the + // original text -- only indices and kind are needed. + for hashtag in &raw.hashtags { + let (start, end) = indices(&hashtag.indices); + entities.push(Entity { + indices: (start, end), + kind: EntityKind::Hashtag, + ..Default::default() + }); + } + + for mention in &raw.user_mentions { + let (start, end) = indices(&mention.indices); + let screen_name = mention.screen_name.as_deref().unwrap_or_default(); + let name = mention.name.as_deref().unwrap_or(screen_name); + entities.push(Entity { + indices: (start, end), + kind: EntityKind::Mention, + url: format!("/{screen_name}"), + display: name.to_owned(), + }); + } + + for symbol in &raw.symbols { + let (start, end) = indices(&symbol.indices); + entities.push(Entity { + indices: (start, end), + kind: EntityKind::Symbol, + ..Default::default() + }); + } + + entities.sort_by_key(|ent| ent.indices.0); + entities +} diff --git a/src/api/parser/media.rs b/src/api/parser/media.rs new file mode 100644 index 0000000..20d1418 --- /dev/null +++ b/src/api/parser/media.rs @@ -0,0 +1,197 @@ +use std::cmp; + +use crate::{ + api::schema::{ + MediaItem, + MediaType, + RawVideoContentType, + TweetLegacy, + }, + types::{ + Gif, + Photo, + User, + Video, + VideoType, + VideoVariant, + }, +}; + +/// Parsed media result (no side effects on tweet text). +pub struct ParsedMedia { + pub photos: Vec, + pub video: Option

+
{}
+ +"#, + html_escape(&msg) + ); + + (status, Html(html)).into_response() + } +} + +// Twitter API error codes +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TwitterError { + NoUserMatches = 17, + ProtectedUser = 22, + UserNotFound = 50, + UserSuspended = 63, + RateLimited = 88, + InvalidToken = 89, + TweetNotFound = 144, + TweetUnavailable = 179, + NoStatusFound = 220, + BadToken = 239, + Locked = 326, + TweetUnavailable421 = 421, + TweetCensored = 422, +} + +impl TwitterError { + pub const fn from_code(code: i64) -> Option { + match code { + 17 => Some(Self::NoUserMatches), + 22 => Some(Self::ProtectedUser), + 50 => Some(Self::UserNotFound), + 63 => Some(Self::UserSuspended), + 88 => Some(Self::RateLimited), + 89 => Some(Self::InvalidToken), + 144 => Some(Self::TweetNotFound), + 179 => Some(Self::TweetUnavailable), + 220 => Some(Self::NoStatusFound), + 239 => Some(Self::BadToken), + 326 => Some(Self::Locked), + 421 => Some(Self::TweetUnavailable421), + 422 => Some(Self::TweetCensored), + _ => None, + } + } +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..15e62f9 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,122 @@ +mod api; +mod cache; +mod config; +mod error; +mod routes; +mod types; +mod utils; +mod views; + +use std::{ + env, + net::SocketAddr, + sync::Arc, +}; + +use axum::{ + Router, + middleware, +}; +use tokio::net::TcpListener; +use tower_http::services::{ + ServeDir, + ServeFile, +}; +use tracing_subscriber::{ + fmt, + layer::SubscriberExt as _, + util::SubscriberInitExt as _, +}; + +use crate::{ + api::{ + ApiClient, + HttpClient, + SessionPool, + }, + cache::Cache, + config::Config, +}; + +/// Application state shared across all routes. +#[derive(Clone)] +pub struct AppState { + pub config: Arc, + pub cache: Cache, + pub api: ApiClient, + pub http_client: HttpClient, +} + +#[tokio::main] +async fn main() -> eyre::Result<()> { + // Initialize logging + tracing_subscriber::registry() + .with( + tracing_subscriber::EnvFilter::try_from_default_env() + .unwrap_or_else(|_| "teapot=debug,tower_http=debug".into()), + ) + .with(fmt::layer()) + .init(); + + // Load configuration + let config_path = env::var("TEAPOT_CONF_FILE").unwrap_or_else(|_| "config/teapot.toml".to_owned()); + let config = Config::load(&config_path)?; + let config = Arc::new(config); + + tracing::info!( + "Starting {} on {}:{}", + config.server.title, + config.server.address, + config.server.port + ); + + // Initialize cache + let cache = Cache::new(); + + // Initialize session pool + let sessions_path = + env::var("TEAPOT_SESSIONS_FILE").unwrap_or_else(|_| "sessions.jsonl".to_owned()); + let sessions = SessionPool::load(&sessions_path).await?; + + // Initialize API client + let api = ApiClient::new(Arc::clone(&config), sessions); + + // Create application state + let state = AppState { + config: Arc::clone(&config), + cache, + api, + http_client: HttpClient::new(), + }; + + // Build router - order matters: specific routes first, then static files + let static_dir = config.server.static_dir.clone(); + let app = Router::new() + .merge(routes::router()) + // Serve static files at various paths + .nest_service("/public", ServeDir::new(&static_dir)) + .nest_service("/css", ServeDir::new(format!("{static_dir}/css"))) + .nest_service("/js", ServeDir::new(format!("{static_dir}/js"))) + .nest_service("/fonts", ServeDir::new(format!("{static_dir}/fonts"))) + .nest_service("/md", ServeDir::new(format!("{static_dir}/md"))) + // Root-level static files + .route_service("/logo.png", ServeFile::new(format!("{static_dir}/logo.png"))) + .route_service("/favicon.ico", ServeFile::new(format!("{static_dir}/favicon.ico"))) + .route_service("/favicon-16x16.png", ServeFile::new(format!("{static_dir}/favicon-16x16.png"))) + .route_service("/favicon-32x32.png", ServeFile::new(format!("{static_dir}/favicon-32x32.png"))) + .route_service("/apple-touch-icon.png", ServeFile::new(format!("{static_dir}/apple-touch-icon.png"))) + .route_service("/site.webmanifest", ServeFile::new(format!("{static_dir}/site.webmanifest"))) + .route_service("/robots.txt", ServeFile::new(format!("{static_dir}/robots.txt"))) + .route_service("/opensearch.xml", ServeFile::new(format!("{static_dir}/opensearch.xml"))) + .layer(middleware::from_fn(routes::prefs_middleware)) + .with_state(state); + + // Start server + let addr = SocketAddr::new(config.server.address.parse()?, config.server.port); + let listener = TcpListener::bind(addr).await?; + + tracing::info!("Listening on {addr}"); + axum::serve(listener, app).await?; + + Ok(()) +} diff --git a/src/routes/debug.rs b/src/routes/debug.rs new file mode 100644 index 0000000..b67e349 --- /dev/null +++ b/src/routes/debug.rs @@ -0,0 +1,30 @@ +use axum::{ + Json, + Router, + extract::State, + routing::get, +}; + +use crate::{ + AppState, + api::{ + DebugResponse, + HealthResponse, + }, +}; + +pub fn router() -> Router { + Router::new() + .route("/.health", get(health_check)) + .route("/.sessions", get(sessions_debug)) +} + +/// Health check endpoint returning session pool statistics. +async fn health_check(State(state): State) -> Json { + Json(state.api.get_session_health().await) +} + +/// Detailed sessions debug endpoint. +async fn sessions_debug(State(state): State) -> Json { + Json(state.api.get_session_debug().await) +} diff --git a/src/routes/embed.rs b/src/routes/embed.rs new file mode 100644 index 0000000..dc46cb4 --- /dev/null +++ b/src/routes/embed.rs @@ -0,0 +1,162 @@ +use axum::{ + Router, + extract::{ + Path, + Query, + State, + }, + http::{ + HeaderMap, + header, + }, + response::{ + Html, + IntoResponse as _, + Json, + Redirect, + Response, + }, + routing::get, +}; +use serde::Deserialize; + +use crate::{ + AppState, + error::{ + Error, + Result, + }, + views::embed as embed_view, +}; + +#[derive(Debug, Deserialize)] +pub struct OEmbedQuery { + pub text: Option, + pub author: Option, + pub status: Option, +} + +#[derive(Debug, Deserialize)] +pub struct LegacyEmbedQuery { + pub id: Option, +} + +pub fn router() -> Router { + Router::new() + .route("/{username}/status/{id}/embed", get(tweet_embed)) + .route("/i/videos/tweet/{id}", get(video_embed)) + // Legacy embed URL support (Twitter's old embed format) + .route("/embed/Tweet.html", get(legacy_embed_redirect)) + // ActivityPub endpoint for Discord multi-image support + .route("/users/{username}/statuses/{id}", get(activity_pub_status)) + // oEmbed endpoint for engagement metrics in Discord embeds + .route("/owoembed", get(oembed)) +} + +/// Redirect legacy `/embed/Tweet.html?id=XXX` to `/i/status/XXX/embed`. +async fn legacy_embed_redirect(Query(query): Query) -> Response { + query.id.map_or_else( + || Redirect::to("/").into_response(), + |id| Redirect::to(&format!("/i/status/{id}/embed")).into_response(), + ) +} + +/// Tweet embed endpoint - returns HTML with proper OG meta tags for +/// Discord/etc. +async fn tweet_embed( + State(state): State, + Path((_username, id)): Path<(String, String)>, +) -> Result { + // Fetch tweet from API + let tweet = state.api.get_tweet(&id).await?; + + let markup = embed_view::render_tweet_embed(&tweet, &state.config); + Ok(Html(markup.into_string()).into_response()) +} + +/// Video embed endpoint - returns HTML with video player. +async fn video_embed(State(state): State, Path(id): Path) -> Result { + // Fetch tweet from API + let tweet = state.api.get_tweet(&id).await?; + + if tweet.video.is_none() && tweet.gif.is_none() { + return Err(Error::NotFound("No video found".into())); + } + + let markup = embed_view::render_video_embed(&tweet, &state.config); + Ok(Html(markup.into_string()).into_response()) +} + +/// `ActivityPub` JSON endpoint for Discord multi-image support. +/// Discord fetches this to get all media attachments for carousel display. +async fn activity_pub_status( + State(state): State, + Path((username, id)): Path<(String, String)>, + headers: HeaderMap, +) -> Result { + let accept = headers + .get(header::ACCEPT) + .and_then(|hv| hv.to_str().ok()) + .unwrap_or(""); + + // Check if client wants ActivityPub JSON + if accept.contains("application/activity+json") || accept.contains("application/ld+json") { + // Fetch tweet from API + let tweet = state.api.get_tweet(&id).await?; + + // Build ActivityPub JSON + let activity = embed_view::build_activity_pub(&tweet, &state.config); + + return Ok(( + [( + header::CONTENT_TYPE, + "application/activity+json; charset=utf-8", + )], + Json(activity), + ) + .into_response()); + } + + // Otherwise redirect to normal status page + Ok(Redirect::to(&format!("/{username}/status/{id}")).into_response()) +} + +/// oEmbed JSON endpoint for engagement metrics in Discord embed footers. +async fn oembed( + State(state): State, + Query(query): Query, +) -> Result { + #[derive(serde::Serialize)] + struct OEmbedData { + author_name: String, + author_url: String, + provider_name: String, + provider_url: String, + title: &'static str, + #[serde(rename = "type")] + kind: &'static str, + version: &'static str, + } + + let text = query.text.unwrap_or_default(); + let author = query.author.unwrap_or_default(); + let status = query.status.unwrap_or_default(); + + let author_url = format!("https://x.com/{author}/status/{status}"); + + let oembed = OEmbedData { + author_name: text, + author_url, + provider_name: state.config.server.title.clone(), + provider_url: state.config.url_prefix().to_owned(), + title: "Post", + kind: "rich", + version: "1.0", + }; + + Ok(( + [(header::CONTENT_TYPE, "application/json; charset=utf-8")], + Json(oembed), + ) + .into_response()) +} diff --git a/src/routes/helpers.rs b/src/routes/helpers.rs new file mode 100644 index 0000000..fa7bbb0 --- /dev/null +++ b/src/routes/helpers.rs @@ -0,0 +1,77 @@ +use axum::{ + http::header, + response::{ + IntoResponse as _, + Response, + }, +}; + +use crate::{ + AppState, + cache::{ + keys as cache_keys, + ttl, + }, + error::Result, + types::{ + Timeline, + Tweet, + Tweets, + User, + }, +}; + +/// Fetch a user, using cache when available. +pub async fn get_cached_user(state: &AppState, username: &str) -> Result { + let cache_key = cache_keys::user(username); + if let Some(cached) = state.cache.get::(&cache_key) { + return Ok(cached); + } + let fetched = state.api.get_user(username).await?; + state.cache.set(&cache_key, &fetched, ttl::DEFAULT); + Ok(fetched) +} + +/// Build an RSS response with `Content-Type` and `Min-Id` headers. +pub fn rss_response(rss: String, tweets: &[Tweet]) -> Response { + let min_id = tweets.iter().map(|tweet| tweet.id).min(); + let mut response = ( + [(header::CONTENT_TYPE, "application/rss+xml; charset=utf-8")], + rss, + ) + .into_response(); + if let Some(id) = min_id { + response.headers_mut().insert( + header::HeaderName::from_static("min-id"), + header::HeaderValue::from(id), + ); + } + response +} + +/// Check RSS cache and return early if hit. +pub fn check_rss_cache(state: &AppState, key: &str) -> Option { + let cached = state.cache.get::(key)?; + Some( + ( + [(header::CONTENT_TYPE, "application/rss+xml; charset=utf-8")], + cached, + ) + .into_response(), + ) +} + +/// Cache an RSS result. +pub fn cache_rss(state: &AppState, key: &str, rss: &str) { + let owned = rss.to_owned(); + state + .cache + .set(key, &owned, state.config.cache.rss_minutes * 60); +} + +/// Extract tweet groups and cursor from a Timeline. +/// Preserves conversation grouping: each inner [`Vec`] is a conversation +/// thread (parent → reply chain) from a single `profile-conversation-*` entry. +pub fn extract_timeline(timeline: Timeline) -> (Vec, Option) { + (timeline.content, timeline.bottom) +} diff --git a/src/routes/intent.rs b/src/routes/intent.rs new file mode 100644 index 0000000..89d82a0 --- /dev/null +++ b/src/routes/intent.rs @@ -0,0 +1,104 @@ +use axum::{ + Router, + extract::{ + Query, + State, + }, + response::{ + Html, + IntoResponse as _, + Redirect, + Response, + }, + routing::get, +}; +use maud::html; +use serde::Deserialize; + +use super::unsupported::render_unsupported_feature; +use crate::{ + AppState, + error::Result, + views::layout, +}; + +pub fn router() -> Router { + Router::new() + .route("/intent/user", get(intent_user)) + .route("/intent/follow", get(intent_follow)) + // Catch-all for other intents + .route("/intent/{*path}", get(intent_unsupported)) +} + +/// Catch-all for unsupported intent handlers. +async fn intent_unsupported(State(state): State) -> Response { + render_unsupported_feature(&state, "Intent").into_response() +} + +#[derive(Debug, Deserialize)] +pub struct IntentUserQuery { + pub user_id: Option, + pub screen_name: Option, +} + +#[derive(Debug, Deserialize)] +pub struct IntentFollowQuery { + pub screen_name: Option, +} + +/// Handle `/intent/user` - redirect to user profile. +async fn intent_user( + State(state): State, + Query(query): Query, +) -> Result { + // If screen_name is provided, redirect directly + if let Some(ref screen_name) = query.screen_name + && !screen_name.is_empty() + { + return Ok(Redirect::to(&format!("/{screen_name}")).into_response()); + } + + // If user_id is provided, we need to look up the username + if let Some(ref user_id) = query.user_id + && !user_id.is_empty() + { + // For now, redirect to /i/user/{id} which should handle the lookup + // In a full implementation, we'd look up the user by ID + return Ok(Redirect::to(&format!("/i/user/{user_id}")).into_response()); + } + + // No valid parameters provided + let content = html! { + div class="error-page" { + h1 { "Invalid Intent" } + p { "Please provide either a screen_name or user_id parameter." } + } + }; + + let markup = layout::PageLayout::new(&state.config, "Invalid Intent", content).render(); + Ok(Html(markup.into_string()).into_response()) +} + +/// Handle `/intent/follow` - redirect to user profile (following requires login +/// on Twitter). +async fn intent_follow( + State(state): State, + Query(query): Query, +) -> Result { + if let Some(ref screen_name) = query.screen_name + && !screen_name.is_empty() + { + return Ok(Redirect::to(&format!("/{screen_name}")).into_response()); + } + + // No screen_name provided + let content = html! { + div class="error-page" { + h1 { "Invalid Intent" } + p { "Please provide a screen_name parameter." } + } + }; + + let markup = layout::PageLayout::new(&state.config, "Invalid Intent", content).render(); + Ok(Html(markup.into_string()).into_response()) +} diff --git a/src/routes/list.rs b/src/routes/list.rs new file mode 100644 index 0000000..4d50810 --- /dev/null +++ b/src/routes/list.rs @@ -0,0 +1,231 @@ +use axum::{ + Router, + extract::{ + Path, + Query, + State, + }, + http::StatusCode, + response::{ + Html, + IntoResponse as _, + Response, + }, + routing::get, +}; +use axum_extra::extract::CookieJar; +use maud::html; +use serde::Deserialize; + +use crate::{ + AppState, + cache::{ + keys as cache_keys, + ttl, + }, + error::{ + Error, + Result, + }, + types::{ + List, + PaginatedResult, + Prefs, + User, + }, + views::{ + layout, + timeline as timeline_view, + user_list, + }, +}; + +#[derive(Debug, Deserialize)] +pub struct ListQuery { + pub cursor: Option, +} + +pub fn router() -> Router { + Router::new() + .route("/{username}/lists", get(user_lists_unsupported)) + .route("/{username}/lists/", get(user_lists_unsupported)) + .route("/{username}/lists/{slug}", get(list_by_slug)) + .route("/i/lists/{id}", get(list_by_id)) + .route("/i/lists/{id}/members", get(list_members)) +} + +/// User lists browsing is not supported. +async fn user_lists_unsupported( + State(state): State, + jar: CookieJar, + Path(username): Path, +) -> Response { + let prefs = Prefs::from_cookies(&jar, &state.config); + let content = html! { + div class="overlay-panel" { + h1 { "User Lists" } + p { + "Browsing user lists is not currently supported. " + "You can access a specific list directly if you have its URL or slug." + } + p { + a href=(format!("/{username}")) { "Go to @" (username) "'s profile" } + } + } + }; + + let title = format!("@{username}'s Lists"); + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .render(); + Html(markup.into_string()).into_response() +} + +async fn list_by_slug( + State(state): State, + jar: CookieJar, + Path((username, slug)): Path<(String, String)>, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + + // Look up list by owner username and slug + let list = state.api.get_list_by_slug(&username, &slug).await?; + + // Fetch list tweets with cursor support + let timeline = state + .api + .get_list_tweets(&list.id, query.cursor.as_deref()) + .await?; + let groups = timeline.content; + let cursor = timeline.bottom.as_deref(); + let base_url = format!("/{username}/lists/{slug}"); + + let content = html! { + div class="timeline-container" { + (timeline_view::render_list_header(&list, "tweets", &state.config)) + (timeline_view::render_timeline(&groups, &state.config, cursor, Some(&base_url))) + } + }; + + let title = format!("{} - List by @{}", list.name, username); + let markup = layout::PageLayout::new(&state.config, &title, content) + .description(&list.description) + .prefs(&prefs) + .render(); + Ok(Html(markup.into_string()).into_response()) +} + +async fn list_by_id( + State(state): State, + jar: CookieJar, + Path(id): Path, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + + let (list, timeline) = tokio::join!( + async { + let cache_key = cache_keys::list(&id); + if let Some(cached) = state.cache.get::(&cache_key) { + tracing::debug!("Cache hit for list: {id}"); + return Ok::<_, Error>(cached); + } + let fetched = state.api.get_list(&id).await?; + state.cache.set(&cache_key, &fetched, ttl::DEFAULT); + Ok(fetched) + }, + state.api.get_list_tweets(&id, query.cursor.as_deref()), + ); + let list = list?; + let timeline = timeline?; + let groups = timeline.content; + let cursor = timeline.bottom.as_deref(); + let base_url = format!("/i/lists/{id}"); + + let content = html! { + div class="timeline-container" { + (timeline_view::render_list_header(&list, "tweets", &state.config)) + (timeline_view::render_timeline(&groups, &state.config, cursor, Some(&base_url))) + } + }; + + let title = format!("{} - List", list.name); + let markup = layout::PageLayout::new(&state.config, &title, content) + .description(&list.description) + .prefs(&prefs) + .render(); + Ok(Html(markup.into_string()).into_response()) +} + +async fn list_members( + State(state): State, + jar: CookieJar, + Path(id): Path, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + + let (list, members_result) = tokio::join!( + async { + let cache_key = cache_keys::list(&id); + if let Some(cached) = state.cache.get::(&cache_key) { + return Ok::<_, Error>(cached); + } + let fetched = state.api.get_list(&id).await?; + state.cache.set(&cache_key, &fetched, ttl::DEFAULT); + Ok(fetched) + }, + async { + let members_cache_key = cache_keys::list_members(&id); + if query.cursor.is_none() { + if let Some(cached) = state.cache.get::>(&members_cache_key) { + tracing::debug!("Cache hit for list members: {id}"); + return Ok(cached); + } + let result = state.api.get_list_members(&id, None).await; + if let Ok(ref members) = result { + state.cache.set(&members_cache_key, members, ttl::DEFAULT); + } + result + } else { + state + .api + .get_list_members(&id, query.cursor.as_deref()) + .await + } + }, + ); + let list = list?; + + match members_result { + Ok(members) => { + let cursor = members.bottom.as_deref(); + let base_url = format!("/i/lists/{id}/members"); + + let content = html! { + div class="timeline-container" { + (timeline_view::render_list_header(&list, "members", &state.config)) + (user_list::render_user_list(&members.content, &state.config, cursor, Some(&base_url), Some(&prefs))) + } + }; + + let title = format!("{} - Members", list.name); + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .render(); + Ok(Html(markup.into_string()).into_response()) + }, + Err(err) => { + let markup = + layout::render_error(&state.config, "Error loading members", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } +} + +// List header rendering lives in views::timeline::render_list_header diff --git a/src/routes/media.rs b/src/routes/media.rs new file mode 100644 index 0000000..c03e0d5 --- /dev/null +++ b/src/routes/media.rs @@ -0,0 +1,256 @@ +use std::sync::LazyLock; + +use axum::{ + Router, + body::Body, + extract::{ + Path, + State, + }, + http::{ + StatusCode, + header, + }, + response::{ + IntoResponse as _, + Response, + }, + routing::get, +}; + +use crate::{ + AppState, + error::{ + Error, + Result, + }, + utils::{ + formatters, + hmac, + }, +}; + +pub fn router() -> Router { + Router::new() + .route("/pic/{url}", get(pic_proxy)) + .route("/pic/enc/{url}", get(pic_proxy_encoded)) + .route("/pic/orig/{url}", get(pic_orig_proxy)) + .route("/pic/orig/enc/{url}", get(pic_orig_proxy_encoded)) + .route("/video/{sig}/{url}", get(video_proxy)) + .route("/video/enc/{sig}/{url}", get(video_proxy_encoded)) +} + +async fn pic_proxy(State(state): State, Path(url): Path) -> Result { + // Reject amplify_video URLs to prevent video thumbnails from being matched + if url.contains("/amplify_video/") { + return Err(Error::InvalidUrl("Not an image URL".into())); + } + proxy_image(&state, &url, false).await +} + +async fn pic_proxy_encoded( + State(state): State, + Path(url): Path, +) -> Result { + let decoded = formatters::base64_decode_url(&url) + .ok_or_else(|| Error::InvalidUrl("Invalid base64 encoding".into()))?; + if decoded.contains("/amplify_video/") { + return Err(Error::InvalidUrl("Not an image URL".into())); + } + proxy_image(&state, &decoded, false).await +} + +async fn pic_orig_proxy( + State(state): State, + Path(url): Path, +) -> Result { + if url.contains("/amplify_video/") { + return Err(Error::InvalidUrl("Not an image URL".into())); + } + proxy_image(&state, &url, true).await +} + +async fn pic_orig_proxy_encoded( + State(state): State, + Path(url): Path, +) -> Result { + let decoded = formatters::base64_decode_url(&url) + .ok_or_else(|| Error::InvalidUrl("Invalid base64 encoding".into()))?; + if decoded.contains("/amplify_video/") { + return Err(Error::InvalidUrl("Not an image URL".into())); + } + proxy_image(&state, &decoded, true).await +} + +async fn video_proxy( + State(state): State, + Path((sig, url)): Path<(String, String)>, +) -> Result { + // URL-decode the URL first + let decoded_url = percent_encoding::percent_decode_str(&url) + .decode_utf8() + .map_err(|_| Error::InvalidUrl("Invalid URL encoding".into()))? + .to_string(); + + // Verify HMAC signature using utility function + if !hmac::verify(&decoded_url, &sig, &state.config.config.hmac_key) { + return Err(Error::HmacVerification); + } + + proxy_video(&state, &decoded_url).await +} + +async fn video_proxy_encoded( + State(state): State, + Path((sig, url)): Path<(String, String)>, +) -> Result { + let decoded = formatters::base64_decode_url(&url) + .ok_or_else(|| Error::InvalidUrl("Invalid base64 encoding".into()))?; + + // Verify HMAC signature using utility function + if !hmac::verify(&decoded, &sig, &state.config.config.hmac_key) { + return Err(Error::HmacVerification); + } + + proxy_video(&state, &decoded).await +} + +async fn proxy_image(state: &AppState, url: &str, original: bool) -> Result { + if url.is_empty() || url == "/" { + return Err(Error::InvalidUrl("Empty image URL".into())); + } + + let full_url = if url.starts_with("http") { + url.to_owned() + } else { + format!("https://pbs.twimg.com{url}") + }; + + let full_url = if original { + format!("{full_url}?name=orig") + } else { + full_url + }; + + let response = state.http_client.get(&full_url).await?; + + let content_type = response + .headers() + .get(header::CONTENT_TYPE) + .and_then(|hv| hv.to_str().ok()) + .unwrap_or("image/jpeg") + .to_owned(); + + let bytes = response.bytes().await?; + + Ok(( + StatusCode::OK, + [ + (header::CONTENT_TYPE, content_type), + (header::CACHE_CONTROL, "max-age=604800".to_owned()), + ], + bytes, + ) + .into_response()) +} + +// Extract M3U8 URL from VMAP XML (regex matches url="...m3u8") +static VMAP_M3U8_RE: LazyLock = + LazyLock::new(|| regex::Regex::new(r#"url="([^"]+\.m3u8)"#).unwrap()); + +async fn proxy_video(state: &AppState, url: &str) -> Result { + let client = &state.http_client; + + // Handle VMAP: fetch XML, extract M3U8 URL, then fetch and proxy the M3U8 + if url.contains(".vmap") { + let vmap_response = client.get(url).await?; + if !vmap_response.status().is_success() { + return Err(Error::InvalidUrl("VMAP fetch failed".into())); + } + let vmap_content = vmap_response.text().await?; + + let m3u8_url = VMAP_M3U8_RE + .captures(&vmap_content) + .and_then(|caps| caps.get(1)) + .map(|mat| mat.as_str()); + + if let Some(m3u8_url) = m3u8_url { + let m3u8_response = client.get(m3u8_url).await?; + let manifest = m3u8_response.text().await?; + let rewritten = formatters::proxify_m3u8( + &manifest, + &state.config.config.hmac_key, + state.config.config.base64_media, + ); + return Ok(( + StatusCode::OK, + [ + ( + header::CONTENT_TYPE, + "application/vnd.apple.mpegurl".to_owned(), + ), + (header::CACHE_CONTROL, "max-age=604800".to_owned()), + ], + rewritten, + ) + .into_response()); + } + + return Err(Error::InvalidUrl("No M3U8 URL found in VMAP".into())); + } + + let response = client.get(url).await?; + + if !response.status().is_success() { + return Err(Error::InvalidUrl(format!( + "Video fetch failed: {}", + response.status() + ))); + } + + // Check if this is an M3U8 playlist that needs rewriting + let is_m3u8 = url.contains(".m3u8"); + + if is_m3u8 { + // Fetch and rewrite M3U8 manifest + let manifest = response.text().await?; + let rewritten = formatters::proxify_m3u8( + &manifest, + &state.config.config.hmac_key, + state.config.config.base64_media, + ); + + Ok(( + StatusCode::OK, + [ + ( + header::CONTENT_TYPE, + "application/vnd.apple.mpegurl".to_owned(), + ), + (header::CACHE_CONTROL, "max-age=604800".to_owned()), + ], + rewritten, + ) + .into_response()) + } else { + // Regular video/segment file - stream as-is + let content_type = response + .headers() + .get(header::CONTENT_TYPE) + .and_then(|hv| hv.to_str().ok()) + .unwrap_or("video/mp4") + .to_owned(); + + let bytes = response.bytes().await?; + + Ok(( + StatusCode::OK, + [ + (header::CONTENT_TYPE, content_type), + (header::CACHE_CONTROL, "max-age=604800".to_owned()), + ], + bytes, + ) + .into_response()) + } +} diff --git a/src/routes/mod.rs b/src/routes/mod.rs new file mode 100644 index 0000000..3b0bb37 --- /dev/null +++ b/src/routes/mod.rs @@ -0,0 +1,234 @@ +mod debug; +mod embed; +pub mod helpers; +mod intent; +mod list; +mod media; +mod preferences; +mod redirect; +mod rss; +mod search; +mod status; +mod timeline; + +mod unsupported; + +use axum::{ + Router, + extract::{ + Request, + State, + }, + middleware::Next, + response::{ + Html, + IntoResponse, + Redirect, + Response, + }, + routing::get, +}; +use axum_extra::extract::{ + CookieJar, + cookie::Cookie, +}; +use maud::html; +use time::Duration; + +use crate::{ + AppState, + types::Prefs, + views::{ + layout::PageLayout, + search as search_view, + }, +}; + +pub fn router() -> Router { + // ROUTE ORDERING RULES: + // + // Axum matches routes in merge order. The following constraints MUST hold: + // + // 1. embed::router() BEFORE status::router() + // - /users/{username}/statuses/{id} (ActivityPub) must not fall through to + // status handler + // + // 2. status::router() BEFORE timeline::router() + // - /{username}/status/{id} must match before the /{username} catch-all + // + // 3. All specific /i/* routes BEFORE unsupported::i_catchall_router() + // - /i/status/{id}, /i/user/{id} are handled by status::router() + // - /i/redirect is handled by redirect::router() + // - Everything else falls through to the /i/{*path} catch-all + // + // 4. timeline::router() MUST BE LAST + // - /{username} is a greedy catch-all that matches any single-segment path + // - timeline.rs validates against RESERVED_PATHS to reject non-usernames + // + // 5. media::router() BEFORE timeline::router() + // - /pic/* and /video/* must not match as usernames + // + // 6. list::router() BEFORE timeline::router() + // - /{username}/lists must match before /{username} catch-all sub-routes + + Router::new() + // ── Fixed routes (no ordering constraints) ── + .route("/", get(home)) + .route("/about", get(about)) + .route("/explore", get(|| async { Redirect::to("/about") })) + .route("/help", get(|| async { Redirect::to("/about") })) + .merge(unsupported::router()) + .merge(debug::router()) + .merge(redirect::router()) + .merge(intent::router()) + .merge(search::router()) + .merge(preferences::router()) + // ── Order-sensitive routes (see rules above) ── + .merge(embed::router()) // Rule 1: before status + .merge(status::router()) // Rule 2: before timeline + .merge(media::router()) // Rule 5: before timeline + .merge(rss::router()) // Has /{username}/rss, before timeline + .merge(list::router()) // Rule 6: before timeline + .merge(unsupported::i_catchall_router()) // Rule 3: after specific /i/* routes + .merge(timeline::router()) // Rule 4: MUST BE LAST +} + +/// Middleware: apply ?prefs= URL parameter overrides. +/// Parses `?prefs=key1=val1,key2=val2` and individual `?key=val` +/// params, sets them as cookies, then redirects to the clean URL. +pub async fn prefs_middleware(request: Request, next: Next) -> Response { + let uri = request.uri().clone(); + let query_string = uri.query().unwrap_or(""); + + // Check if ?prefs= parameter exists + let prefs_param = form_urlencoded::parse(query_string.as_bytes()) + .find(|&(ref key, _)| key == "prefs") + .map(|(_, val)| val.to_string()); + + if let Some(prefs_value) = prefs_param { + // Parse prefs: "key=val,key2=val2" + let mut jar = CookieJar::new(); + let pref_names = Prefs::COOKIE_NAMES; + + for pair in prefs_value.split(',') { + let (key, value) = match pair.split_once('=') { + Some((pkey, pval)) => (pkey, pval), + None if !pair.is_empty() => (pair, ""), + _ => continue, + }; + + if pref_names.contains(&key) { + let cookie = Cookie::build((key.to_owned(), value.to_owned())) + .path("/") + .max_age(Duration::days(365)) + .http_only(true) + .build(); + jar = jar.add(cookie); + } + } + + // Rebuild URL without prefs param + let path = uri.path(); + let clean_params = form_urlencoded::parse(query_string.as_bytes()) + .filter(|&(ref key, _)| key != "prefs") + .map(|(key, val)| { + if val.is_empty() { + key.to_string() + } else { + format!("{key}={val}") + } + }) + .collect::>(); + let redirect_url = if clean_params.is_empty() { + path.to_owned() + } else { + format!("{}?{}", path, clean_params.join("&")) + }; + + return (jar, Redirect::to(&redirect_url)).into_response(); + } + + // Also handle individual pref params (e.g., ?hlsPlayback=on) for cookieless + // overrides These don't redirect — they just override the cookie jar for + // this request + next.run(request).await +} + +async fn home(State(state): State, jar: CookieJar) -> impl IntoResponse { + let prefs = Prefs::from_cookies(&jar, &state.config); + let content = search_view::render_search_page(); + + let markup = PageLayout::new(&state.config, "Home", content) + .description("A privacy-focused Twitter/X frontend") + .prefs(&prefs) + .render(); + Html(markup.into_string()) +} + +async fn about(State(state): State, jar: CookieJar) -> impl IntoResponse { + let prefs = Prefs::from_cookies(&jar, &state.config); + let content = html! { + div class="overlay-panel" { + h1 { "About" } + + p { + "teapot is a free and open source alternative Twitter front-end focused on privacy and performance. " + "The source is available on GitHub at " + a href="https://github.com/amaanq/teapot" { "https://github.com/amaanq/teapot" } + } + + ul { + li { "No JavaScript or ads" } + li { "All requests go through the backend, client never talks to Twitter" } + li { "Prevents Twitter from tracking your IP or JavaScript fingerprint" } + li { "Uses Twitter's unofficial API (no developer account required)" } + li { "Lightweight" } + li { "RSS feeds" } + li { "Themes" } + li { "Mobile support (responsive design)" } + li { "AGPLv3 licensed, no proprietary instances permitted" } + } + + p { + "teapot's GitHub wiki contains " + a href="https://github.com/amaanq/teapot/wiki/Instances" { "instances" } + " and " + a href="https://github.com/amaanq/teapot/wiki/Extensions" { "browser extensions" } + " maintained by the community." + } + + h2 { "Why use teapot?" } + + p { + "It's impossible to use Twitter without JavaScript enabled, and as of 2024 you need to sign up. " + "For privacy-minded folks, preventing JavaScript analytics and IP-based tracking is important, " + "but apart from using a VPN and uBlock/uMatrix, it's impossible. Despite being behind a VPN and " + "using heavy-duty adblockers, you can get accurately tracked with your " + a href="https://restoreprivacy.com/browser-fingerprinting/" { "browser's fingerprint" } + ", " + a href="https://noscriptfingerprint.com/" { "no JavaScript required" } + ". This all became particularly important after Twitter " + a href="https://www.eff.org/deeplinks/2020/04/twitter-removes-privacy-option-and-shows-why-we-need-strong-privacy-laws" { "removed the ability" } + " for users to control whether their data gets sent to advertisers." + } + + p { + "Using an instance of teapot (hosted on a VPS for example), you can browse Twitter without " + "JavaScript while retaining your privacy. In addition to respecting your privacy, teapot is on " + "average around 15 times lighter than Twitter, and in most cases serves pages faster " + "(eg. timelines load 2-4x faster)." + } + + h2 { "Instance info" } + p { + "Version: teapot" + } + } + }; + + let markup = PageLayout::new(&state.config, "About", content) + .description("About teapot") + .prefs(&prefs) + .render(); + Html(markup.into_string()) +} diff --git a/src/routes/preferences.rs b/src/routes/preferences.rs new file mode 100644 index 0000000..874e650 --- /dev/null +++ b/src/routes/preferences.rs @@ -0,0 +1,368 @@ +use std::{ + fs, + path::Path, +}; + +use axum::{ + Form, + Router, + extract::{ + Query, + State, + }, + http::{ + HeaderMap, + header::REFERER, + }, + response::{ + Html, + IntoResponse as _, + Redirect, + Response, + }, + routing::{ + get, + post, + }, +}; +use axum_extra::extract::cookie::{ + Cookie, + CookieJar, +}; +use serde::Deserialize; +use time::Duration; + +use crate::{ + AppState, + config::Config, + error::Result, + types::Prefs, + views::{ + layout, + preferences as pref_view, + }, +}; + +/// Convert preferences to HTTP cookies for storage. +fn prefs_to_cookies(prefs: &Prefs) -> Vec> { + let max_age = Duration::days(365); + let bool_cookie = + |name: &str, val: bool| make_cookie(name, if val { "on" } else { "" }, max_age); + let str_cookie = |name: &str, val: &str| make_cookie(name, val, max_age); + + vec![ + str_cookie("theme", &prefs.theme), + bool_cookie("infiniteScroll", prefs.infinite_scroll), + bool_cookie("stickyProfile", prefs.sticky_profile), + bool_cookie("bidiSupport", prefs.bidi_support), + bool_cookie("hlsPlayback", prefs.hls_playback), + bool_cookie("mp4Playback", prefs.mp4_playback), + bool_cookie("proxyVideos", prefs.proxy_videos), + bool_cookie("autoplayGifs", prefs.autoplay_gifs), + bool_cookie("muteVideos", prefs.mute_videos), + bool_cookie("hideTweetStats", prefs.hide_tweet_stats), + bool_cookie("hideBanner", prefs.hide_banner), + bool_cookie("hidePins", prefs.hide_pins), + bool_cookie("hideReplies", prefs.hide_replies), + bool_cookie("hideCommunityNotes", prefs.hide_community_notes), + bool_cookie("squareAvatars", prefs.square_avatars), + bool_cookie("stickyNav", prefs.sticky_nav), + str_cookie("replaceTwitter", &prefs.replace_twitter), + str_cookie("replaceYouTube", &prefs.replace_youtube), + str_cookie("replaceReddit", &prefs.replace_reddit), + ] +} + +/// Encode non-default prefs as a comma-separated string for URL sharing. +#[expect( + clippy::cognitive_complexity, + reason = "macro-driven repetition, not actual complexity" +)] +fn encode_prefs(prefs: &Prefs, config: &Config) -> String { + let defaults = Prefs::with_defaults(config); + let mut pairs = Vec::new(); + + macro_rules! enc_checkbox { + ($name:expr, $field:ident) => { + if prefs.$field != defaults.$field { + if prefs.$field { + pairs.push(format!("{}=on", $name)); + } else { + pairs.push(format!("{}=", $name)); + } + } + }; + } + macro_rules! enc_string { + ($name:expr, $field:ident) => { + if prefs.$field != defaults.$field { + pairs.push(format!("{}={}", $name, prefs.$field)); + } + }; + } + + enc_string!("theme", theme); + enc_checkbox!("infiniteScroll", infinite_scroll); + enc_checkbox!("stickyProfile", sticky_profile); + enc_checkbox!("stickyNav", sticky_nav); + enc_checkbox!("bidiSupport", bidi_support); + enc_checkbox!("hideTweetStats", hide_tweet_stats); + enc_checkbox!("hideBanner", hide_banner); + enc_checkbox!("hidePins", hide_pins); + enc_checkbox!("hideReplies", hide_replies); + enc_checkbox!("hideCommunityNotes", hide_community_notes); + enc_checkbox!("squareAvatars", square_avatars); + enc_checkbox!("mp4Playback", mp4_playback); + enc_checkbox!("hlsPlayback", hls_playback); + enc_checkbox!("proxyVideos", proxy_videos); + enc_checkbox!("muteVideos", mute_videos); + enc_checkbox!("autoplayGifs", autoplay_gifs); + enc_string!("replaceTwitter", replace_twitter); + enc_string!("replaceYouTube", replace_youtube); + enc_string!("replaceReddit", replace_reddit); + + pairs.join(",") +} + +fn make_cookie(name: &str, value: &str, max_age: Duration) -> Cookie<'static> { + Cookie::build((name.to_owned(), value.to_owned())) + .path("/") + .max_age(max_age) + .http_only(true) + .build() +} + +pub fn router() -> Router { + Router::new() + .route("/settings", get(settings)) + .route("/saveprefs", post(save_prefs)) + .route("/resetprefs", post(reset_prefs)) + .route("/enablehls", post(enable_hls)) + .route("/enablemp4", post(enable_mp4)) +} + +#[derive(Debug, Deserialize)] +pub struct PrefsForm { + pub referer: Option, + pub theme: Option, + #[serde(rename = "infiniteScroll")] + pub infinite_scroll: Option, + #[serde(rename = "stickyProfile")] + pub sticky_profile: Option, + #[serde(rename = "bidiSupport")] + pub bidi_support: Option, + #[serde(rename = "hlsPlayback")] + pub hls_playback: Option, + #[serde(rename = "mp4Playback")] + pub mp4_playback: Option, + #[serde(rename = "proxyVideos")] + pub proxy_videos: Option, + #[serde(rename = "autoplayGifs")] + pub autoplay_gifs: Option, + #[serde(rename = "muteVideos")] + pub mute_videos: Option, + #[serde(rename = "hideTweetStats")] + pub hide_tweet_stats: Option, + #[serde(rename = "hideBanner")] + pub hide_banner: Option, + #[serde(rename = "hidePins")] + pub hide_pins: Option, + #[serde(rename = "hideReplies")] + pub hide_replies: Option, + #[serde(rename = "hideCommunityNotes")] + pub hide_community_notes: Option, + #[serde(rename = "squareAvatars")] + pub square_avatars: Option, + #[serde(rename = "stickyNav")] + pub sticky_nav: Option, + #[serde(rename = "replaceTwitter")] + pub replace_twitter: Option, + #[serde(rename = "replaceYouTube")] + pub replace_youtube: Option, + #[serde(rename = "replaceReddit")] + pub replace_reddit: Option, +} + +impl PrefsForm { + fn to_prefs(&self, config: &Config) -> Prefs { + let defaults = Prefs::with_defaults(config); + let parse_bool = + |field: &Option, default: bool| field.as_ref().map_or(default, |val| val == "on"); + let parse_str = |field: &Option, default: String| field.clone().unwrap_or(default); + + Prefs { + theme: parse_str(&self.theme, defaults.theme), + infinite_scroll: parse_bool(&self.infinite_scroll, defaults.infinite_scroll), + sticky_profile: parse_bool(&self.sticky_profile, defaults.sticky_profile), + bidi_support: parse_bool(&self.bidi_support, defaults.bidi_support), + hls_playback: parse_bool(&self.hls_playback, defaults.hls_playback), + mp4_playback: parse_bool(&self.mp4_playback, defaults.mp4_playback), + proxy_videos: parse_bool(&self.proxy_videos, defaults.proxy_videos), + autoplay_gifs: parse_bool(&self.autoplay_gifs, defaults.autoplay_gifs), + mute_videos: parse_bool(&self.mute_videos, defaults.mute_videos), + hide_tweet_stats: parse_bool(&self.hide_tweet_stats, defaults.hide_tweet_stats), + hide_banner: parse_bool(&self.hide_banner, defaults.hide_banner), + hide_pins: parse_bool(&self.hide_pins, defaults.hide_pins), + hide_replies: parse_bool(&self.hide_replies, defaults.hide_replies), + hide_community_notes: parse_bool( + &self.hide_community_notes, + defaults.hide_community_notes, + ), + square_avatars: parse_bool(&self.square_avatars, defaults.square_avatars), + sticky_nav: parse_bool(&self.sticky_nav, defaults.sticky_nav), + replace_twitter: parse_str(&self.replace_twitter, defaults.replace_twitter), + replace_youtube: parse_str(&self.replace_youtube, defaults.replace_youtube), + replace_reddit: parse_str(&self.replace_reddit, defaults.replace_reddit), + } + } +} + +#[derive(Debug, Deserialize)] +pub struct SettingsQuery { + pub referer: Option, +} + +async fn settings( + State(state): State, + jar: CookieJar, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + let themes = find_themes(&state.config.server.static_dir); + + let referer = query.referer.as_deref().unwrap_or("/"); + let prefs_code = encode_prefs(&prefs, &state.config); + let prefs_url = format!("{}/?prefs={}", state.config.url_prefix(), prefs_code); + let content = pref_view::render_preferences_form(&prefs, &themes, referer, &prefs_url); + let markup = layout::PageLayout::new(&state.config, "Settings", content) + .description("Customize your teapot experience") + .prefs(&prefs) + .render(); + + Ok(Html(markup.into_string()).into_response()) +} + +async fn save_prefs( + State(state): State, + jar: CookieJar, + Form(form): Form, +) -> Result { + let prefs = form.to_prefs(&state.config); + let cookies = prefs_to_cookies(&prefs); + + // Add all cookies to the jar + let mut updated_jar = jar; + for cookie in cookies { + updated_jar = updated_jar.add(cookie); + } + + // Redirect back to referer or settings page + let redirect_to = form.referer.as_deref().unwrap_or("/settings"); + + Ok((updated_jar, Redirect::to(redirect_to)).into_response()) +} + +#[derive(Debug, Deserialize)] +pub struct ResetPrefsForm { + pub referer: Option, +} + +async fn reset_prefs(jar: CookieJar, Form(form): Form) -> Result { + // Remove all preference cookies + let mut updated_jar = jar; + for name in Prefs::COOKIE_NAMES { + let removal = Cookie::build(name.to_string()) + .path("/") + .max_age(Duration::seconds(0)) + .build(); + updated_jar = updated_jar.remove(removal); + } + + // Redirect to referer or settings page + let redirect_to = form.referer.as_deref().unwrap_or("/settings"); + Ok((updated_jar, Redirect::to(redirect_to)).into_response()) +} + +async fn enable_hls(jar: CookieJar, headers: HeaderMap) -> Result { + let cookie = Cookie::build(("hlsPlayback".to_owned(), "on".to_owned())) + .path("/") + .max_age(Duration::days(365)) + .http_only(true) + .build(); + + let updated_jar = jar.add(cookie); + let referer = headers + .get(REFERER) + .and_then(|hv| hv.to_str().ok()) + .unwrap_or("/settings"); + + Ok((updated_jar, Redirect::to(referer)).into_response()) +} + +async fn enable_mp4(jar: CookieJar, headers: HeaderMap) -> Result { + let cookie = Cookie::build(("mp4Playback".to_owned(), "on".to_owned())) + .path("/") + .max_age(Duration::days(365)) + .http_only(true) + .build(); + + let updated_jar = jar.add(cookie); + let referer = headers + .get(REFERER) + .and_then(|hv| hv.to_str().ok()) + .unwrap_or("/settings"); + + Ok((updated_jar, Redirect::to(referer)).into_response()) +} + +/// Discover available themes from CSS files in the static directory. +#[expect( + clippy::absolute_paths, + reason = "crate's Result type shadows std::result::Result" +)] +fn find_themes(static_dir: &str) -> Vec { + let themes_dir = format!("{static_dir}/css/themes"); + + fs::read_dir(&themes_dir).map_or_else( + |_| { + vec![ + "Auto".to_owned(), + "Black".to_owned(), + "Dracula".to_owned(), + "Mastodon".to_owned(), + "teapot".to_owned(), + "Pleroma".to_owned(), + "Twitter".to_owned(), + "Twitter Dark".to_owned(), + ] + }, + |entries| { + let mut themes = entries + .filter_map(std::result::Result::ok) + .filter_map(|entry| entry.file_name().into_string().ok()) + .filter(|name| { + Path::new(name) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("css")) + }) + .map(|name| { + name + .trim_end_matches(".css") + .replace('_', " ") + .split_whitespace() + .map(|word| { + let mut chars = word.chars(); + chars.next().map_or_else(String::new, |first| { + first.to_uppercase().chain(chars).collect() + }) + }) + .collect::>() + .join(" ") + }) + .collect::>(); + + themes.sort(); + themes + }, + ) +} diff --git a/src/routes/redirect.rs b/src/routes/redirect.rs new file mode 100644 index 0000000..4354382 --- /dev/null +++ b/src/routes/redirect.rs @@ -0,0 +1,170 @@ +use axum::{ + Router, + extract::{ + Path, + Query, + State, + }, + response::{ + IntoResponse as _, + Redirect, + Response, + }, + routing::get, +}; +use axum_extra::extract::CookieJar; +use serde::Deserialize; + +use crate::{ + AppState, + config::Config, + error::{ + Error, + Result, + }, + types::Prefs, +}; + +pub fn router() -> Router { + Router::new() + .route("/i/redirect", get(redirect_handler)) + .route("/t.co/{url}", get(tco_resolver)) + .route("/cards/{card}/{id}", get(card_resolver)) +} + +#[derive(Debug, Deserialize)] +pub struct RedirectQuery { + pub url: Option, +} + +/// Handle `/i/redirect?url=...` - decode and transform URL before redirecting. +async fn redirect_handler( + State(state): State, + Query(query): Query, + jar: CookieJar, +) -> Result { + let url = query.url.as_deref().unwrap_or(""); + + if url.is_empty() { + return Err(Error::NotFound("No URL provided".into())); + } + + // Decode URL if needed + let decoded_url = percent_decode(url); + + // Get user preferences for URL replacements + let prefs = Prefs::from_cookies(&jar, &state.config); + + // Apply URL transformations based on preferences + let transformed = apply_url_replacements(&decoded_url, &prefs, &state.config); + + Ok(Redirect::to(&transformed).into_response()) +} + +/// Resolve `t.co` short URLs. +async fn tco_resolver( + State(state): State, + Path(url): Path, + jar: CookieJar, +) -> Result { + let full_url = format!("https://t.co/{url}"); + + // Resolve the redirect + let resolved = resolve_url(&state, &full_url).await?; + + // Get user preferences for URL replacements + let prefs = Prefs::from_cookies(&jar, &state.config); + + // Apply URL transformations + let transformed = apply_url_replacements(&resolved, &prefs, &state.config); + + Ok(Redirect::to(&transformed).into_response()) +} + +/// Resolve Twitter card URLs. +async fn card_resolver( + State(state): State, + Path((card, id)): Path<(String, String)>, + jar: CookieJar, +) -> Result { + let full_url = format!("https://cards.twitter.com/cards/{card}/{id}"); + + // Resolve the redirect + let resolved = resolve_url(&state, &full_url).await?; + + // Get user preferences for URL replacements + let prefs = Prefs::from_cookies(&jar, &state.config); + + // Apply URL transformations + let transformed = apply_url_replacements(&resolved, &prefs, &state.config); + + Ok(Redirect::to(&transformed).into_response()) +} + +/// Resolve a URL by following redirects and returning the final Location +/// header. +/// +/// hyper doesn't follow redirects by default (unlike reqwest), so a simple HEAD +/// request gives us the redirect Location directly. +async fn resolve_url(state: &AppState, url: &str) -> Result { + let response = state + .http_client + .head(url) + .await + .map_err(|err| Error::Internal(format!("Failed to resolve URL: {err}")))?; + + // Get Location header for redirect + if let Some(location) = response.headers().get("location") { + let resolved = location + .to_str() + .map_err(|_| Error::Internal("Invalid location header".into()))? + .to_owned(); + Ok(resolved) + } else { + // No redirect, return original URL + Ok(url.to_owned()) + } +} + +/// Apply URL replacements based on user preferences. +fn apply_url_replacements(url: &str, prefs: &Prefs, config: &Config) -> String { + let mut result = url.to_owned(); + + // Replace Twitter URLs + let twitter_replacement = if prefs.replace_twitter.is_empty() { + &config.server.hostname + } else { + &prefs.replace_twitter + }; + + result = result + .replace("mobile.twitter.com", twitter_replacement) + .replace("twitter.com", twitter_replacement) + .replace("x.com", twitter_replacement); + + // Replace YouTube URLs if configured + if !prefs.replace_youtube.is_empty() { + result = result + .replace("www.youtube.com", &prefs.replace_youtube) + .replace("youtube.com", &prefs.replace_youtube) + .replace("youtu.be", &prefs.replace_youtube); + } + + // Replace Reddit URLs if configured + if !prefs.replace_reddit.is_empty() { + result = result + .replace("old.reddit.com", &prefs.replace_reddit) + .replace("www.reddit.com", &prefs.replace_reddit) + .replace("reddit.com", &prefs.replace_reddit); + } + + result +} + +/// Percent-decode a URL string. +fn percent_decode(url: &str) -> String { + percent_encoding::percent_decode_str(url) + .decode_utf8() + .unwrap_or_else(|_| url.into()) + .into_owned() +} diff --git a/src/routes/rss.rs b/src/routes/rss.rs new file mode 100644 index 0000000..1c84087 --- /dev/null +++ b/src/routes/rss.rs @@ -0,0 +1,266 @@ +use axum::{ + Router, + extract::{ + Path, + Query, + State, + }, + response::Response, + routing::get, +}; +use serde::Deserialize; + +use super::helpers::{ + cache_rss, + check_rss_cache, + get_cached_user, + rss_response, +}; +use crate::{ + AppState, + cache::keys as cache_keys, + error::{ + Error, + Result, + }, + views::rss as rss_view, +}; + +#[derive(Debug, Deserialize)] +pub struct RssQuery { + pub cursor: Option, +} + +pub fn router() -> Router { + Router::new() + .route("/{username}/rss", get(user_rss)) + .route("/{username}/with_replies/rss", get(user_replies_rss)) + .route("/{username}/media/rss", get(user_media_rss)) + .route("/{username}/search/rss", get(user_search_rss)) + .route("/{username}/lists/{slug}/rss", get(list_by_slug_rss)) + .route("/search/rss", get(search_rss)) + .route("/i/lists/{id}/rss", get(list_rss)) +} + +/// Check if RSS is enabled; return 404 error if not. +fn check_rss_enabled(state: &AppState) -> Result<()> { + if !state.config.config.enable_rss { + return Err(Error::NotFound("RSS feeds are disabled".to_owned())); + } + Ok(()) +} + +/// Which user timeline variant to fetch for RSS. +enum UserRssKind { + Tweets, + Replies, + Media, +} + +/// Unified user RSS handler -- handles tweets, replies, and media variants. +async fn user_rss_handler( + state: &AppState, + username: &str, + cursor: Option<&str>, + kind: UserRssKind, +) -> Result { + let (feed_kind, cache_key_fn): (&str, fn(&str) -> String) = match kind { + UserRssKind::Tweets => ("tweets", cache_keys::rss_user), + UserRssKind::Replies => ("replies", cache_keys::rss_replies), + UserRssKind::Media => ("media", cache_keys::rss_media), + }; + + // Check RSS cache (first page only) + let rss_cache_key = if cursor.is_none() { + let key = cache_key_fn(username); + if let Some(cached) = check_rss_cache(state, &key) { + return Ok(cached); + } + Some(key) + } else { + None + }; + + let user = get_cached_user(state, username).await?; + + let timeline = match kind { + UserRssKind::Tweets | UserRssKind::Replies => { + state.api.get_user_tweets(&user.id, cursor).await? + }, + UserRssKind::Media => state.api.get_user_media(&user.id, cursor).await?, + }; + let tweets = timeline.content.into_iter().flatten().collect::>(); + let rss = rss_view::render_user_rss(&user, &tweets, &state.config, feed_kind); + + if let Some(ref key) = rss_cache_key { + cache_rss(state, key, &rss); + } + + Ok(rss_response(rss, &tweets)) +} + +async fn user_rss( + State(state): State, + Path(username): Path, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + user_rss_handler( + &state, + &username, + query.cursor.as_deref(), + UserRssKind::Tweets, + ) + .await +} + +async fn user_replies_rss( + State(state): State, + Path(username): Path, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + user_rss_handler( + &state, + &username, + query.cursor.as_deref(), + UserRssKind::Replies, + ) + .await +} + +async fn user_media_rss( + State(state): State, + Path(username): Path, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + user_rss_handler( + &state, + &username, + query.cursor.as_deref(), + UserRssKind::Media, + ) + .await +} + +async fn search_rss( + State(state): State, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + let search_query = query.query.as_deref().unwrap_or_default(); + if search_query.is_empty() { + return Err(Error::InvalidUrl("Search query is required".into())); + } + + let rss_cache_key = cache_keys::rss_search(search_query); + if let Some(cached) = check_rss_cache(&state, &rss_cache_key) { + return Ok(cached); + } + + let timeline = state.api.search(search_query, None).await?; + let tweets = timeline.content.into_iter().flatten().collect::>(); + let rss = rss_view::render_search_rss(search_query, &tweets, &state.config); + + cache_rss(&state, &rss_cache_key, &rss); + Ok(rss_response(rss, &tweets)) +} + +/// User search RSS - search tweets from a specific user. +#[derive(Debug, Deserialize)] +pub struct UserSearchRssQuery { + #[serde(rename = "q")] + pub query: Option, +} + +async fn user_search_rss( + State(state): State, + Path(username): Path, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + let search_query = query.query.as_deref().unwrap_or_default(); + if search_query.is_empty() { + return Err(Error::InvalidUrl("Search query is required".into())); + } + + let api_query = format!("from:{username} {search_query}"); + let rss_cache_key = cache_keys::rss_user_search(&username, search_query); + if let Some(cached) = check_rss_cache(&state, &rss_cache_key) { + return Ok(cached); + } + + let timeline = state.api.search(&api_query, None).await?; + let tweets = timeline.content.into_iter().flatten().collect::>(); + let rss = rss_view::render_search_rss( + &format!("from:{username} {search_query}"), + &tweets, + &state.config, + ); + + cache_rss(&state, &rss_cache_key, &rss); + Ok(rss_response(rss, &tweets)) +} + +async fn list_rss( + State(state): State, + Path(id): Path, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + let rss_cache_key = if query.cursor.is_none() { + let key = cache_keys::rss_list(&id); + if let Some(cached) = check_rss_cache(&state, &key) { + return Ok(cached); + } + Some(key) + } else { + None + }; + + let (list, timeline) = tokio::join!( + state.api.get_list(&id), + state.api.get_list_tweets(&id, query.cursor.as_deref()), + ); + let list = list?; + let timeline = timeline?; + let tweets = timeline.content.into_iter().flatten().collect::>(); + let rss = rss_view::render_list_rss(&list, &tweets, &state.config); + + if let Some(ref key) = rss_cache_key { + cache_rss(&state, key, &rss); + } + Ok(rss_response(rss, &tweets)) +} + +/// List RSS by owner username and slug. +async fn list_by_slug_rss( + State(state): State, + Path((username, slug)): Path<(String, String)>, + Query(query): Query, +) -> Result { + check_rss_enabled(&state)?; + let rss_cache_key = if query.cursor.is_none() { + let key = cache_keys::rss_list_slug(&username, &slug); + if let Some(cached) = check_rss_cache(&state, &key) { + return Ok(cached); + } + Some(key) + } else { + None + }; + + let list = state.api.get_list_by_slug(&username, &slug).await?; + let timeline = state + .api + .get_list_tweets(&list.id, query.cursor.as_deref()) + .await?; + let tweets = timeline.content.into_iter().flatten().collect::>(); + let rss = rss_view::render_list_rss(&list, &tweets, &state.config); + + if let Some(ref key) = rss_cache_key { + cache_rss(&state, key, &rss); + } + Ok(rss_response(rss, &tweets)) +} diff --git a/src/routes/search.rs b/src/routes/search.rs new file mode 100644 index 0000000..795e467 --- /dev/null +++ b/src/routes/search.rs @@ -0,0 +1,395 @@ +use axum::{ + Router, + extract::{ + Path, + Query as AxumQuery, + RawQuery, + State, + }, + http::{ + StatusCode, + header::CONTENT_TYPE, + }, + response::{ + Html, + IntoResponse as _, + Redirect, + Response, + }, + routing::get, +}; +use axum_extra::extract::CookieJar; +use serde::Deserialize; + +use crate::{ + AppState, + error::Result, + types::{ + Prefs, + Query, + QueryKind, + }, + views::{ + layout, + search as search_view, + }, +}; + +#[derive(Debug, Default, Deserialize)] +pub struct SearchQuery { + #[serde(rename = "q")] + pub query: Option, + #[serde(rename = "f")] + pub filter: Option, + pub cursor: Option, + // Filter parameters + pub from: Option, + pub since: Option, + pub until: Option, + pub min_faves: Option, + // Filter toggles (f-media=on, e-replies=on, etc.) + #[serde(rename = "f-media")] + pub f_media: Option, + #[serde(rename = "f-images")] + pub f_images: Option, + #[serde(rename = "f-videos")] + pub f_videos: Option, + #[serde(rename = "f-links")] + pub f_links: Option, + #[serde(rename = "f-news")] + pub f_news: Option, + #[serde(rename = "f-quote")] + pub f_quote: Option, + #[serde(rename = "f-verified")] + pub f_verified: Option, + #[serde(rename = "e-replies")] + pub e_replies: Option, + #[serde(rename = "e-retweets")] + pub e_retweets: Option, +} + +impl SearchQuery { + /// Convert URL parameters to `SearchFilters` for form rendering. + pub fn to_filters(&self) -> search_view::SearchFilters { + search_view::SearchFilters { + media: self.f_media.as_deref() == Some("on"), + images: self.f_images.as_deref() == Some("on"), + videos: self.f_videos.as_deref() == Some("on"), + links: self.f_links.as_deref() == Some("on"), + news: self.f_news.as_deref() == Some("on"), + quote: self.f_quote.as_deref() == Some("on"), + verified: self.f_verified.as_deref() == Some("on"), + exclude_replies: self.e_replies.as_deref() == Some("on"), + exclude_retweets: self.e_retweets.as_deref() == Some("on"), + since: self.since.clone().unwrap_or_default(), + until: self.until.clone().unwrap_or_default(), + min_faves: self.min_faves.clone().unwrap_or_default(), + } + } + + /// Convert URL parameters to `Query` struct. + fn to_query(&self) -> Query { + let raw_query = self.query.as_deref().unwrap_or(""); + + // Determine query kind from 'f' parameter + let kind = match self.filter.as_deref() { + Some("replies") => QueryKind::Replies, + Some("media") => QueryKind::Media, + Some("users") => QueryKind::Users, + _ => QueryKind::Posts, + }; + + // Parse the raw query text for inline filters + let mut query = Query::parse(raw_query, kind); + + // Add from user if specified as parameter + if let Some(ref from) = self.from { + for user in from.split(',') { + let user = user.trim(); + if !user.is_empty() && !query.from_user.contains(&user.to_owned()) { + query.from_user.push(user.to_owned()); + } + } + } + + // Add date filters from parameters + if let Some(ref since) = self.since + && query.since.is_empty() + { + query.since.clone_from(since); + } + if let Some(ref until) = self.until + && query.until.is_empty() + { + query.until.clone_from(until); + } + if let Some(ref min) = self.min_faves + && query.min_likes.is_empty() + { + query.min_likes.clone_from(min); + } + + // Add filter toggles (data-driven to avoid repetition) + let filter_toggles: &[(&Option, &str)] = &[ + (&self.f_media, "media"), + (&self.f_images, "images"), + (&self.f_videos, "videos"), + (&self.f_links, "links"), + (&self.f_news, "news"), + (&self.f_quote, "quote"), + (&self.f_verified, "verified"), + ]; + for &(param, name) in filter_toggles { + if param.as_deref() == Some("on") && !query.filters.iter().any(|filter| filter == name) { + query.filters.push((*name).to_owned()); + } + } + + // Add exclude toggles + let exclude_toggles: &[(&Option, &str)] = + &[(&self.e_replies, "replies"), (&self.e_retweets, "retweets")]; + for &(param, name) in exclude_toggles { + if param.as_deref() == Some("on") && !query.excludes.iter().any(|excl| excl == name) { + query.excludes.push((*name).to_owned()); + } + } + + query + } +} + +pub fn router() -> Router { + Router::new() + .route("/search", get(search)) + .route("/hashtag/{tag}", get(hashtag)) + .route("/opensearch", get(opensearch)) +} + +async fn search( + State(state): State, + jar: CookieJar, + AxumQuery(params): AxumQuery, +) -> Result { + // Extract prefs from cookies + let prefs = Prefs::from_cookies(&jar, &state.config); + + let raw_q = params.query.clone().unwrap_or_default(); + + // Redirect comma-separated usernames to multi-user timeline + if raw_q.contains(',') + && raw_q.split(',').all(|segment| { + let trimmed = segment.trim(); + !trimmed.is_empty() + && trimmed + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == '@') + }) + { + let cleaned = raw_q + .split(',') + .map(|segment| segment.trim().trim_start_matches('@')) + .collect::>() + .join(","); + return Ok(Redirect::to(&format!("/{cleaned}")).into_response()); + } + + // Check if this is a user search + let is_user_search = params.filter.as_deref() == Some("users"); + + // Handle empty query - show search UI without calling API + if raw_q.is_empty() && params.from.is_none() { + let filters = params.to_filters(); + if is_user_search { + let content = search_view::render_user_search_results( + &raw_q, + &[], + &state.config, + None, + None, + Some(&prefs), + ); + let markup = layout::PageLayout::new(&state.config, "Search", content) + .prefs(&prefs) + .render(); + return Ok(Html(markup.into_string()).into_response()); + } + let empty_tweets = Vec::new(); + let content = search_view::render_search_results_with_prefs( + &raw_q, + &empty_tweets, + &state.config, + None, + Some(&prefs), + Some(&filters), + None, + ); + let markup = layout::PageLayout::new(&state.config, "Search", content) + .prefs(&prefs) + .render(); + return Ok(Html(markup.into_string()).into_response()); + } + + if is_user_search { + // User search + let search_result = state + .api + .search_users(&raw_q, params.cursor.as_deref()) + .await; + + match search_result { + Ok(result) => { + let cursor = result.bottom.as_deref(); + + let newer_url = params.cursor.is_some().then(|| { + format!( + "/search?q={}&f=users", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ) + }); + let content = search_view::render_user_search_results( + &raw_q, + &result.content, + &state.config, + cursor, + newer_url.as_deref(), + Some(&prefs), + ); + let title = format!("Search ({raw_q}) | Users"); + let canonical = format!( + "https://x.com/search?q={}&src=typed_query", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ); + let referer = format!( + "/search?q={}&f=users", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ); + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .canonical(&canonical) + .referer(&referer) + .render(); + Ok(Html(markup.into_string()).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Search Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } + } else { + // Tweet search + // Parse query with filters + let query = params.to_query(); + + // Build the actual search query for Twitter API + let api_query = query.build(); + + // Execute search + let search_result = state.api.search(&api_query, params.cursor.as_deref()).await; + + match search_result { + Ok(timeline) => { + let tweets = timeline.content.into_iter().flatten().collect::>(); + let cursor = timeline.bottom.as_deref(); + + // Display the original user query, not the API query + let display_query = if query.has_filters() { + &api_query + } else { + &raw_q + }; + + let filters = params.to_filters(); + let newer_url = params.cursor.is_some().then(|| { + format!( + "/search?q={}", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ) + }); + let content = search_view::render_search_results_with_prefs( + display_query, + &tweets, + &state.config, + cursor, + Some(&prefs), + Some(&filters), + newer_url.as_deref(), + ); + let title = format!("Search ({raw_q})"); + let canonical = format!( + "https://x.com/search?f=live&q={}&src=typed_query", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ); + let rss_url = format!( + "/search/rss?f=tweets&q={}", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ); + let referer = format!( + "/search?q={}", + percent_encoding::utf8_percent_encode(&raw_q, percent_encoding::NON_ALPHANUMERIC) + ); + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .rss(&rss_url) + .canonical(&canonical) + .referer(&referer) + .render(); + Ok(Html(markup.into_string()).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Search Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } + } +} + +async fn hashtag( + State(state): State, + jar: CookieJar, + Path(tag): Path, + AxumQuery(query): AxumQuery, +) -> Result { + // Search for the hashtag + let hashtag_query = format!("#{tag}"); + + search( + State(state), + jar, + AxumQuery(SearchQuery { + query: Some(hashtag_query), + filter: query.filter, + cursor: query.cursor, + ..Default::default() + }), + ) + .await +} + +async fn opensearch(State(state): State) -> Response { + let xml = format!( + r#" + + {} + Twitter search via {} + UTF-8 + +"#, + state.config.server.title, + state.config.server.hostname, + state.config.url_prefix() + ); + + ( + [(CONTENT_TYPE, "application/opensearchdescription+xml")], + xml, + ) + .into_response() +} diff --git a/src/routes/status.rs b/src/routes/status.rs new file mode 100644 index 0000000..43a4e67 --- /dev/null +++ b/src/routes/status.rs @@ -0,0 +1,399 @@ +use axum::{ + Router, + extract::{ + Path, + Query, + State, + }, + http::StatusCode, + response::{ + Html, + IntoResponse as _, + Redirect, + Response, + }, + routing::get, +}; +use axum_extra::extract::CookieJar; +use maud::html; +use serde::Deserialize; +use tweet_view::{ + TweetRenderer, + render_reply_chains, + thread_context, +}; + +use crate::{ + AppState, + config::Config, + cache::{ + keys as cache_keys, + ttl, + }, + error::{ + Error, + Result, + }, + types::{ + Prefs, + Tweet, + }, + views::{ + embed, + layout, + timeline::render_to_top_with_focus, + tweet as tweet_view, + }, +}; + +#[derive(Debug, Deserialize)] +pub struct StatusQuery { + pub cursor: Option, + pub scroll: Option, +} + +pub fn router() -> Router { + let mut router = Router::new(); + + for prefix in ["status", "statuses"] { + router = router + .route(&format!("/{{username}}/{prefix}/{{id}}"), get(status)) + .route( + &format!("/{{username}}/{prefix}/{{id}}/photo"), + get(status_media_redirect), + ) + .route( + &format!("/{{username}}/{prefix}/{{id}}/photo/{{idx}}"), + get(status_media_redirect), + ) + .route( + &format!("/{{username}}/{prefix}/{{id}}/video"), + get(status_media_redirect), + ) + .route( + &format!("/{{username}}/{prefix}/{{id}}/video/{{idx}}"), + get(status_media_redirect), + ) + .route( + &format!("/{{username}}/{prefix}/{{id}}/history"), + get(edit_history), + ); + } + + router + .route("/{username}/thread/{id}", get(thread_redirect)) + .route("/i/status/{id}", get(status_by_id)) + .route("/i/web/status/{id}", get(status_by_id)) + .route("/i/user/{id}", get(user_by_id)) +} + +async fn thread_redirect(Path((username, id)): Path<(String, String)>) -> Response { + Redirect::to(&format!("/{username}/status/{id}")).into_response() +} + +/// Redirect legacy media URLs (photo/video/history) to the main status page. +async fn status_media_redirect(Path((username, id)): Path<(String, String)>) -> Response { + Redirect::to(&format!("/{username}/status/{id}")).into_response() +} + +async fn status( + State(state): State, + jar: CookieJar, + Path((username, id)): Path<(String, String)>, + Query(query): Query, +) -> Result { + // Extract prefs from cookies + let prefs = Prefs::from_cookies(&jar, &state.config); + + // Fetch conversation (with cache for first page) + let conv_result = if query.cursor.is_none() { + let cache_key = cache_keys::conversation(&id); + if let Some(cached) = state.cache.get(&cache_key) { + tracing::debug!("Cache hit for conversation: {id}"); + Ok(cached) + } else { + let result = state.api.get_conversation(&id, None).await; + if let Ok(ref conv) = result { + state.cache.set(&cache_key, conv, ttl::DEFAULT); + } + result + } + } else { + state + .api + .get_conversation(&id, query.cursor.as_deref()) + .await + }; + + match conv_result { + Ok(conversation) => { + let tweet = &conversation.tweet; + + // If the main tweet is unavailable (tombstone/withheld), show error + if !tweet.available && tweet.id == 0 { + let msg = if tweet.tombstone.is_empty() { + "Tweet is unavailable" + } else { + &tweet.tombstone + }; + let markup = layout::render_error(&state.config, "Tweet not found", msg); + return Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()); + } + + let is_scroll = query.scroll.as_ref().is_some_and(|val| val == "true"); + + // For AJAX scroll requests, return only the replies HTML fragment + if is_scroll { + if conversation.replies.content.is_empty() { + return Ok(StatusCode::NOT_FOUND.into_response()); + } + let content = render_reply_chains( + &conversation.replies.content, + conversation.replies.bottom.as_deref().unwrap_or_default(), + &username, + &id, + &state.config, + &prefs, + ); + return Ok(Html(content.into_string()).into_response()); + } + + let content = html! { + div class="conversation" { + div class="main-thread" { + // Before context (parent tweets) with thread line + @if !conversation.before.content.is_empty() { + div class="before-tweet thread-line" { + // "Earlier replies" link when thread doesn't start at root + @let first = &conversation.before.content[0]; + @if tweet.thread_id != first.id && (first.reply_id > 0 || !first.available) { + div class="timeline-item more-replies earlier-replies" { + a class="more-replies-text" href=(format!("/{}/status/{}#m", first.user.username, first.id)) { + "earlier replies" + } + } + } + @let before_len = conversation.before.content.len(); + @for (idx, tw) in conversation.before.content.iter().enumerate() { + @let ctx = thread_context(idx, before_len, false); + (TweetRenderer::new(tw, &state.config, false).prefs(&prefs).thread_ctx(ctx).render()) + } + } + } + + // Main tweet (highlighted, larger) + @let has_after = !conversation.after.content.is_empty(); + @let after_class = if has_after { "thread thread-line" } else { "" }; + div class="main-tweet" id="m" { + (TweetRenderer::new(tweet, &state.config, true).prefs(&prefs).extra_class(after_class).render()) + } + + // After context (thread continuation) with thread line + @if has_after { + div class="after-tweet thread-line" { + @let after_len = conversation.after.content.len(); + @let has_more = conversation.after.has_more; + @for (idx, tw) in conversation.after.content.iter().enumerate() { + @let is_last = idx == after_len - 1 && !has_more; + @let ctx = thread_context(idx, after_len, is_last); + (TweetRenderer::new(tw, &state.config, false).prefs(&prefs).thread_ctx(ctx).render()) + } + @if has_more { + @if let Some(last_after) = conversation.after.content.last() { + div class="timeline-item more-replies" { + @if last_after.available { + a class="more-replies-text" href=(format!("/{}/status/{}#m", last_after.user.username, last_after.id)) { + "more replies" + } + } @else { + a class="more-replies-text" { "more replies" } + } + } + } + } + } + } + } + + // Replies section + @if !prefs.hide_replies { + // "Load newest" for replies when viewing paginated replies + @if query.cursor.is_some() { + div class="timeline-item show-more" { + a href=(format!("/{username}/status/{id}#r")) { "Load newest" } + } + } + } + @if !prefs.hide_replies && !conversation.replies.content.is_empty() { + div class="replies" id="r" { + (render_reply_chains( + &conversation.replies.content, + conversation.replies.bottom.as_deref().unwrap_or_default(), + &username, + &id, + &state.config, + &prefs, + )) + } + } + + // Scroll to top button + (render_to_top_with_focus("#m")) + } + }; + + let markup = + embed::render_status_page(tweet, &content, &prefs, &state.config, &username, &id); + + Ok(Html(markup.into_string()).into_response()) + }, + Err(Error::TweetNotFound(msg)) => { + let markup = layout::render_error(&state.config, "Tweet not found", &msg); + Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } +} + +async fn status_by_id( + State(state): State, + Path(id): Path, + Query(_query): Query, +) -> Result { + // Validate tweet ID is numeric + let Ok(tweet_id) = id.parse() else { + return Ok(( + StatusCode::NOT_FOUND, + Html(layout::render_error(&state.config, "Not Found", "Invalid tweet ID").into_string()), + ) + .into_response()); + }; + let cache_key = cache_keys::tweet(tweet_id); + + let tweet_result = if let Some(cached) = state.cache.get::(&cache_key) { + tracing::debug!("Cache hit for tweet: {id}"); + Ok(cached) + } else { + let result = state.api.get_tweet(&id).await; + if let Ok(ref tweet) = result { + state.cache.set(&cache_key, tweet, ttl::DEFAULT); + } + result + }; + + match tweet_result { + Ok(tweet) => { + let redirect_url = format!("/{}/status/{id}", tweet.user.username); + Ok(Redirect::to(&redirect_url).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Tweet not found", &err.to_string()); + Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()) + }, + } +} + +/// Lookup user by ID and redirect to their profile. +async fn user_by_id(State(state): State, Path(id): Path) -> Result { + // Check cache first for ID -> username mapping + let cache_key = cache_keys::user_id(&id); + if let Some(username) = state.cache.get::(&cache_key) { + tracing::debug!("Cache hit for user ID: {id} -> {username}"); + return Ok(Redirect::to(&format!("/{username}")).into_response()); + } + + // Fetch user from API by ID + match state.api.get_user_by_id(&id).await { + Ok(user) => { + // Cache the ID -> username mapping (long TTL since IDs don't change) + state + .cache + .set(&cache_key, &user.username, ttl::USER_ID_MAPPING); + Ok(Redirect::to(&format!("/{}", user.username)).into_response()) + }, + Err(Error::UserNotFound(msg)) => { + let content = html! { + div class="error-page" { + h1 { "User Not Found" } + p { (msg) } + p class="user-id" { "User ID: " (id) } + } + }; + let markup = layout::PageLayout::new(&state.config, "User Not Found", content).render(); + Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } +} + +/// Edit history page handler. +async fn edit_history( + State(state): State, + jar: CookieJar, + Path((username, id)): Path<(String, String)>, +) -> Result { + // Validate tweet ID + if id.len() > 19 || !id.chars().all(|ch| ch.is_ascii_digit()) { + let markup = layout::render_error(&state.config, "Not Found", "Invalid tweet ID"); + return Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()); + } + + let prefs = Prefs::from_cookies(&jar, &state.config); + + if let Ok(edits) = state.api.get_edit_history(&id).await { + let title = format!( + "Edit History: {} (@{})", + edits.latest.text.chars().take(40).collect::(), + if edits.latest.user.username.is_empty() { + &username + } else { + &edits.latest.user.username + } + ); + + let content = html! { + div class="edit-history" { + div class="latest-edit" { + div class="edit-history-header" { "Latest post" } + (TweetRenderer::new(&edits.latest, &state.config, false) + .prefs(&prefs) + .render()) + } + @if !edits.history.is_empty() { + div class="previous-edits" { + div class="edit-history-header" { "Version history" } + @for tweet in &edits.history { + div class="tweet-edit" { + (TweetRenderer::new(tweet, &state.config, false) + .prefs(&prefs) + .render()) + } + } + } + } + } + }; + + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .render(); + Ok(Html(markup.into_string()).into_response()) + } else { + let markup = layout::render_error(&state.config, "Not Found", "Tweet edit history not found"); + Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()) + } +} diff --git a/src/routes/timeline.rs b/src/routes/timeline.rs new file mode 100644 index 0000000..b363202 --- /dev/null +++ b/src/routes/timeline.rs @@ -0,0 +1,568 @@ +use std::cmp; + +use axum::{ + Router, + extract::{ + Path, + Query, + State, + }, + http::StatusCode, + response::{ + Html, + IntoResponse as _, + Response, + }, + routing::get, +}; +use axum_extra::extract::CookieJar; +use maud::html; +use serde::Deserialize; + +use super::helpers::{ + extract_timeline, + get_cached_user, +}; +use crate::{ + AppState, + cache::{ + keys as cache_keys, + ttl, + }, + error::{ + Error, + Result, + }, + types::{ + GalleryPhoto, + Prefs, + Profile, + Timeline, + TimelineKind, + }, + utils::formatters, + views::{ + layout, + profile, + search::render_search_panel_with_action, + timeline, + }, +}; + +/// Reserved path names that cannot be usernames. +const RESERVED_PATHS: &[&str] = &[ + "pic", + "gif", + "video", + "search", + "settings", + "login", + "intent", + "i", + "about", + "explore", + "help", + "oauth", + "oauth2", + "saveprefs", + "resetprefs", + "enablehls", + "enablemp4", + "public", + "css", + "js", + "fonts", + "embed", +]; + +/// Validate username contains only valid characters. +fn is_valid_username(name: &str) -> bool { + !name.is_empty() + && name + .chars() + .all(|ch| ch.is_ascii_alphanumeric() || ch == '_' || ch == ',') + && !name.contains('.') +} + +/// Check if a username is actually a reserved path. +fn is_reserved_path(name: &str) -> bool { + let lower = name.to_lowercase(); + RESERVED_PATHS.iter().any(|&reserved| reserved == lower) +} + +/// Split comma-separated usernames and validate each. +fn parse_usernames(name: &str) -> Option> { + let names = name + .split(',') + .map(|part| part.trim().to_owned()) + .filter(|part| !part.is_empty()) + .collect::>(); + + if names.is_empty() + || names + .iter() + .any(|n| !is_valid_username(n) || is_reserved_path(n)) + { + return None; + } + + Some(names) +} + +#[derive(Debug, Deserialize)] +pub struct TimelineQuery { + pub tab: Option, + pub cursor: Option, + /// For AJAX infinite scroll requests - returns only tweet HTML. + pub scroll: Option, +} + +pub fn router() -> Router { + Router::new() + .route("/{username}", get(user_timeline)) + .route("/{username}/", get(user_timeline)) + .route("/{username}/with_replies", get(user_replies)) + .route("/{username}/media", get(user_media)) + .route("/{username}/search", get(user_search)) +} + +async fn user_timeline( + State(state): State, + jar: CookieJar, + Path(username): Path, + Query(query): Query, +) -> Result { + // Validate username + if is_reserved_path(&username) { + return Err(Error::InvalidUrl(format!( + "'{username}' is a reserved path", + ))); + } + + // Check for multi-user search (comma-separated) + if username.contains(',') { + return multi_user_timeline(state, jar, username, query).await; + } + + if !is_valid_username(&username) { + return Err(Error::InvalidUrl("Invalid username format".into())); + } + + // Get the tab type and validate + let tab = query.tab.as_deref().unwrap_or("tweets"); + if !["tweets", "with_replies", "media", "search", ""].contains(&tab) { + return Err(Error::InvalidUrl("Invalid tab parameter".into())); + } + + // Check if this is an AJAX scroll request + let is_scroll = query.scroll.as_ref().is_some_and(|val| val == "true"); + + // Extract prefs from cookies + let prefs = Prefs::from_cookies(&jar, &state.config); + + // Check cache first (only for initial page load without cursor) + let cache_key = cache_keys::profile(&username); + let profile_result = if query.cursor.is_none() { + if let Some(cached) = state.cache.get::(&cache_key) { + tracing::debug!("Cache hit for profile: {username}"); + Ok(cached) + } else { + let result = state.api.get_profile(&username, None).await; + if let Ok(ref profile_data) = result { + state.cache.set(&cache_key, profile_data, ttl::DEFAULT); + } + result + } + } else { + state + .api + .get_profile(&username, query.cursor.as_deref()) + .await + }; + + match profile_result { + Ok(profile_data) => { + // For AJAX scroll requests, return only the tweets HTML + if is_scroll { + let (tweets, cursor) = extract_timeline(profile_data.tweets); + let base_url = format!("/{}", profile_data.user.username); + let content = timeline::render_timeline_with_prefs( + &tweets, + &state.config, + cursor.as_deref(), + Some(&base_url), + &prefs, + None, + ); + return Ok(Html(content.into_string()).into_response()); + } + + let title = format!( + "{} (@{})", + profile_data.user.fullname, profile_data.user.username + ); + let desc = format!("The latest tweets from {}", profile_data.user.fullname); + let canonical = format!("https://x.com/{}", profile_data.user.username); + let rss_url = format!("/{}/rss", profile_data.user.username); + let referer = format!("/{}", profile_data.user.username); + let og_image = format!( + "{}{}", + state.config.url_prefix(), + formatters::get_pic_url( + &profile_data.user.user_pic.replace("_normal", "_400x400"), + state.config.config.base64_media, + ) + ); + let newer = query + .cursor + .is_some() + .then(|| format!("/{}", profile_data.user.username)); + let content = profile::render_profile_with_prefs( + &profile_data, + &state.config, + tab, + Some(&prefs), + newer.as_deref(), + ); + let markup = layout::PageLayout::new(&state.config, &title, content) + .description(&desc) + .prefs(&prefs) + .rss(&rss_url) + .canonical(&canonical) + .referer(&referer) + .og_image(&og_image) + .og_type("object") + .render(); + + Ok(Html(markup.into_string()).into_response()) + }, + Err(Error::UserNotFound(msg)) => { + let markup = layout::render_error(&state.config, "User not found", &msg); + Ok((StatusCode::NOT_FOUND, Html(markup.into_string())).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } +} + +/// Shared handler for user sub-tab timelines (replies, media). +async fn user_tab_handler( + state: &AppState, + prefs: &Prefs, + username: &str, + cursor: Option<&str>, + tab: TimelineKind, + include_photo_rail: bool, +) -> Result { + let user = get_cached_user(state, username).await?; + + let (cache_kind, tab_str, title_prefix) = match tab { + TimelineKind::Replies => ("replies", "with_replies", "Tweets & replies from"), + TimelineKind::Media => ("media", "media", "Media from"), + _ => unreachable!(), + }; + + // Fetch timeline (with cache for first page) + let fetch_timeline = async { + let cache_key = cache_keys::timeline(username, cache_kind); + if cursor.is_none() { + if let Some(cached) = state.cache.get::(&cache_key) { + return Ok(cached); + } + let result = match tab { + TimelineKind::Replies => state.api.get_user_tweets_and_replies(&user.id, None).await, + TimelineKind::Media => state.api.get_user_media(&user.id, None).await, + _ => unreachable!(), + }; + if let Ok(ref data) = result { + state.cache.set(&cache_key, data, ttl::DEFAULT); + } + result + } else { + match tab { + TimelineKind::Replies => { + state + .api + .get_user_tweets_and_replies(&user.id, cursor) + .await + }, + TimelineKind::Media => state.api.get_user_media(&user.id, cursor).await, + _ => unreachable!(), + } + } + }; + + let (timeline_result, photo_rail) = if include_photo_rail { + let (tl, rail) = tokio::join!(fetch_timeline, fetch_photo_rail(state, &user.id)); + (tl, rail) + } else { + (fetch_timeline.await, vec![]) + }; + + let has_request_cursor = cursor.is_some(); + + match timeline_result { + Ok(data) => { + let (tweets, next_cursor) = extract_timeline(data); + let base_url = format!("/{username}/{tab_str}"); + let newer = has_request_cursor.then_some(base_url.as_str()); + + let timeline_content = timeline::render_timeline_with_prefs( + &tweets, + &state.config, + next_cursor.as_deref(), + Some(&base_url), + prefs, + newer, + ); + let content = profile::render_profile_page( + &user, + &photo_rail, + &state.config, + prefs, + tab, + &timeline_content, + ); + + let title = format!("{title_prefix} @{}", user.username); + let canonical = format!("https://x.com/{username}/{tab_str}"); + let rss_url = format!("/{username}/{tab_str}/rss"); + let referer = format!("/{username}/{tab_str}"); + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(prefs) + .rss(&rss_url) + .canonical(&canonical) + .referer(&referer) + .render(); + Ok(Html(markup.into_string()).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } +} + +async fn user_replies( + State(state): State, + jar: CookieJar, + Path(username): Path, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + user_tab_handler( + &state, + &prefs, + &username, + query.cursor.as_deref(), + TimelineKind::Replies, + true, + ) + .await +} + +async fn user_media( + State(state): State, + jar: CookieJar, + Path(username): Path, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + user_tab_handler( + &state, + &prefs, + &username, + query.cursor.as_deref(), + TimelineKind::Media, + false, + ) + .await +} + +#[derive(Debug, serde::Deserialize)] +pub struct UserSearchQuery { + #[serde(rename = "q")] + pub query: Option, + pub cursor: Option, +} + +async fn user_search( + State(state): State, + jar: CookieJar, + Path(username): Path, + Query(query): Query, +) -> Result { + let prefs = Prefs::from_cookies(&jar, &state.config); + let user = get_cached_user(&state, &username).await?; + + let search_query = query.query.as_deref().unwrap_or(""); + let search_action = format!("/{username}/search"); + + // When search query is empty, default to showing the user's recent tweets + let api_query = if search_query.is_empty() { + format!("from:{username} include:nativeretweets") + } else { + format!("from:{username} {search_query}") + }; + let (photo_rail, search_result) = tokio::join!( + fetch_photo_rail(&state, &user.id), + state.api.search(&api_query, query.cursor.as_deref()), + ); + + match search_result { + Ok(results) => { + let (tweets, cursor) = extract_timeline(results); + let base_url = if search_query.is_empty() { + format!("/{username}/search") + } else { + format!( + "/{}/search?q={}", + username, + percent_encoding::utf8_percent_encode( + search_query, + percent_encoding::NON_ALPHANUMERIC, + ) + ) + }; + + let newer = query.cursor.is_some().then_some(base_url.as_str()); + let timeline_content = html! { + div class="timeline-header" { + (render_search_panel_with_action(search_query, None, &search_action)) + } + (timeline::render_timeline_with_prefs(&tweets, &state.config, cursor.as_deref(), Some(&base_url), &prefs, newer)) + }; + let content = profile::render_profile_page( + &user, + &photo_rail, + &state.config, + &prefs, + TimelineKind::Search, + &timeline_content, + ); + + let title = if search_query.is_empty() { + format!("Search @{username}") + } else { + format!("Search @{username}: {search_query}") + }; + let canonical = format!("https://x.com/{username}"); + let rss_url = format!("/{username}/search/rss?f=tweets"); + let referer = format!("/{username}/search"); + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .rss(&rss_url) + .canonical(&canonical) + .referer(&referer) + .render(); + Ok(Html(markup.into_string()).into_response()) + }, + Err(err) => { + let markup = layout::render_error(&state.config, "Search Error", &err.to_string()); + Ok(( + StatusCode::INTERNAL_SERVER_ERROR, + Html(markup.into_string()), + ) + .into_response()) + }, + } +} + +/// Fetch photo rail for a user, returning empty vec on error. +async fn fetch_photo_rail(state: &AppState, user_id: &str) -> Vec { + state.api.get_photo_rail(user_id).await.unwrap_or_default() +} + +/// Handle multi-user timeline (comma-separated usernames). +async fn multi_user_timeline( + state: AppState, + jar: CookieJar, + usernames_str: String, + query: TimelineQuery, +) -> Result { + const PAGE_SIZE: usize = 20; + + let prefs = Prefs::from_cookies(&jar, &state.config); + + let Some(usernames) = parse_usernames(&usernames_str) else { + return Err(Error::InvalidUrl( + "Invalid username format in multi-user search".into(), + )); + }; + + if usernames.len() > 10 { + return Err(Error::InvalidUrl( + "Maximum 10 users allowed in multi-user search".into(), + )); + } + + let cursor_id = query + .cursor + .as_ref() + .and_then(|val| val.parse::().ok()); + + let mut all_tweets = Vec::new(); + let handles: Vec<_> = usernames + .iter() + .map(|username| { + let state = state.clone(); + let username = username.clone(); + tokio::spawn(async move { + let user = get_cached_user(&state, &username).await.ok()?; + let timeline = state.api.get_user_tweets(&user.id, None).await.ok()?; + Some(timeline.content.into_iter().flatten().collect::>()) + }) + }) + .collect(); + + for handle in handles { + if let Ok(Some(tweets)) = handle.await { + all_tweets.extend(tweets); + } + } + + all_tweets.sort_by_key(|tweet| cmp::Reverse(tweet.time)); + + if let Some(cursor) = cursor_id { + all_tweets.retain(|tweet| tweet.id < cursor); + } + + let has_more = all_tweets.len() > PAGE_SIZE; + all_tweets.truncate(PAGE_SIZE); + + let next_cursor = if has_more { + all_tweets.last().map(|tweet| tweet.id.to_string()) + } else { + None + }; + + let title = format!("Tweets from {}", usernames.join(", ")); + let base_url = format!("/{usernames_str}"); + + let newer = query.cursor.is_some().then_some(base_url.as_str()); + let groups = all_tweets + .into_iter() + .map(|tweet| vec![tweet]) + .collect::>>(); + let content = html! { + div class="multi-user-timeline" { + h2 { "Combined timeline: " (usernames.iter().map(|name| format!("@{name}")).collect::>().join(", ")) } + (timeline::render_timeline_with_prefs(&groups, &state.config, next_cursor.as_deref(), Some(&base_url), &prefs, newer)) + } + }; + + let markup = layout::PageLayout::new(&state.config, &title, content) + .prefs(&prefs) + .render(); + Ok(Html(markup.into_string()).into_response()) +} diff --git a/src/routes/unsupported.rs b/src/routes/unsupported.rs new file mode 100644 index 0000000..184a770 --- /dev/null +++ b/src/routes/unsupported.rs @@ -0,0 +1,78 @@ +use axum::{ + Router, + extract::State, + response::{ + Html, + IntoResponse as _, + Response, + }, + routing::get, +}; +use maud::html; + +use crate::{ + AppState, + views::layout, +}; + +pub fn router() -> Router { + Router::new() + .route("/login", get(unsupported_login)) + .route("/login/{*path}", get(unsupported_login)) + .route("/about/feature", get(unsupported_feature)) +} + +/// Catch-all router for `/i/*` routes - must be merged AFTER specific `/i/*` +/// routes. +pub fn i_catchall_router() -> Router { + Router::new().route("/i/{*path}", get(i_unsupported)) +} + +/// Catch-all for unsupported `/i/*` routes. +async fn i_unsupported(State(state): State) -> Response { + render_unsupported_feature(&state, "Feature").into_response() +} + +async fn unsupported_login(State(state): State) -> Response { + let content = html! { + div class="overlay-panel" { + h1 { "Login Not Supported" } + p { "teapot does not support logging in to Twitter." } + p { "teapot is a privacy-focused frontend that allows you to browse Twitter without an account." } + p { + a href="/" { "Go to Homepage" } + } + } + }; + + let markup = layout::PageLayout::new(&state.config, "Login Not Supported", content).render(); + Html(markup.into_string()).into_response() +} + +async fn unsupported_feature(State(state): State) -> Response { + render_unsupported_feature(&state, "Feature").into_response() +} + +/// Render the unsupported feature page. +pub fn render_unsupported_feature(state: &AppState, feature_name: &str) -> Response { + let content = html! { + div class="overlay-panel" { + h1 { "Unsupported feature" } + p { + "teapot doesn't support this feature yet, but it might in the future. " + "You can check for an issue and open one if needed here: " + a href="https://github.com/amaanq/teapot/issues" target="_blank" { + "https://github.com/amaanq/teapot/issues" + } + } + p { + "To find out more about the teapot project, see the " + a href="/about" { "About page" } + } + } + }; + + let title = format!("{feature_name} Not Available"); + let markup = layout::PageLayout::new(&state.config, &title, content).render(); + Html(markup.into_string()).into_response() +} diff --git a/src/types/mod.rs b/src/types/mod.rs new file mode 100644 index 0000000..02782a2 --- /dev/null +++ b/src/types/mod.rs @@ -0,0 +1,13 @@ +mod prefs; +mod query; +mod session; +mod timeline; +mod tweet; +mod user; + +pub use prefs::*; +pub use query::*; +pub use session::*; +pub use timeline::*; +pub use tweet::*; +pub use user::*; diff --git a/src/types/prefs.rs b/src/types/prefs.rs new file mode 100644 index 0000000..d751ab4 --- /dev/null +++ b/src/types/prefs.rs @@ -0,0 +1,120 @@ +use axum_extra::extract::cookie::CookieJar; +use serde::{ + Deserialize, + Serialize, +}; + +use crate::config::Config; + +/// User preferences stored in cookies. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[expect( + clippy::struct_excessive_bools, + reason = "each bool is an independent user preference" +)] +pub struct Prefs { + pub theme: String, + pub infinite_scroll: bool, + pub sticky_profile: bool, + pub bidi_support: bool, + pub hls_playback: bool, + pub mp4_playback: bool, + pub proxy_videos: bool, + pub autoplay_gifs: bool, + pub mute_videos: bool, + pub hide_tweet_stats: bool, + pub hide_banner: bool, + pub hide_pins: bool, + pub hide_replies: bool, + pub hide_community_notes: bool, + pub square_avatars: bool, + pub sticky_nav: bool, + pub replace_twitter: String, + pub replace_youtube: String, + pub replace_reddit: String, +} + +impl Prefs { + /// Canonical defaults -- the ONE place all defaults are defined. + pub fn with_defaults(config: &Config) -> Self { + Self { + theme: config.preferences.theme.clone(), + infinite_scroll: config.preferences.infinite_scroll, + sticky_profile: true, + bidi_support: false, + hls_playback: config.preferences.hls_playback, + mp4_playback: true, + proxy_videos: config.preferences.proxy_videos, + autoplay_gifs: true, + mute_videos: false, + hide_tweet_stats: false, + hide_banner: false, + hide_pins: false, + hide_replies: false, + hide_community_notes: false, + square_avatars: false, + sticky_nav: true, + replace_twitter: config.preferences.replace_twitter.clone(), + replace_youtube: config.preferences.replace_youtube.clone(), + replace_reddit: config.preferences.replace_reddit.clone(), + } + } + + /// Extract preferences from cookies, falling back to config defaults. + pub fn from_cookies(jar: &CookieJar, config: &Config) -> Self { + let defaults = Self::with_defaults(config); + let bool_pref = |name: &str, default: bool| { + jar.get(name) + .map_or(default, |cookie| cookie.value() == "on") + }; + let str_pref = |name: &str, default: String| { + jar.get(name) + .map_or(default, |cookie| cookie.value().to_owned()) + }; + + Self { + theme: str_pref("theme", defaults.theme), + infinite_scroll: bool_pref("infiniteScroll", defaults.infinite_scroll), + sticky_profile: bool_pref("stickyProfile", defaults.sticky_profile), + bidi_support: bool_pref("bidiSupport", defaults.bidi_support), + hls_playback: bool_pref("hlsPlayback", defaults.hls_playback), + mp4_playback: bool_pref("mp4Playback", defaults.mp4_playback), + proxy_videos: bool_pref("proxyVideos", defaults.proxy_videos), + autoplay_gifs: bool_pref("autoplayGifs", defaults.autoplay_gifs), + mute_videos: bool_pref("muteVideos", defaults.mute_videos), + hide_tweet_stats: bool_pref("hideTweetStats", defaults.hide_tweet_stats), + hide_banner: bool_pref("hideBanner", defaults.hide_banner), + hide_pins: bool_pref("hidePins", defaults.hide_pins), + hide_replies: bool_pref("hideReplies", defaults.hide_replies), + hide_community_notes: bool_pref("hideCommunityNotes", defaults.hide_community_notes), + square_avatars: bool_pref("squareAvatars", defaults.square_avatars), + sticky_nav: bool_pref("stickyNav", defaults.sticky_nav), + replace_twitter: str_pref("replaceTwitter", defaults.replace_twitter), + replace_youtube: str_pref("replaceYouTube", defaults.replace_youtube), + replace_reddit: str_pref("replaceReddit", defaults.replace_reddit), + } + } + + /// Cookie names for all preference fields. + pub const COOKIE_NAMES: &[&str] = &[ + "theme", + "infiniteScroll", + "stickyProfile", + "bidiSupport", + "hlsPlayback", + "mp4Playback", + "proxyVideos", + "autoplayGifs", + "muteVideos", + "hideTweetStats", + "hideBanner", + "hidePins", + "hideReplies", + "hideCommunityNotes", + "squareAvatars", + "stickyNav", + "replaceTwitter", + "replaceYouTube", + "replaceReddit", + ]; +} diff --git a/src/types/query.rs b/src/types/query.rs new file mode 100644 index 0000000..05e93c7 --- /dev/null +++ b/src/types/query.rs @@ -0,0 +1,283 @@ +use std::fmt::Write as _; + +use serde::{ + Deserialize, + Serialize, +}; + +/// Valid Twitter search filters. +pub const VALID_FILTERS: &[&str] = &[ + "media", + "images", + "twimg", + "videos", + "native_video", + "consumer_video", + "pro_video", + "spaces", + "links", + "news", + "quote", + "mentions", + "replies", + "retweets", + "nativeretweets", + "verified", + "blue_verified", + "safe", +]; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum QueryKind { + #[default] + Posts, + Replies, + Media, + Users, + Tweets, + UserList, +} + +/// Search query with filters. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +pub struct Query { + #[serde(default)] + pub kind: QueryKind, + #[serde(default)] + pub text: String, + #[serde(default)] + pub filters: Vec, + #[serde(default)] + pub includes: Vec, + #[serde(default)] + pub excludes: Vec, + #[serde(default)] + pub from_user: Vec, + #[serde(default)] + pub since: String, + #[serde(default)] + pub until: String, + #[serde(default)] + pub min_likes: String, + #[serde(default)] + pub sep: String, +} + +impl Query { + /// Build the search query string for Twitter API. + pub fn build(&self) -> String { + let mut param = String::new(); + + // Add from:user with OR (from users come first) + for (idx, user) in self.from_user.iter().enumerate() { + let _ = write!(param, "from:{user} "); + if idx < self.from_user.len() - 1 { + param.push_str("OR "); + } + } + + // Add self_threads filter for from-user queries with posts/media kind + if !self.from_user.is_empty() && matches!(self.kind, QueryKind::Posts | QueryKind::Media) { + param.push_str("filter:self_threads OR -filter:replies "); + } + + // Add include:nativeretweets unless explicitly excluded + if !self.excludes.contains(&"nativeretweets".to_owned()) { + param.push_str("include:nativeretweets "); + } + + // Build filters + let mut filters = Vec::new(); + for filter in &self.filters { + filters.push(format!("filter:{filter}")); + } + for exclude in &self.excludes { + if exclude == "nativeretweets" { + continue; + } + filters.push(format!("-filter:{exclude}")); + } + for inc in &self.includes { + filters.push(format!("include:{inc}")); + } + + let sep = if self.sep.is_empty() { " " } else { &self.sep }; + let mut result = format!("{param}{}", filters.join(&format!(" {sep} "))) + .trim() + .to_owned(); + + if !self.since.is_empty() { + let _ = write!(result, " since:{}", self.since); + } + if !self.until.is_empty() { + let _ = write!(result, " until:{}", self.until); + } + if !self.min_likes.is_empty() { + let _ = write!(result, " min_faves:{}", self.min_likes); + } + + // Add text last + if !self.text.is_empty() { + if result.is_empty() { + result.clone_from(&self.text); + } else { + let _ = write!(result, " {}", self.text); + } + } + + result + } + + /// Parse a query string into a [`Query`] struct. + /// Extracts filters like "from:user", "filter:media", "since:2024-01-01" + /// etc. + pub fn parse(text: &str, kind: QueryKind) -> Self { + let mut query = Self { + kind, + ..Default::default() + }; + + let mut remaining_text = Vec::new(); + + for part in text.split_whitespace() { + if let Some(value) = part.strip_prefix("from:") { + // Handle comma-separated users + for user in value.split(',') { + let user = user.trim(); + if !user.is_empty() { + query.from_user.push(user.to_owned()); + } + } + } else if let Some(value) = part.strip_prefix("filter:") { + if VALID_FILTERS.contains(&value) { + query.filters.push(value.to_owned()); + } else { + // Unknown filter, keep as text + remaining_text.push(part.to_owned()); + } + } else if let Some(value) = part.strip_prefix("-filter:") { + if VALID_FILTERS.contains(&value) { + query.excludes.push(value.to_owned()); + } else { + remaining_text.push(part.to_owned()); + } + } else if let Some(value) = part.strip_prefix("include:") { + query.includes.push(value.to_owned()); + } else if let Some(value) = part.strip_prefix("exclude:") { + query.excludes.push(value.to_owned()); + } else if let Some(value) = part.strip_prefix("since:") { + // Validate date format (basic check) + if is_valid_date(value) { + value.clone_into(&mut query.since); + } else { + remaining_text.push(part.to_owned()); + } + } else if let Some(value) = part.strip_prefix("until:") { + if is_valid_date(value) { + value.clone_into(&mut query.until); + } else { + remaining_text.push(part.to_owned()); + } + } else if let Some(value) = part.strip_prefix("min_faves:") { + if value.chars().all(|ch| ch.is_ascii_digit()) { + value.clone_into(&mut query.min_likes); + } else { + remaining_text.push(part.to_owned()); + } + } else if let Some(value) = part.strip_prefix("min_retweets:") { + // Not directly supported, but keep for completeness + if value.chars().all(|ch| ch.is_ascii_digit()) { + remaining_text.push(part.to_owned()); + } + } else if part.starts_with('-') && part.len() > 1 { + // Negative word filter like "-spam" + query.excludes.push(part[1..].to_string()); + } else { + remaining_text.push(part.to_owned()); + } + } + + query.text = remaining_text.join(" "); + query + } + + /// Generate URL parameters for preserving search state. + pub fn to_url_params(&self) -> String { + let mut params = Vec::new(); + + if !self.text.is_empty() { + params.push(format!("q={}", urlencoding::encode(&self.text))); + } + + match self.kind { + QueryKind::Posts => {}, + QueryKind::Replies => params.push("f=replies".to_owned()), + QueryKind::Media => params.push("f=media".to_owned()), + QueryKind::Users => params.push("f=users".to_owned()), + QueryKind::Tweets => params.push("f=tweets".to_owned()), + QueryKind::UserList => params.push("f=userlist".to_owned()), + } + + for filter in &self.filters { + params.push(format!("f-{filter}=on")); + } + + for exclude in &self.excludes { + params.push(format!("e-{exclude}=on")); + } + + for user in &self.from_user { + params.push(format!("from={}", urlencoding::encode(user))); + } + + if !self.since.is_empty() { + params.push(format!("since={}", self.since)); + } + + if !self.until.is_empty() { + params.push(format!("until={}", self.until)); + } + + if !self.min_likes.is_empty() { + params.push(format!("min_faves={}", self.min_likes)); + } + + params.join("&") + } + + /// Check if query has any active filters. + pub const fn has_filters(&self) -> bool { + !self.filters.is_empty() + || !self.excludes.is_empty() + || !self.from_user.is_empty() + || !self.since.is_empty() + || !self.until.is_empty() + || !self.min_likes.is_empty() + } +} + +/// Basic date validation (YYYY-MM-DD format). +fn is_valid_date(date_str: &str) -> bool { + if date_str.len() != 10 { + return false; + } + + let parts = date_str.split('-').collect::>(); + if parts.len() != 3 { + return false; + } + + parts[0].len() == 4 + && parts[1].len() == 2 + && parts[2].len() == 2 + && parts + .iter() + .all(|part| part.chars().all(|ch| ch.is_ascii_digit())) +} + +mod urlencoding { + pub fn encode(input: &str) -> String { + percent_encoding::utf8_percent_encode(input, percent_encoding::NON_ALPHANUMERIC).to_string() + } +} diff --git a/src/types/session.rs b/src/types/session.rs new file mode 100644 index 0000000..d3795d3 --- /dev/null +++ b/src/types/session.rs @@ -0,0 +1,111 @@ +use std::collections::HashMap; + +use serde::{ + Deserialize, + Serialize, +}; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct RateLimit { + pub limit: i32, + pub remaining: i32, + pub reset: i64, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SessionKind { + #[default] + OAuth, + Cookie, +} + +/// Immutable session credentials, loaded once and `Arc`-shared. +#[derive(Debug, Clone, Serialize)] +pub struct SessionCredentials { + pub id: i64, + pub username: String, + pub kind: SessionKind, + pub oauth_token: String, + pub oauth_secret: String, + pub auth_token: String, + pub ct0: String, +} + +/// Mutable rate-limit state, stored separately in the pool. +#[derive(Debug, Clone, Default, Serialize)] +pub struct SessionLimits { + pub pending: i32, + pub limited: bool, + pub limited_at: i64, + pub apis: HashMap, +} + +impl SessionLimits { + /// Check if rate limited for a specific API. + pub fn is_limited(&self, api: &str) -> bool { + if self.limited { + return true; + } + if let Some(limit) = self.apis.get(api) { + let now = time::OffsetDateTime::now_utc().unix_timestamp(); + if limit.remaining == 0 && limit.reset > now { + return true; + } + } + false + } + + pub fn update_limit(&mut self, api: &str, limit: i32, remaining: i32, reset: i64) { + self.apis.insert(api.to_owned(), RateLimit { + limit, + remaining, + reset, + }); + } +} + +/// Authentication session for Twitter API (used for JSONL deserialization). +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Session { + pub id: i64, + pub username: String, + pub pending: i32, + pub limited: bool, + pub limited_at: i64, + pub apis: HashMap, + pub kind: SessionKind, + + // OAuth credentials + pub oauth_token: String, + pub oauth_secret: String, + + // Cookie credentials + pub auth_token: String, + pub ct0: String, +} + +impl Session { + /// Split into immutable credentials and mutable rate-limit state. + pub fn into_credentials_and_limits(self) -> (SessionCredentials, SessionLimits) { + ( + SessionCredentials { + id: self.id, + username: self.username, + kind: self.kind, + oauth_token: self.oauth_token, + oauth_secret: self.oauth_secret, + auth_token: self.auth_token, + ct0: self.ct0, + }, + SessionLimits { + pending: self.pending, + limited: self.limited, + limited_at: self.limited_at, + apis: self.apis, + }, + ) + } +} diff --git a/src/types/timeline.rs b/src/types/timeline.rs new file mode 100644 index 0000000..fab8b13 --- /dev/null +++ b/src/types/timeline.rs @@ -0,0 +1,81 @@ +use super::{ + PhotoRail, + Query, + Tweet, + User, +}; + +pub type Tweets = Vec; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)] +pub enum TimelineKind { + #[default] + Tweets, + Replies, + Media, + Search, +} + +/// Generic paginated result. +#[derive(Debug, Clone, Default)] +pub struct PaginatedResult { + pub content: Vec, + pub top: Option, + pub bottom: Option, + pub beginning: bool, + pub query: Query, +} + +/// A chain of tweets (for conversation threads). +#[derive(Debug, Clone, Default)] +pub struct Chain { + pub content: Tweets, + pub has_more: bool, + pub cursor: Option, +} + +impl Chain { + pub fn contains(&self, tweet: &Tweet) -> bool { + self.content.iter().any(|entry| entry.id == tweet.id) + } +} + +/// A conversation view (tweet + context + replies). +#[derive(Debug, Clone, Default)] +pub struct Conversation { + pub tweet: Tweet, + pub before: Chain, + pub after: Chain, + pub replies: PaginatedResult, +} + +/// User timeline. +pub type Timeline = PaginatedResult; + +/// User profile with tweets and photo rail. +#[derive(Debug, Clone, Default)] +pub struct Profile { + pub user: User, + pub photo_rail: PhotoRail, + pub pinned: Option, + pub tweets: Timeline, +} + +/// Edit history for a tweet. +#[derive(Debug, Clone, Default)] +pub struct EditHistory { + pub latest: Tweet, + pub history: Tweets, +} + +/// Twitter list. +#[derive(Debug, Clone, Default)] +pub struct List { + pub id: String, + pub name: String, + pub user_id: String, + pub username: String, + pub description: String, + pub members: i32, + pub banner: String, +} diff --git a/src/types/tweet.rs b/src/types/tweet.rs new file mode 100644 index 0000000..332d68c --- /dev/null +++ b/src/types/tweet.rs @@ -0,0 +1,313 @@ +use serde::{ + Deserialize, + Serialize, +}; + +use super::User; +use crate::api::schema::CommunityNote; + +/// Entity type for text expansion. +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum EntityKind { + #[default] + Url, + Mention, + Hashtag, + Symbol, +} + +/// Text entity with position and content info. +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Entity { + /// Start and end byte indices in the text. + pub indices: (usize, usize), + /// Type of entity. + pub kind: EntityKind, + /// Target URL for the link. + pub url: String, + /// Display text for the link. + pub display: String, +} + +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)] +pub enum VideoType { + #[serde(rename = "application/x-mpegURL")] + M3u8, + #[default] + #[serde(rename = "video/mp4")] + Mp4, + #[serde(rename = "video/vmap")] + Vmap, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct VideoVariant { + pub content_type: VideoType, + pub url: String, + pub bitrate: i32, + pub resolution: i32, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Video { + pub duration_ms: i32, + pub url: String, + pub thumb: String, + pub available: bool, + pub reason: String, + pub title: String, + pub description: String, + pub playback_type: VideoType, + pub variants: Vec, +} + +impl Video { + /// Get the best MP4 URL for embedding. + pub fn best_mp4_url(&self) -> Option<&str> { + self + .variants + .iter() + .filter(|variant| matches!(variant.content_type, VideoType::Mp4)) + .max_by_key(|variant| variant.resolution) + .map(|variant| variant.url.as_str()) + } + + /// Get the best HLS/m3u8 URL for streaming. + pub fn best_hls_url(&self) -> Option<&str> { + self + .variants + .iter() + .filter(|variant| matches!(variant.content_type, VideoType::M3u8 | VideoType::Vmap)) + .max_by_key(|variant| variant.resolution) + .map(|variant| variant.url.as_str()) + } + + /// Get the best video dimensions. + #[expect( + clippy::cast_possible_truncation, + reason = "aspect ratio calculation yields reasonable pixel values" + )] + #[expect( + clippy::cast_precision_loss, + reason = "video resolution i32 fits in f32 mantissa" + )] + pub fn best_dimensions(&self) -> (i32, i32) { + self + .variants + .iter() + .filter(|variant| matches!(variant.content_type, VideoType::Mp4)) + .max_by_key(|variant| variant.resolution) + .map_or((1280, 720), |variant| { + // Try to parse width from URL (format: /vid/WIDTHxHEIGHT/) + // Default to 16:9 aspect ratio based on resolution + let height = variant.resolution; + let width = (height as f32 * 16.0 / 9.0) as i32; + (width, height) + }) + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Gif { + pub url: String, + pub thumb: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Photo { + pub url: String, + pub alt_text: String, +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct GalleryPhoto { + pub url: String, + pub tweet_id: String, + pub color: String, +} + +pub type PhotoRail = Vec; + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Poll { + pub options: Vec, + pub values: Vec, + pub votes: i64, + pub leader: i64, + #[serde(with = "time::serde::timestamp::option")] + pub end_time: Option, + pub image: Option, +} + +impl Poll { + /// Compute the poll status display string from `end_time`. + /// Returns "Final results" for ended polls, or "N days/hours/minutes" for + /// active ones. + pub fn status_text(&self) -> String { + let Some(end_time) = self.end_time else { + return "Final results".to_owned(); + }; + let now = time::OffsetDateTime::now_utc(); + if end_time <= now { + return "Final results".to_owned(); + } + let diff = end_time - now; + if diff.whole_days() > 0 { + format!("{} days", diff.whole_days()) + } else if diff.whole_hours() > 0 { + format!("{} hours", diff.whole_hours()) + } else { + format!("{} minutes", diff.whole_minutes()) + } + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum CardKind { + Amplify, + App, + #[serde(rename = "appplayer")] + AppPlayer, + Player, + Summary, + #[serde(rename = "summary_large_image")] + SummaryLarge, + #[serde(rename = "promo_website")] + PromoWebsite, + #[serde(rename = "promo_video_website")] + PromoVideo, + #[serde(rename = "promo_video_convo")] + PromoVideoConvo, + #[serde(rename = "promo_image_convo")] + PromoImageConvo, + #[serde(rename = "promo_image_app")] + PromoImageApp, + #[serde(rename = "direct_store_link_app")] + StoreLink, + #[serde(rename = "live_event")] + LiveEvent, + Broadcast, + #[serde(rename = "periscope_broadcast")] + Periscope, + #[serde(rename = "unified_card")] + Unified, + Moment, + #[serde(rename = "message_me")] + MessageMe, + #[serde(rename = "video_direct_message")] + VideoDirectMessage, + #[serde(rename = "image_direct_message")] + ImageDirectMessage, + Audiospace, + #[serde(rename = "newsletter_publication")] + NewsletterPublication, + #[serde(rename = "job_details")] + JobDetails, + Hidden, + #[default] + Unknown, +} + +impl CardKind { + /// Parse a card kind from the Twitter API card name suffix. + pub fn from_name(name: &str) -> Self { + match name { + "summary" => Self::Summary, + "summary_large_image" => Self::SummaryLarge, + "player" => Self::Player, + "amplify" => Self::Amplify, + "app" | "promo_image_app" => Self::App, + "promo_image_convo" => Self::PromoImageConvo, + "promo_video_website" => Self::PromoVideo, + "promo_video_convo" => Self::PromoVideoConvo, + "promo_website" => Self::PromoWebsite, + "appplayer" => Self::AppPlayer, + "direct_store_link_app" => Self::StoreLink, + "live_event" => Self::LiveEvent, + "broadcast" => Self::Broadcast, + "periscope_broadcast" => Self::Periscope, + "audiospace" => Self::Audiospace, + "moment" => Self::Moment, + "message_me" => Self::MessageMe, + "video_direct_message" => Self::VideoDirectMessage, + "image_direct_message" => Self::ImageDirectMessage, + "newsletter_publication" => Self::NewsletterPublication, + "unified_card" => Self::Unified, + _ => Self::Unknown, + } + } +} + +#[derive(Debug, Clone, Default, Serialize, Deserialize)] +#[serde(default)] +pub struct Card { + pub kind: CardKind, + pub url: String, + pub title: String, + pub dest: String, + pub text: String, + pub image: String, + pub video: Option