diff --git a/.github/workflows/build-docker.yml b/.github/workflows/build-docker.yml index 20a15a0..30a209b 100644 --- a/.github/workflows/build-docker.yml +++ b/.github/workflows/build-docker.yml @@ -1,4 +1,4 @@ -name: Docker +name: CI/CD on: push: @@ -7,105 +7,32 @@ on: branches: - master -concurrency: - group: docker-publish-${{ github.ref }} - cancel-in-progress: true - -env: - IMAGE: zedeus/nitter - jobs: - tests: - uses: ./.github/workflows/run-tests.yml - secrets: inherit - - build: - needs: [tests] - strategy: - fail-fast: false - matrix: - include: - - runner: ubuntu-24.04 - platform: linux/amd64 - - runner: ubuntu-24.04-arm - platform: linux/arm64 - runs-on: ${{ matrix.runner }} + build-docker: + runs-on: ubuntu-latest steps: - - name: Prepare platform name - run: echo "PLATFORM_PAIR=${platform//\//-}" >> "$GITHUB_ENV" - env: - platform: ${{ matrix.platform }} - - - uses: actions/checkout@v6 - + - uses: actions/checkout@v2 + with: + fetch-depth: 0 + - name: Set up QEMU + uses: docker/setup-qemu-action@v1 + with: + platforms: all - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + id: buildx + uses: docker/setup-buildx-action@v1 with: version: latest - - name: Login to DockerHub - uses: docker/login-action@v3 + uses: docker/login-action@v1 with: username: ${{ secrets.DOCKER_USERNAME }} password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Build and push by digest - id: build - uses: docker/build-push-action@v6 + - name: Build and push + uses: docker/build-push-action@v2 with: context: . file: ./Dockerfile - platforms: ${{ matrix.platform }} - outputs: type=image,name=${{ env.IMAGE }},push-by-digest=true,name-canonical=true,push=true - provenance: false - sbom: false - - - name: Export digest - run: | - mkdir -p "${{ runner.temp }}/digests" - digest="${{ steps.build.outputs.digest }}" - touch "${{ runner.temp }}/digests/${digest#sha256:}" - - - name: Upload digest - uses: actions/upload-artifact@v4 - with: - name: digests-${{ env.PLATFORM_PAIR }} - path: ${{ runner.temp }}/digests/* - if-no-files-found: error - retention-days: 1 - - # Combine the per-arch digests into one multi-arch manifest so that - # `docker pull zedeus/nitter:latest` serves the right image on any CPU. - merge: - needs: [build] - runs-on: ubuntu-24.04 - steps: - - name: Download digests - uses: actions/download-artifact@v4 - with: - path: ${{ runner.temp }}/digests - pattern: digests-* - merge-multiple: true - - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 - with: - version: latest - - - name: Login to DockerHub - uses: docker/login-action@v3 - with: - username: ${{ secrets.DOCKER_USERNAME }} - password: ${{ secrets.DOCKER_PASSWORD }} - - - name: Create manifest list and push - working-directory: ${{ runner.temp }}/digests - run: | - docker buildx imagetools create \ - -t ${{ env.IMAGE }}:latest \ - -t ${{ env.IMAGE }}:latest-arm64 \ - -t ${{ env.IMAGE }}:${{ github.sha }} \ - $(printf '${{ env.IMAGE }}@sha256:%s ' *) - - - name: Inspect image - run: docker buildx imagetools inspect ${{ env.IMAGE }}:${{ github.sha }} + platforms: linux/amd64 + push: true + tags: zedeus/nitter:latest,zedeus/nitter:${{ github.sha }} diff --git a/.github/workflows/run-tests.yml b/.github/workflows/run-tests.yml deleted file mode 100644 index 33dcba5..0000000 --- a/.github/workflows/run-tests.yml +++ /dev/null @@ -1,152 +0,0 @@ -name: Tests - -on: - push: - paths-ignore: - - "*.md" - branches-ignore: - - master - workflow_call: - -# Ensure that multiple runs on the same branch do not overlap. -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -defaults: - run: - shell: bash - -jobs: - build-test: - name: Build and test - runs-on: ubuntu-24.04 - strategy: - matrix: - nim: ["2.0.x", "2.2.x", "devel"] - steps: - - name: Checkout Code - uses: actions/checkout@v6 - - - name: Cache Nimble Dependencies - id: cache-nimble - uses: actions/cache@v5 - with: - path: | - ~/.nimble/pkgcache - ~/.nimble/packages_official.json - key: ${{ matrix.nim }}-nimble-v6-${{ hashFiles('*.nimble') }} - restore-keys: | - ${{ matrix.nim }}-nimble-v6- - - - name: Setup Nim - uses: jiro4989/setup-nim-action@v2 - with: - nim-version: ${{ matrix.nim }} - use-nightlies: true - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Build Project - run: nimble build -Y - - - name: Upload 2.2.x build artifact - if: matrix.nim == '2.2.x' - uses: actions/upload-artifact@v6 - with: - name: nitter-linux-nim-2.2.x-${{ github.sha }} - path: | - ./nitter - if-no-files-found: error - - integration-test: - needs: [build-test] - name: Integration test - runs-on: ubuntu-24.04 - timeout-minutes: 30 - - services: - redis: - image: redis:7 - ports: - - 6379:6379 - - steps: - - name: Install runtime deps - run: | - sudo apt-get install -y --no-install-recommends libsass-dev libpcre3 - - - name: Checkout code - uses: actions/checkout@v6 - - - name: Cache pipx (poetry) - uses: actions/cache@v5 - with: - path: | - ~/.local/pipx - ~/.local/bin - key: pipx-poetry-${{ runner.os }} - - - name: Install poetry - env: - PIPX_HOME: ~/.local/pipx - PIPX_BIN_DIR: ~/.local/bin - run: command -v poetry >/dev/null 2>&1 || pipx install poetry - - - name: Setup Python (3.14) with Poetry cache - uses: actions/setup-python@v6 - with: - python-version: "3.14" - cache: poetry - cache-dependency-path: tests/poetry.lock - - - name: Install Python deps - working-directory: tests - run: poetry sync - - - name: Cache Nimble Dependencies - uses: actions/cache@v5 - with: - path: | - ~/.nimble/pkgcache - ~/.nimble/packages_official.json - key: 2.2.x-nimble-v6-${{ hashFiles('*.nimble') }} - restore-keys: | - 2.2.x-nimble-v6- - - - name: Setup Nim - uses: jiro4989/setup-nim-action@v2 - with: - nim-version: 2.2.x - use-nightlies: true - repo-token: ${{ secrets.GITHUB_TOKEN }} - - - name: Install Nimble dependencies - run: nimble install -y --depsOnly - - - name: Download 2.2.x build artifact - uses: actions/download-artifact@v4 - with: - name: nitter-linux-nim-2.2.x-${{ github.sha }} - path: . - - - name: Make nitter binary executable - run: chmod +x ./nitter - - - name: Prepare Nitter Environment - run: | - cp nitter.example.conf nitter.conf - sed -i 's/enableDebug = false/enableDebug = true/g' nitter.conf - sed -i 's/maxRetries = 1/maxRetries = 3/g' nitter.conf - sed -i 's/hostname = "nitter.net"/hostname = "localhost:8080"/g' nitter.conf - - nim r tools/rendermd.nim - nim r tools/gencss.nim - - echo '${{ secrets.SESSIONS }}' | head -n1 - echo '${{ secrets.SESSIONS }}' > ./sessions.jsonl - - - name: Run Tests - run: | - ./nitter & - cd tests - poetry run pytest -n2 --rs . diff --git a/.gitignore b/.gitignore index 2e52163..4d742bb 100644 --- a/.gitignore +++ b/.gitignore @@ -3,18 +3,9 @@ nitter *.db /tests/__pycache__ /tests/geckodriver.log -/tests/downloaded_files -/tests/latest_logs +/tests/downloaded_files/* /tools/gencss /tools/rendermd /public/css/style.css /public/md/*.html nitter.conf -guest_accounts.json* -sessions.json* -dump.rdb -*.bak -/tools/*.json* -nimbledeps/ -nimble.paths -nimble.develop diff --git a/Dockerfile b/Dockerfile index 251b63a..29e5d4e 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,6 @@ -FROM nimlang/nim:2.2.6-alpine-regular as nim +FROM nimlang/nim:1.6.2-alpine-regular as nim LABEL maintainer="setenforce@protonmail.com" +EXPOSE 8080 RUN apk --no-cache add libsass-dev pcre @@ -9,17 +10,14 @@ COPY nitter.nimble . RUN nimble install -y --depsOnly COPY . . -RUN nimble build -d:danger -d:lto -d:strip --mm:refc \ +RUN nimble build -d:danger -d:lto -d:strip \ && nimble scss \ && nimble md FROM alpine:latest WORKDIR /src/ -RUN apk --no-cache add pcre ca-certificates openssl +RUN apk --no-cache add pcre COPY --from=nim /src/nitter/nitter ./ COPY --from=nim /src/nitter/nitter.example.conf ./nitter.conf COPY --from=nim /src/nitter/public ./public -EXPOSE 8080 -RUN adduser -h /src/ -D -s /bin/sh nitter -USER nitter CMD ./nitter diff --git a/README.md b/README.md index 86ebd47..460ce21 100644 --- a/README.md +++ b/README.md @@ -1,36 +1,30 @@ # Nitter -> [!NOTE] -> On 24 August 2026 cease and desist letters were sent by X Corp. demanding a permanent takedown of Nitter instances and the project's repository. +[![Test Matrix](https://github.com/zedeus/nitter/workflows/CI/CD/badge.svg)](https://github.com/zedeus/nitter/actions?query=workflow%3ACI/CD) +[![License](https://img.shields.io/github/license/zedeus/nitter?style=flat)](#license) A free and open source alternative Twitter front-end focused on privacy and performance. \ -Inspired by the [Invidious](https://github.com/iv-org/invidious) project. - -## Donations - -**Liberapay**: https://liberapay.com/zedeus
-**Patreon**: https://patreon.com/nitter
-**Ko-fi**: https://ko-fi.com/zedeus
-**BTC**: bc1qpqpzjkcpgluhzf7x9yqe7jfe8gpfm5v08mdr55
-**ETH**: 0x24a0DB59A923B588c7A5EBd0dBDFDD1bCe9c4460
-**XMR**: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL
-**SOL**: FF5bheiD5AqPEdc3eyjymJ8AoMRF1hS78Ht6FiSZZF1t
-**$Nitter**: 4fSxCKc91ELQYVdv3tmHW8R15KoALPwEngyoQe1Xpump
-**ZEC**: u1vndfqtzyy6qkzhkapxelel7ams38wmfeccu3fdpy2wkuc4erxyjm8ncjhnyg747x6t0kf0faqhh2hxyplgaum08d2wnj4n7cyu9s6zhxkqw2aef4hgd4s6vh5hpqvfken98rg80kgtgn64ff70djy7s8f839z00hwhuzlcggvefhdlyszkvwy3c7yw623vw3rvar6q6evd3xcvveypt - -## Features +Inspired by the [Invidious](https://github.com/iv-org/invidious) +project. - No JavaScript or ads - All requests go through the backend, client never talks to Twitter - Prevents Twitter from tracking your IP or JavaScript fingerprint -- Uses Twitter's unofficial API (no developer account required) +- Uses Twitter's unofficial API (no rate limits or developer account required) - Lightweight (for [@nim_lang](https://nitter.net/nim_lang), 60KB vs 784KB from twitter.com) - RSS feeds - Themes - Mobile support (responsive design) - AGPLv3 licensed, no proprietary instances permitted +Liberapay: https://liberapay.com/zedeus \ +Patreon: https://patreon.com/nitter \ +BTC: bc1qp7q4qz0fgfvftm5hwz3vy284nue6jedt44kxya \ +ETH: 0x66d84bc3fd031b62857ad18c62f1ba072b011925 \ +LTC: ltc1qhsz5nxw6jw9rdtw9qssjeq2h8hqk2f85rdgpkr \ +XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL + ## Roadmap - Embeds @@ -40,20 +34,19 @@ Inspired by the [Invidious](https://github.com/iv-org/invidious) project. ## Resources -The wiki contains +The wiki contains [a list of instances](https://github.com/zedeus/nitter/wiki/Instances) and [browser extensions](https://github.com/zedeus/nitter/wiki/Extensions) maintained by the community. ## Why? -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 [browser's -fingerprint](https://restoreprivacy.com/browser-fingerprinting/), [no -JavaScript required](https://noscriptfingerprint.com/). This all became +It's impossible to use Twitter without JavaScript enabled. 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 +[browser's fingerprint](https://restoreprivacy.com/browser-fingerprinting/), +[no JavaScript required](https://noscriptfingerprint.com/). This all became particularly important after Twitter [removed the ability](https://www.eff.org/deeplinks/2020/04/twitter-removes-privacy-option-and-shows-why-we-need-strong-privacy-laws) for users to control whether their data gets sent to advertisers. @@ -74,24 +67,21 @@ Twitter account. ## Installation ### Dependencies - -- libpcre -- libsass -- redis/valkey +* libpcre +* libsass +* redis To compile Nitter you need a Nim installation, see -[nim-lang.org](https://nim-lang.org/install.html) for details. It is possible -to install it system-wide or in the user directory you create below. +[nim-lang.org](https://nim-lang.org/install.html) for details. It is possible to +install it system-wide or in the user directory you create below. To compile the scss files, you need to install `libsass`. On Ubuntu and Debian, you can use `libsass-dev`. -Redis is required for caching and in the future for account info. As of 2024 -Redis is no longer open source, so using the fork Valkey is recommended. It -should be available on most distros as `redis` or `redis-server` -(Ubuntu/Debian), or `valkey`/`valkey-server`. Running it with the default -config is fine, Nitter's default config is set to use the default port and -localhost. +Redis is required for caching and in the future for account info. It should be +available on most distros as `redis` or `redis-server` (Ubuntu/Debian). +Running it with the default config is fine, Nitter's default config is set to +use the default Redis port and localhost. Here's how to create a `nitter` user, clone the repo, and build the project along with the scss and md files. @@ -101,9 +91,9 @@ along with the scss and md files. # su nitter $ git clone https://github.com/zedeus/nitter $ cd nitter -$ nimble -l build -d:danger --mm:refc -$ nimble -l scss -$ nimble -l md +$ nimble build -d:release +$ nimble scss +$ nimble md $ cp nitter.example.conf nitter.conf ``` @@ -118,50 +108,31 @@ performance reasons. ### Docker -Page for the Docker image: https://hub.docker.com/r/zedeus/nitter - -#### NOTE: The published image is multi-arch — `zedeus/nitter:latest` runs natively on both `amd64` and `arm64`. +#### NOTE: For ARM64/ARM support, please use [unixfox's image](https://quay.io/repository/unixfox/nitter?tab=tags), more info [here](https://github.com/zedeus/nitter/issues/399#issuecomment-997263495) To run Nitter with Docker, you'll need to install and run Redis separately before you can run the container. See below for how to also run Redis using Docker. -First create your config file. The Docker commands mount it into the container, -so it has to exist on the host beforehand. If you've cloned the repo: - -```bash -cp nitter.example.conf nitter.conf -``` - -If you're using the prebuilt image without a local clone, download -[`nitter.example.conf`](https://raw.githubusercontent.com/zedeus/nitter/master/nitter.example.conf) -and save it as `nitter.conf` instead. - To build and run Nitter in Docker: - ```bash docker build -t nitter:latest . docker run -v $(pwd)/nitter.conf:/src/nitter.conf -d --network host nitter:latest ``` A prebuilt Docker image is provided as well: - ```bash docker run -v $(pwd)/nitter.conf:/src/nitter.conf -d --network host zedeus/nitter:latest ``` Using docker-compose to run both Nitter and Redis as different containers: Change `redisHost` from `localhost` to `nitter-redis` in `nitter.conf`, then run: - ```bash docker-compose up -d ``` -Note the Docker commands mount `nitter.conf` (and `sessions.jsonl` for -docker-compose) from the directory you run them in. If a mounted file doesn't -exist, Docker silently creates a directory in its place and the container fails -with `not a directory: Are you trying to mount a directory onto a file`. Remove -that directory and create the file as shown above. +Note the Docker commands expect a `nitter.conf` file in the directory you run +them. ### systemd @@ -206,5 +177,3 @@ lines). If you're running the Docker image, you can do this: Feel free to join our [Matrix channel](https://matrix.to/#/#nitter:matrix.org). You can email me at zedeus@pm.me if you wish to contact me personally. - -For legal inquiries, contact legal@poast.org diff --git a/compose.yml b/compose.yml deleted file mode 100644 index 72d5a96..0000000 --- a/compose.yml +++ /dev/null @@ -1,46 +0,0 @@ -services: - - nitter: - image: zedeus/nitter:latest - container_name: nitter - ports: - - "127.0.0.1:8080:8080" # Replace with "8080:8080" if you don't use a reverse proxy - volumes: - - ./nitter.conf:/src/nitter.conf:Z,ro - - ./sessions.jsonl:/src/sessions.jsonl:Z,ro # Run get_sessions.py to get the credentials - depends_on: - - nitter-redis - restart: unless-stopped - healthcheck: - test: wget -nv --tries=1 --spider http://127.0.0.1:8080/Jack/status/20 || exit 1 - interval: 30s - timeout: 5s - retries: 2 - user: "998:998" - read_only: true - security_opt: - - no-new-privileges:true - cap_drop: - - ALL - - nitter-redis: - image: redis:6-alpine - container_name: nitter-redis - command: redis-server --save 60 1 --loglevel warning - volumes: - - nitter-redis:/data - restart: unless-stopped - healthcheck: - test: redis-cli ping - interval: 30s - timeout: 5s - retries: 2 - user: "999:1000" - read_only: true - security_opt: - - no-new-privileges:true - cap_drop: - - ALL - -volumes: - nitter-redis: diff --git a/config.nims b/config.nims index 3ee4842..ee77289 100644 --- a/config.nims +++ b/config.nims @@ -1,17 +1,17 @@ --define:ssl --define:useStdLib ---threads:off # workaround httpbeast file upload bug --assertions:off # disable annoying warnings warning("GcUnsafe2", off) -warning("HoleEnumConv", off) hint("XDeclaredButNotUsed", off) hint("XCannotRaiseY", off) hint("User", off) -# begin Nimble config (version 2) -when withDir(thisDir(), system.fileExists("nimble.paths")): - include "nimble.paths" -# end Nimble config + +const + nimVersion = (major: NimMajor, minor: NimMinor, patch: NimPatch) + +when nimVersion >= (1, 6, 0): + warning("HoleEnumConv", off) diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a98855f --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,25 @@ +version: "3" + +services: + + nitter: + image: zedeus/nitter:latest + container_name: nitter + ports: + - "127.0.0.1:8080:8080" # Replace with "8080:8080" if you don't use a reverse proxy + volumes: + - ./nitter.conf:/src/nitter.conf:ro + depends_on: + - nitter-redis + restart: unless-stopped + + nitter-redis: + image: redis:6-alpine + container_name: nitter-redis + command: redis-server --save 60 1 --loglevel warning + volumes: + - nitter-redis:/data + restart: unless-stopped + +volumes: + nitter-redis: diff --git a/nitter.example.conf b/nitter.example.conf index 4e040f8..b987cd5 100644 --- a/nitter.example.conf +++ b/nitter.example.conf @@ -1,49 +1,45 @@ [Server] -hostname = "nitter.net" # for generating links, change this to your own domain/ip -title = "nitter" address = "0.0.0.0" port = 8080 -https = false # disable to enable cookies when not using https +https = false # disable to enable cookies when not using https httpMaxConnections = 100 staticDir = "./public" +title = "nitter" +hostname = "nitter.net" [Cache] -listMinutes = 240 # how long to cache list info (not the tweets, so keep it high) -rssMinutes = 10 # how long to cache rss queries -redisHost = "localhost" # Change to "nitter-redis" if using docker-compose +listMinutes = 240 # how long to cache list info (not the tweets, so keep it high) +rssMinutes = 10 # how long to cache rss queries +redisHost = "localhost" # Change to "nitter-redis" if using docker-compose redisPort = 6379 redisPassword = "" -redisConnections = 20 # minimum open connections in pool +redisConnections = 20 # connection pool size redisMaxConnections = 30 -# new connections are opened when none are available, but if the pool size +# max, new connections are opened when none are available, but if the pool size # goes above this, they're closed when released. don't worry about this unless # you receive tons of requests per second [Config] -hmacKey = "secretkey" # CHANGE THIS to a unique random value (e.g. `openssl rand -hex 32`); signs media urls -base64Media = false # use base64 encoding for proxied media urls -enableRSS = true # master switch, set to false to disable all RSS feeds -enableRSSUserTweets = true # /@user/rss -enableRSSUserReplies = true # /@user/with_replies/rss -enableRSSUserMedia = true # /@user/media/rss -enableRSSUserArticles = true # /@user/articles/rss -enableRSSSearch = true # /search/rss and /@user/search/rss -enableRSSList = true # list RSS feeds -enableDebug = false # enable request logs and debug endpoints (/.sessions) -proxy = "" # http/https url, SOCKS proxies are not supported +hmacKey = "secretkey" # random key for cryptographic signing of video urls +base64Media = false # use base64 encoding for proxied media urls +enableRSS = true # set this to false to disable RSS feeds +enableDebug = false # enable request logs and debug endpoints +proxy = "" # http/https url, SOCKS proxies are not supported proxyAuth = "" -apiProxy = "" # nitter-proxy host, e.g. localhost:7000 -disableTid = false # enable this if cookie-based auth is failing -maxConcurrentReqs = 2 # max requests at a time per session to avoid race conditions -maxRetries = 1 # max number of retries on rate limit errors -retryDelayMs = 150 # delay in ms between retries +tokenCount = 10 +# minimum amount of usable tokens. tokens are used to authorize API requests, +# but they expire after ~1 hour, and have a limit of 187 requests. +# the limit gets reset every 15 minutes, and the pool is filled up so there's +# always at least $tokenCount usable tokens. again, only increase this if +# you receive major bursts all the time # Change default preferences here, see src/prefs_impl.nim for a complete list [Preferences] theme = "Nitter" replaceTwitter = "nitter.net" -replaceYouTube = "piped.video" +replaceYouTube = "piped.kavin.rocks" replaceReddit = "teddit.net" +replaceInstagram = "" proxyVideos = true hlsPlayback = false infiniteScroll = false diff --git a/nitter.nimble b/nitter.nimble index b36f498..ea2436c 100644 --- a/nitter.nimble +++ b/nitter.nimble @@ -10,24 +10,25 @@ bin = @["nitter"] # Dependencies -requires "nim >= 2.0.0" -requires "jester == 0.6.0" -requires "karax == 1.5.0" -requires "sass == 0.2.0" -requires "nimcrypto == 0.7.3" -requires "markdown == 0.8.8" -requires "packedjson#9e6fbb6" -requires "supersnappy == 2.1.4" -requires "redpool == 0.2.2" -requires "zippy == 0.10.19" -requires "flatty == 0.4.0" -requires "jsony == 1.1.6" -requires "oauth == 0.11" +requires "nim >= 1.4.8" +requires "jester >= 0.5.0" +requires "karax#c71bc92" +requires "sass#e683aa1" +requires "nimcrypto#a5742a9" +requires "markdown#abdbe5e" +requires "packedjson#d11d167" +requires "supersnappy#2.1.1" +requires "redpool#8b7c1db" +requires "https://github.com/zedeus/redis#d0a0e6f" +requires "zippy#0.7.3" +requires "flatty#0.2.3" +requires "jsony#d0e69bd" + # Tasks task scss, "Generate css": - exec "nim r --hint[Processing]:off tools/gencss" + exec "nimble c --hint[Processing]:off -d:danger -r tools/gencss" task md, "Render md": - exec "nim r --hint[Processing]:off tools/rendermd" + exec "nimble c --hint[Processing]:off -d:danger -r tools/rendermd" diff --git a/public/css/fontello.css b/public/css/fontello.css index 8f9abad..d022bb5 100644 --- a/public/css/fontello.css +++ b/public/css/fontello.css @@ -1,153 +1,53 @@ @font-face { - font-family: "fontello"; - src: url("/fonts/fontello.eot?59696369"); - src: - url("/fonts/fontello.eot?59696369#iefix") format("embedded-opentype"), - url("/fonts/fontello.woff2?59696369") format("woff2"), - url("/fonts/fontello.woff?59696369") format("woff"), - url("/fonts/fontello.ttf?59696369") format("truetype"), - url("/fonts/fontello.svg?59696369#fontello") format("svg"); + font-family: 'fontello'; + src: url('/fonts/fontello.eot?21002321'); + src: url('/fonts/fontello.eot?21002321#iefix') format('embedded-opentype'), + url('/fonts/fontello.woff2?21002321') format('woff2'), + url('/fonts/fontello.woff?21002321') format('woff'), + url('/fonts/fontello.ttf?21002321') format('truetype'), + url('/fonts/fontello.svg?21002321#fontello') format('svg'); font-weight: normal; font-style: normal; } -[class^="icon-"]:before, -[class*=" icon-"]:before { + [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-group:before { - content: "\e0c3"; -} - -/* '' */ -.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-attention-circled:before { - content: "\e812"; -} - -/* '' */ -.icon-download-alt:before { - content: "\e813"; -} - -/* '' */ -.icon-circle:before { - content: "\f111"; -} - -/* '' */ -.icon-info:before { - content: "\f128"; -} - -/* '' */ -.icon-bird:before { - content: "\f309"; -} - -/* '' */ + +.icon-heart:before { content: '\2665'; } /* '♥' */ +.icon-quote:before { content: '\275e'; } /* '❞' */ +.icon-comment:before { content: '\e802'; } /* '' */ +.icon-ok:before { content: '\e803'; } /* '' */ +.icon-play:before { content: '\e804'; } /* '' */ +.icon-link:before { content: '\e805'; } /* '' */ +.icon-calendar:before { content: '\e806'; } /* '' */ +.icon-location:before { content: '\e807'; } /* '' */ +.icon-picture:before { content: '\e809'; } /* '' */ +.icon-lock:before { content: '\e80a'; } /* '' */ +.icon-down:before { content: '\e80b'; } /* '' */ +.icon-retweet:before { content: '\e80d'; } /* '' */ +.icon-search:before { content: '\e80e'; } /* '' */ +.icon-pin:before { content: '\e80f'; } /* '' */ +.icon-cog:before { content: '\e812'; } /* '' */ +.icon-rss-feed:before { content: '\e813'; } /* '' */ +.icon-info:before { content: '\f128'; } /* '' */ +.icon-bird:before { content: '\f309'; } /* '' */ diff --git a/public/css/themes/dracula.css b/public/css/themes/dracula.css deleted file mode 100644 index 6042f7d..0000000 --- a/public/css/themes/dracula.css +++ /dev/null @@ -1,41 +0,0 @@ -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/fonts/LICENSE.txt b/public/fonts/LICENSE.txt index 41f18a8..c8d90ff 100644 --- a/public/fonts/LICENSE.txt +++ b/public/fonts/LICENSE.txt @@ -1,15 +1,6 @@ Font license info -## Modern Pictograms - - Copyright (c) 2012 by John Caserta. All rights reserved. - - Author: John Caserta - License: SIL (http://scripts.sil.org/OFL) - Homepage: http://thedesignoffice.org/project/modern-pictograms/ - - ## Entypo Copyright (C) 2012 by Daniel Bruce @@ -46,3 +37,12 @@ Font license info Homepage: http://aristeides.com/ +## Modern Pictograms + + Copyright (c) 2012 by John Caserta. All rights reserved. + + Author: John Caserta + License: SIL (http://scripts.sil.org/OFL) + Homepage: http://thedesignoffice.org/project/modern-pictograms/ + + diff --git a/public/fonts/fontello.eot b/public/fonts/fontello.eot index a5b8b1c..aaddd6b 100644 Binary files a/public/fonts/fontello.eot and b/public/fonts/fontello.eot differ diff --git a/public/fonts/fontello.svg b/public/fonts/fontello.svg index 5dc65a8..1f30ccc 100644 --- a/public/fonts/fontello.svg +++ b/public/fonts/fontello.svg @@ -1,28 +1,26 @@ -Copyright (C) 2026 by original authors @ fontello.com +Copyright (C) 2020 by original authors @ fontello.com - + - + - + - + - + - + - + - - - + @@ -30,27 +28,19 @@ - + - + - + - + - - - - - - - - - + - + \ No newline at end of file diff --git a/public/fonts/fontello.ttf b/public/fonts/fontello.ttf index a2b972e..29f1ec6 100644 Binary files a/public/fonts/fontello.ttf and b/public/fonts/fontello.ttf differ diff --git a/public/fonts/fontello.woff b/public/fonts/fontello.woff index 65508ca..8428cf8 100644 Binary files a/public/fonts/fontello.woff and b/public/fonts/fontello.woff differ diff --git a/public/fonts/fontello.woff2 b/public/fonts/fontello.woff2 index a8d96da..551f49d 100644 Binary files a/public/fonts/fontello.woff2 and b/public/fonts/fontello.woff2 differ diff --git a/public/js/embedResize.js b/public/js/embedResize.js deleted file mode 100644 index 3fb05a0..0000000 --- a/public/js/embedResize.js +++ /dev/null @@ -1,34 +0,0 @@ -(function () { - var embed = document.querySelector(".embed-wrapper, .embed-video"); - if (!embed) return; - - var video = embed.querySelector("video"); - if (video) { - video.onplay = function () { - embed.classList.add("video-playing"); - }; - video.onpause = video.onended = function () { - embed.classList.remove("video-playing"); - }; - } - - var lastHeight = 0; - - function sendHeight() { - var h = embed.offsetHeight; - if (h !== lastHeight && h > 0) { - lastHeight = h; - window.parent.postMessage(["resizeIframe", { h: h }], "*"); - } - } - - // MessageChannel height request (used by oEmbed) - window.addEventListener("message", function (e) { - if (e.source === window.parent && e.ports && e.ports[0]) { - e.ports[0].postMessage(embed.offsetHeight); - } - }); - - window.addEventListener("load", sendHeight); - new ResizeObserver(sendHeight).observe(embed); -})(); diff --git a/public/js/hls.light.min.js b/public/js/hls.light.min.js new file mode 100644 index 0000000..af4758a --- /dev/null +++ b/public/js/hls.light.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.0.6 +"undefined"!=typeof window&&function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.Hls=e():t.Hls=e()}(this,(function(){return function(t){var e={};function r(i){if(e[i])return e[i].exports;var a=e[i]={i:i,l:!1,exports:{}};return t[i].call(a.exports,a,a.exports,r),a.l=!0,a.exports}return r.m=t,r.c=e,r.d=function(t,e,i){r.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:i})},r.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},r.t=function(t,e){if(1&e&&(t=r(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var i=Object.create(null);if(r.r(i),Object.defineProperty(i,"default",{enumerable:!0,value:t}),2&e&&"string"!=typeof t)for(var a in t)r.d(i,a,function(e){return t[e]}.bind(null,a));return i},r.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return r.d(e,"a",e),e},r.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},r.p="/dist/",r(r.s=19)}([function(t,e,r){"use strict";var i;r.d(e,"a",(function(){return i})),function(t){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.ERROR="hlsError",t.DESTROYING="hlsDestroying",t.KEY_LOADING="hlsKeyLoading",t.KEY_LOADED="hlsKeyLoaded",t.LIVE_BACK_BUFFER_REACHED="hlsLiveBackBufferReached",t.BACK_BUFFER_REACHED="hlsBackBufferReached"}(i||(i={}))},function(t,e,r){"use strict";r.d(e,"a",(function(){return o})),r.d(e,"b",(function(){return l}));var i=function(){},a={trace:i,debug:i,log:i,warn:i,info:i,error:i},n=a;function s(t){var e=self.console[t];return e?e.bind(self.console,"["+t+"] >"):i}function o(t){if(self.console&&!0===t||"object"==typeof t){!function(t){for(var e=arguments.length,r=new Array(e>1?e-1:0),i=1;i>8*(15-r)&255;return e},r.setDecryptDataFromLevelKey=function(t,e){var r=t;return"AES-128"===(null==t?void 0:t.method)&&t.uri&&!t.iv&&((r=o.a.fromURI(t.uri)).method=t.method,r.iv=this.createInitializationVector(e),r.keyFormat="identity"),r},r.setElementaryStreamInfo=function(t,e,r,i,a,n){void 0===n&&(n=!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,a)):s[t]={startPTS:e,endPTS:r,startDTS:i,endDTS:a,partial:n}},r.clearElementaryStreamInfo=function(){var t=this.elementaryStreams;t[i.AUDIO]=null,t[i.VIDEO]=null,t[i.AUDIOVIDEO]=null},f(e,[{key:"decryptdata",get:function(){if(!this.levelkey&&!this._decryptdata)return null;if(!this._decryptdata&&this.levelkey){var t=this.sn;"number"!=typeof t&&(this.levelkey&&"AES-128"===this.levelkey.method&&!this.levelkey.iv&&s.b.warn('missing IV for initialization segment with method="'+this.levelkey.method+'" - compliance issue'),t=0),this._decryptdata=this.setDecryptDataFromLevelKey(this.levelkey,t)}return this._decryptdata}},{key:"end",get:function(){return this.start+this.duration}},{key:"endProgramDateTime",get:function(){if(null===this.programDateTime)return null;if(!Object(a.a)(this.programDateTime))return null;var t=Object(a.a)(this.duration)?this.duration:0;return this.programDateTime+1e3*t}},{key:"encrypted",get:function(){var t;return!(null===(t=this.decryptdata)||void 0===t||!t.keyFormat||!this.decryptdata.uri)}}]),e}(c),g=function(t){function e(e,r,i,a,n){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 l.a,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=a;var o=e.enumeratedString("BYTERANGE");return o&&s.setByteRange(o,n),n&&(s.fragOffset=n.fragOffset+n.duration),s}return u(e,t),f(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}(c)},function(t,e,r){"use strict";r.d(e,"b",(function(){return d})),r.d(e,"g",(function(){return h})),r.d(e,"f",(function(){return f})),r.d(e,"d",(function(){return c})),r.d(e,"c",(function(){return v})),r.d(e,"e",(function(){return p})),r.d(e,"h",(function(){return m})),r.d(e,"a",(function(){return y}));var i=r(8),a=r(5),n=Math.pow(2,32)-1,s=[].push;function o(t){return String.fromCharCode.apply(null,t)}function l(t,e){"data"in t&&(e+=t.start,t=t.data);var r=t[e]<<24|t[e+1]<<16|t[e+2]<<8|t[e+3];return r<0?4294967296+r:r}function u(t,e,r){"data"in t&&(e+=t.start,t=t.data),t[e]=r>>24,t[e+1]=r>>16&255,t[e+2]=r>>8&255,t[e+3]=255&r}function d(t,e){var r,i,a,n=[];if(!e.length)return n;"data"in t?(r=t.data,i=t.start,a=t.end):(i=0,a=(r=t).byteLength);for(var u=i;u1?u+h:a;if(o(r.subarray(u+4,u+8))===e[0])if(1===e.length)n.push({data:r,start:u+8,end:f});else{var c=d({data:r,start:u+8,end:f},e.slice(1));c.length&&s.apply(n,c)}u=f}return n}function h(t){var e=d(t,["moov"])[0],r=e?e.end:null,i=d(t,["sidx"]);if(!i||!i[0])return null;var a=[],n=i[0],s=n.data[0],o=0===s?8:16,u=l(n,o);o+=4;o+=0===s?8:16,o+=2;var h=n.end+0,f=function(t,e){"data"in t&&(e+=t.start,t=t.data);var r=t[e]<<8|t[e+1];return r<0?65536+r:r}(n,o);o+=2;for(var c=0;c>>31)return console.warn("SIDX has hierarchical references (not supported)"),null;var m=l(n,v);v+=4,a.push({referenceSize:p,subsegmentDuration:m,info:{duration:m/u,start:h,end:h+p-1}}),h+=p,o=v+=4}return{earliestPresentationTime:0,timescale:u,version:s,referencesCount:f,references:a,moovEndOffset:r}}function f(t){for(var e=[],r=d(t,["moov","trak"]),i=0;i0)return t.subarray(r,r+i)},o=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]},l=function(t,e){return a(t,e)&&o(t,e+6)+10<=t.length-e},u=function(t){for(var e=f(t),r=0;r>4){case 0:case 1:case 2:case 3:case 4:case 5:case 6:case 7:u+=String.fromCharCode(n);break;case 12:case 13:s=t[d++],u+=String.fromCharCode((31&n)<<6|63&s);break;case 14:s=t[d++],o=t[d++],u+=String.fromCharCode((15&n)<<12|(63&s)<<6|(63&o)<<0)}}return u};function b(){return i||void 0===self.TextDecoder||(i=new self.TextDecoder("utf-8")),i}},function(t,e,r){"use strict";function i(t,e,r){return Uint8Array.prototype.slice?t.slice(e,r):new Uint8Array(Array.prototype.slice.call(t,e,r))}r.d(e,"a",(function(){return i}))},function(t,e,r){"use strict";r.d(e,"c",(function(){return St})),r.d(e,"d",(function(){return At})),r.d(e,"a",(function(){return Rt})),r.d(e,"b",(function(){return Dt}));var i=r(0),a=r(2),n=r(14),s=r(3),o=r(7);var l=r(6),u=r(8),d=function(){function t(){this._audioTrack=void 0,this._id3Track=void 0,this.frameIndex=0,this.cachedData=null,this.initPTS=null}var e=t.prototype;return e.resetInitSegment=function(t,e,r){this._id3Track={type:"id3",id:0,pid:-1,inputTimeScale:9e4,sequenceNumber:0,samples:[],dropped:0}},e.resetTimeStamp=function(){},e.resetContiguity=function(){},e.canParse=function(t,e){return!1},e.appendFrame=function(t,e,r){},e.demux=function(t,e){this.cachedData&&(t=Object(l.a)(this.cachedData,t),this.cachedData=null);var r,i,a=o.b(t,0),n=a?a.length:0,s=this._audioTrack,d=this._id3Track,f=a?o.d(a):void 0,c=t.length;for(0!==this.frameIndex&&null!==this.initPTS||(this.initPTS=h(f,e)),a&&a.length>0&&d.samples.push({pts:this.initPTS,dts:this.initPTS,data:a}),i=this.initPTS;n>>5}function m(t,e){return e+1=t.length)return!1;var i=p(t,e);if(i<=r)return!1;var a=e+i;return a===t.length||m(t,a)}return!1}function b(t,e,r,n,s){if(!t.samplerate){var o=function(t,e,r,n){var s,o,l,u,d=navigator.userAgent.toLowerCase(),h=n,f=[96e3,88200,64e3,48e3,44100,32e3,24e3,22050,16e3,12e3,11025,8e3,7350];s=1+((192&e[r+2])>>>6);var v=(60&e[r+2])>>>2;if(!(v>f.length-1))return l=(1&e[r+2])<<2,l|=(192&e[r+3])>>>6,c.b.log("manifest codec:"+n+", ADTS type:"+s+", samplingIndex:"+v),/firefox/i.test(d)?v>=6?(s=5,u=new Array(4),o=v-3):(s=2,u=new Array(2),o=v):-1!==d.indexOf("android")?(s=2,u=new Array(2),o=v):(s=5,u=new Array(4),n&&(-1!==n.indexOf("mp4a.40.29")||-1!==n.indexOf("mp4a.40.5"))||!n&&v>=6?o=v-3:((n&&-1!==n.indexOf("mp4a.40.2")&&(v>=6&&1===l||/vivaldi/i.test(d))||!n&&1===l)&&(s=2,u=new Array(2)),o=v)),u[0]=s<<3,u[0]|=(14&v)>>1,u[1]|=(1&v)<<7,u[1]|=l<<3,5===s&&(u[1]|=(14&o)>>1,u[2]=(1&o)<<7,u[2]|=8,u[3]=0),{config:u,samplerate:f[v],channelCount:l,codec:"mp4a.40."+s,manifestCodec:h};t.trigger(i.a.ERROR,{type:a.b.MEDIA_ERROR,details:a.a.FRAG_PARSING_ERROR,fatal:!0,reason:"invalid ADTS sampling index:"+v})}(e,r,n,s);if(!o)return;t.config=o.config,t.samplerate=o.samplerate,t.channelCount=o.channelCount,t.codec=o.codec,t.manifestCodec=o.manifestCodec,c.b.log("parsed codec:"+t.codec+", rate:"+o.samplerate+", channels:"+o.channelCount)}}function T(t){return 9216e4/t}function E(t,e,r,i,a){var n=function(t,e,r,i,a){var n=g(t,e),s=p(t,e);if((s-=n)>0)return{headerLength:n,frameLength:s,stamp:r+i*a}}(e,r,i,a,T(t.samplerate));if(n){var s,o=n.frameLength,l=n.headerLength,u=n.stamp,d=l+o,h=Math.max(0,r+d-e.length);h?(s=new Uint8Array(d-l)).set(e.subarray(r+l,e.length),0):s=e.subarray(r+l,r+d);var f={unit:s,pts:u};return h||t.samples.push(f),{sample:f,length:d,missing:h}}}function S(t,e){return(S=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}var L=function(t){var e,r;function i(e,r){var i;return(i=t.call(this)||this).observer=void 0,i.config=void 0,i.observer=e,i.config=r,i}r=t,(e=i).prototype=Object.create(r.prototype),e.prototype.constructor=e,S(e,r);var a=i.prototype;return a.resetInitSegment=function(e,r,i){t.prototype.resetInitSegment.call(this,e,r,i),this._audioTrack={container:"audio/adts",type:"audio",id:0,pid:-1,sequenceNumber:0,isAAC:!0,samples:[],manifestCodec:e,duration:i,inputTimeScale:9e4,dropped:0}},i.probe=function(t){if(!t)return!1;for(var e=(o.b(t,0)||[]).length,r=t.length;e0},e.demux=function(t){var e=t,r={type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0};if(this.config.progressive){this.remainderData&&(e=Object(l.a)(this.remainderData,t));var i=Object(l.h)(e);this.remainderData=i.remainder,r.samples=i.valid||new Uint8Array}else r.samples=e;return{audioTrack:{type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0},avcTrack:r,id3Track:{type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0},textTrack:{type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0}}},e.flush=function(){var t={type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0};return t.samples=this.remainderData||new Uint8Array,this.remainderData=null,{audioTrack:{type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0},avcTrack:t,id3Track:{type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0},textTrack:{type:"",id:-1,pid:-1,inputTimeScale:9e4,sequenceNumber:-1,samples:[],dropped:0}}},e.demuxSampleAes=function(t,e,r){return Promise.reject(new Error("The MP4 demuxer does not support SAMPLE-AES decryption"))},e.destroy=function(){},t}();R.minProbeByteLength=1024;var D=R,_=null,k=[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],x=[44100,48e3,32e3,22050,24e3,16e3,11025,12e3,8e3],w=[[0,72,144,12],[0,0,0,0],[0,72,144,12],[0,144,144,12]],C=[0,1,1,4];function O(t,e,r,i,a){if(!(r+24>e.length)){var n=I(e,r);if(n&&r+n.frameLength<=e.length){var s=i+a*(9e4*n.samplesPerFrame/n.sampleRate),o={unit:e.subarray(r,r+n.frameLength),pts:s,dts:s};return t.config=[],t.channelCount=n.channelCount,t.samplerate=n.sampleRate,t.samples.push(o),{sample:o,length:n.frameLength,missing:0}}}}function I(t,e){var r=t[e+1]>>3&3,i=t[e+1]>>1&3,a=t[e+2]>>4&15,n=t[e+2]>>2&3;if(1!==r&&0!==a&&15!==a&&3!==n){var s=t[e+2]>>1&1,o=t[e+3]>>6,l=1e3*k[14*(3===r?3-i:3===i?3:4)+a-1],u=x[3*(3===r?0:2===r?1:2)+n],d=3===o?1:2,h=w[r][i],f=C[i],c=8*h*f,v=Math.floor(h*l/u+s)*f;if(null===_){var g=(navigator.userAgent||"").match(/Chrome\/(\d+)/i);_=g?parseInt(g[1]):0}return!!_&&_<=87&&2===i&&l>=224e3&&0===o&&(t[e+3]=128|t[e+3]),{sampleRate:u,channelCount:d,frameLength:v,samplesPerFrame:c}}}function P(t,e){return 255===t[e]&&224==(224&t[e+1])&&0!=(6&t[e+1])}function F(t,e){return e+1t?(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;return t>32&&c.b.error("Cannot read more than 32 bits at a time"),this.bitsAvailable-=e,this.bitsAvailable>0?this.word<<=e:this.bytesAvailable>0&&this.loadWord(),(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;i=t.length)return void r();if(!(t[e].unit.length<32)){var i=this.decrypter.isSync();if(this.decryptAacSample(t,e,r,i),!i)return}}},e.getAvcEncryptedData=function(t){for(var e=16*Math.floor((t.length-48)/160)+16,r=new Int8Array(e),i=0,a=32;a<=t.length-16;a+=160,i+=16)r.set(t.subarray(a,a+16),i);return r},e.getAvcDecryptedUnit=function(t,e){for(var r=new Uint8Array(e),i=0,a=32;a<=t.length-16;a+=160,i+=16)t.set(r.subarray(i,i+16),a);return t},e.decryptAvcSample=function(t,e,r,i,a,n){var s=X(a.data),o=this.getAvcEncryptedData(s),l=this;this.decryptBuffer(o.buffer,(function(o){a.data=l.getAvcDecryptedUnit(s,o),n||l.decryptAvcSamples(t,e,r+1,i)}))},e.decryptAvcSamples=function(t,e,r,i){if(t instanceof Uint8Array)throw new Error("Cannot decrypt samples of type Uint8Array");for(;;e++,r=0){if(e>=t.length)return void i();for(var a=t[e].units;!(r>=a.length);r++){var n=a[r];if(!(n.data.length<=48||1!==n.type&&5!==n.type)){var s=this.decrypter.isSync();if(this.decryptAvcSample(t,e,r,i,n,s),!s)return}}}},t}(),U={video:1,audio:2,id3:3,text:4},G=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.aacLastPTS=null,this._initPTS=null,this._initDTS=null,this._pmtId=-1,this._avcTrack=void 0,this._audioTrack=void 0,this._id3Track=void 0,this._txtTrack=void 0,this.aacOverFlow=null,this.avcSample=null,this.remainderData=null,this.observer=t,this.config=e,this.typeSupported=r}t.probe=function(e){var r=t.syncOffset(e);return!(r<0)&&(r&&c.b.warn("MPEG2-TS detected but first sync word found @ offset "+r+", junk ahead ?"),!0)},t.syncOffset=function(t){for(var e=Math.min(1e3,t.length-564),r=0;r>4>1){if((_=A+5+e[A+4])===A+188)continue}else _=A+4;switch(D){case f:R&&(v&&(o=V(v))&&this.parseAVCPES(o,!1),v={data:[],size:0}),v&&(v.data.push(e.subarray(_,A+188)),v.size+=A+188-_);break;case g:R&&(m&&(o=V(m))&&(d.isAAC?this.parseAACPES(o):this.parseMPEGPES(o)),m={data:[],size:0}),m&&(m.data.push(e.subarray(_,A+188)),m.size+=A+188-_);break;case p:R&&(y&&(o=V(y))&&this.parseID3PES(o),y={data:[],size:0}),y&&(y.data.push(e.subarray(_,A+188)),y.size+=A+188-_);break;case 0:R&&(_+=e[_]+1),E=this._pmtId=K(e,_);break;case E:R&&(_+=e[_]+1);var k=H(e,_,!0===this.typeSupported.mpeg||!0===this.typeSupported.mp3,n);(f=k.avc)>0&&(u.pid=f),(g=k.audio)>0&&(d.pid=g,d.isAAC=k.isAAC),(p=k.id3)>0&&(h.pid=p),b&&!T&&(c.b.log("reparse from beginning"),b=!1,A=L-188),T=this.pmtParsed=!0;break;case 17:case 8191:break;default:b=!0}}else this.observer.emit(i.a.ERROR,i.a.ERROR,{type:a.b.MEDIA_ERROR,details:a.a.FRAG_PARSING_ERROR,fatal:!1,reason:"TS packet did not start with 0x47"});u.pesData=v,d.pesData=m,h.pesData=y;var x={audioTrack:d,avcTrack:u,id3Track:h,textTrack:this._txtTrack};return s&&this.extractRemainingSamples(x),x},e.flush=function(){var t,e=this.remainderData;return this.remainderData=null,t=e?this.demux(e,-1,!1,!0):{audioTrack:this._audioTrack,avcTrack:this._avcTrack,textTrack:this._txtTrack,id3Track:this._id3Track},this.extractRemainingSamples(t),this.sampleAes?this.decrypt(t,this.sampleAes):t},e.extractRemainingSamples=function(t){var e,r=t.audioTrack,i=t.avcTrack,a=t.id3Track,n=i.pesData,s=r.pesData,o=a.pesData;n&&(e=V(n))?(this.parseAVCPES(e,!0),i.pesData=null):i.pesData=n,s&&(e=V(s))?(r.isAAC?this.parseAACPES(e):this.parseMPEGPES(e),r.pesData=null):(null!=s&&s.size&&c.b.log("last AAC PES packet truncated,might overlap between fragments"),r.pesData=s),o&&(e=V(o))?(this.parseID3PES(e),a.pesData=null):a.pesData=o},e.demuxSampleAes=function(t,e,r){var i=this.demux(t,r,!0,!this.config.progressive),a=this.sampleAes=new B(this.observer,this.config,e);return this.decrypt(i,a)},e.decrypt=function(t,e){return new Promise((function(r){var i=t.audioTrack,a=t.avcTrack;i.samples&&i.isAAC?e.decryptAacSamples(i.samples,0,(function(){a.samples?e.decryptAvcSamples(a.samples,0,0,(function(){r(t)})):r(t)})):a.samples&&e.decryptAvcSamples(a.samples,0,0,(function(){r(t)}))}))},e.destroy=function(){this._initPTS=this._initDTS=null,this._duration=0},e.parseAVCPES=function(t,e){var r,i=this,a=this._avcTrack,n=this.parseAVCNALu(t.data),s=this.avcSample,l=!1;t.data=null,s&&n.length&&!a.audFound&&(W(s,a),s=this.avcSample=j(!1,t.pts,t.dts,"")),n.forEach((function(e){switch(e.type){case 1:r=!0,s||(s=i.avcSample=j(!0,t.pts,t.dts,"")),s.frame=!0;var n=e.data;if(l&&n.length>4){var u=new N(n).readSliceType();2!==u&&4!==u&&7!==u&&9!==u||(s.key=!0)}break;case 5:r=!0,s||(s=i.avcSample=j(!0,t.pts,t.dts,"")),s.key=!0,s.frame=!0;break;case 6:r=!0;var d=new N(X(e.data));d.readUByte();for(var h=0,f=0,c=!1,v=0;!c&&d.bytesAvailable>1;){h=0;do{h+=v=d.readUByte()}while(255===v);f=0;do{f+=v=d.readUByte()}while(255===v);if(4===h&&0!==d.bytesAvailable){if(c=!0,181===d.readUByte())if(49===d.readUShort())if(1195456820===d.readUInt())if(3===d.readUByte()){for(var g=d.readUByte(),p=31&g,m=[g,d.readUByte()],y=0;y16){for(var b=[],T=0;T<16;T++)b.push(d.readUByte().toString(16)),3!==T&&5!==T&&7!==T&&9!==T||b.push("-");for(var E=f-16,S=new Uint8Array(E),L=0;L=0){var h={data:t.subarray(u,l-n-1),type:d};o.push(h)}else{var f=this.getLastNalUnit();if(f&&(s&&l<=4-s&&f.state&&(f.data=f.data.subarray(0,f.data.byteLength-s)),(r=l-n-1)>0)){var c=new Uint8Array(f.data.byteLength+r);c.set(f.data,0),c.set(t.subarray(0,r),f.data.byteLength),f.data=c}}l=0&&n>=0){var v={data:t.subarray(u,i),type:d,state:n};o.push(v)}if(0===o.length){var g=this.getLastNalUnit();if(g){var p=new Uint8Array(g.data.byteLength+t.byteLength);p.set(g.data,0),p.set(t,g.data.byteLength),g.data=p}}return a.naluState=n,o},e.parseAACPES=function(t){var e,r,n,s,o,l=0,u=this._audioTrack,d=this.aacOverFlow,h=t.data;if(d){this.aacOverFlow=null;var f=d.sample.unit.byteLength,v=Math.min(d.missing,f),g=f-v;d.sample.unit.set(h.subarray(0,v),g),u.samples.push(d.sample),l=d.missing}for(e=l,r=h.length;e1;){var l=new Uint8Array(o[0].length+o[1].length);l.set(o[0]),l.set(o[1],o[0].length),o[0]=l,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 u=e[7];192&u&&(a=536870912*(14&e[9])+4194304*(255&e[10])+16384*(254&e[11])+128*(255&e[12])+(254&e[13])/2,64&u?a-(n=536870912*(14&e[14])+4194304*(255&e[15])+16384*(254&e[16])+128*(255&e[17])+(254&e[18])/2)>54e5&&(c.b.warn(Math.round((a-n)/9e4)+"s delta between PTS and DTS, align them"),a=n):n=a);var d=(i=e[8])+9;if(t.size<=d)return null;t.size-=d;for(var h=new Uint8Array(t.size),f=0,v=o.length;fg){d-=g;continue}e=e.subarray(d),g-=d,d=0}h.set(e,s),s+=g}return r&&(r-=i+3),{data:h,pts:a,dts:n,len:r}}return null}function W(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 a=r[i-1];t.pts=a.pts,t.dts=a.dts}e.samples.push(t)}t.debug.length&&c.b.log(t.pts+"/"+t.dts+":"+t.debug)}function Y(t,e){var r=t.length;if(r>0){if(e.pts>=t[r-1].pts)t.push(e);else for(var i=r-1;i>=0;i--)if(e.pts1?r-1:0),a=1;a>24&255,o[1]=e>>16&255,o[2]=e>>8&255,o[3]=255&e,o.set(t,4),n=0,e=8;n>24&255,e>>16&255,e>>8&255,255&e,i>>24,i>>16&255,i>>8&255,255&i,a>>24,a>>16&255,a>>8&255,255&a,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/(J+1)),a=Math.floor(r%(J+1)),n=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,a>>24,a>>16&255,a>>8&255,255&a,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,n)},t.sdtp=function(e){var r,i,a=e.samples||[],n=new Uint8Array(4+a.length);for(r=0;r>>8&255),n.push(255&a),n=n.concat(Array.prototype.slice.call(i));for(r=0;r>>8&255),s.push(255&a),s=s.concat(Array.prototype.slice.call(i));var o=t.box(t.types.avcC,new Uint8Array([1,n[3],n[4],n[5],255,224|e.sps.length].concat(n).concat([e.pps.length]).concat(s))),l=e.width,u=e.height,d=e.pixelRatio[0],h=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([d>>24,d>>16&255,d>>8&255,255&d,h>>24,h>>16&255,h>>8&255,255&h])))},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.mp4a=function(e){var r=e.samplerate;return t.box(t.types.mp4a,new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,e.channelCount,0,16,0,0,0,0,r>>8&255,255&r,0,0]),t.box(t.types.esds,t.esds(e)))},t.mp3=function(e){var r=e.samplerate;return t.box(t.types[".mp3"],new Uint8Array([0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,e.channelCount,0,16,0,0,0,0,r>>8&255,255&r,0,0]))},t.stsd=function(e){return"audio"===e.type?e.isAAC||"mp3"!==e.codec?t.box(t.types.stsd,t.STSD,t.mp4a(e)):t.box(t.types.stsd,t.STSD,t.mp3(e)):t.box(t.types.stsd,t.STSD,t.avc1(e))},t.tkhd=function(e){var r=e.id,i=e.duration*e.timescale,a=e.width,n=e.height,s=Math.floor(i/(J+1)),o=Math.floor(i%(J+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,a>>8&255,255&a,0,0,n>>8&255,255&n,0,0]))},t.traf=function(e,r){var i=t.sdtp(e),a=e.id,n=Math.floor(r/(J+1)),s=Math.floor(r%(J+1));return t.box(t.types.traf,t.box(t.types.tfhd,new Uint8Array([0,0,0,0,a>>24,a>>16&255,a>>8&255,255&a])),t.box(t.types.tfdt,new Uint8Array([1,0,0,0,n>>24,n>>16&255,n>>8&255,255&n,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,a,n,s,o,l,u=e.samples||[],d=u.length,h=12+16*d,f=new Uint8Array(h);for(r+=8+h,f.set([0,0,15,1,d>>>24&255,d>>>16&255,d>>>8&255,255&d,r>>>24&255,r>>>16&255,r>>>8&255,255&r],0),i=0;i>>24&255,n>>>16&255,n>>>8&255,255&n,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,f)},t.initSegment=function(e){t.types||t.init();var r=t.moov(e),i=new Uint8Array(t.FTYP.byteLength+r.byteLength);return i.set(t.FTYP),i.set(r,t.FTYP.byteLength),i},t}();tt.types=void 0,tt.HDLR_TYPES=void 0,tt.STTS=void 0,tt.STSC=void 0,tt.STCO=void 0,tt.STSZ=void 0,tt.VMHD=void 0,tt.SMHD=void 0,tt.STSD=void 0,tt.FTYP=void 0,tt.DINF=void 0;var et=tt,rt=r(4);function it(t,e,r,i){void 0===r&&(r=1),void 0===i&&(i=!1);var a=t*e*r;return i?Math.round(a):a}function at(t,e){return void 0===e&&(e=!1),it(t,1e3,1/9e4,e)}function nt(){return(nt=Object.assign||function(t){for(var e=1;e0?t:r.pts}),t[0].pts);return e&&c.b.debug("PTS rollover detected"),r},e.remux=function(t,e,r,i,a,n,s,o){var l,u,d,h,f,v,g=a,p=a,m=t.pid>-1,y=e.pid>-1,b=e.samples.length,T=t.samples.length>0,E=b>1;if((!m||T)&&(!y||E)||this.ISGenerated||s){this.ISGenerated||(d=this.generateIS(t,e,a));var S=this.isVideoContiguous,L=-1;if(E&&(L=function(t){for(var e=0;e0){c.b.warn("[mp4-remuxer]: Dropped "+L+" out of "+b+" video samples due to a missing keyframe");var A=this.getVideoStartPts(e.samples);e.samples=e.samples.slice(L),e.dropped+=L,p+=(e.samples[0].pts-A)/(e.timescale||9e4)}else-1===L&&(c.b.warn("[mp4-remuxer]: No keyframe found out of "+b+" video samples"),v=!1);if(this.ISGenerated){if(T&&E){var R=this.getVideoStartPts(e.samples),D=(dt(t.samples[0].pts,R)-R)/e.inputTimeScale;g+=Math.max(0,D),p+=Math.max(0,-D)}if(T){if(t.samplerate||(c.b.warn("[mp4-remuxer]: regenerate InitSegment as audio detected"),d=this.generateIS(t,e,a)),u=this.remuxAudio(t,g,this.isAudioContiguous,n,y||E||o===rt.b.AUDIO?p:void 0),E){var _=u?u.endPTS-u.startPTS:0;e.inputTimeScale||(c.b.warn("[mp4-remuxer]: regenerate InitSegment as video detected"),d=this.generateIS(t,e,a)),l=this.remuxVideo(e,p,S,_)}}else E&&(l=this.remuxVideo(e,p,S,0));l&&(l.firstKeyFrame=L,l.independent=-1!==L)}}return this.ISGenerated&&(r.samples.length&&(f=this.remuxID3(r,a)),i.samples.length&&(h=this.remuxText(i,a))),{audio:u,video:l,initSegment:d,independent:v,text:h,id3:f}},e.generateIS=function(t,e,r){var i,a,n,o=t.samples,l=e.samples,u=this.typeSupported,d={},h=!Object(s.a)(this._initPTS),f="audio/mp4";if(h&&(i=a=1/0),t.config&&o.length&&(t.timescale=t.samplerate,t.isAAC||(u.mpeg?(f="audio/mpeg",t.codec=""):u.mp3&&(t.codec="mp3")),d.audio={id:"audio",container:f,codec:t.codec,initSegment:!t.isAAC&&u.mpeg?new Uint8Array(0):et.initSegment([t]),metadata:{channelCount:t.channelCount}},h&&(n=t.inputTimeScale,i=a=o[0].pts-Math.round(n*r))),e.sps&&e.pps&&l.length&&(e.timescale=e.inputTimeScale,d.video={id:"main",container:"video/mp4",codec:e.codec,initSegment:et.initSegment([e]),metadata:{width:e.width,height:e.height}},h)){n=e.inputTimeScale;var c=this.getVideoStartPts(l),v=Math.round(n*r);a=Math.min(a,dt(l[0].dts,c)-v),i=Math.min(i,c-v)}if(Object.keys(d).length)return this.ISGenerated=!0,h&&(this._initPTS=i,this._initDTS=a),{tracks:d,initPTS:i,timescale:n}},e.remuxVideo=function(t,e,r,n){var s,o,l,u=t.inputTimeScale,d=t.samples,h=[],f=d.length,v=this._initPTS,g=this.nextAvcDts,p=8,m=Number.POSITIVE_INFINITY,y=Number.NEGATIVE_INFINITY,b=0,T=!1;r&&null!==g||(g=e*u-(d[0].pts-dt(d[0].dts,d[0].pts)));for(var E=0;ES.pts){b=Math.max(Math.min(b,S.pts-S.dts),-18e3)}S.dts0?E-1:E].dts&&(T=!0)}T&&d.sort((function(t,e){var r=t.dts-e.dts,i=t.pts-e.pts;return r||i})),o=d[0].dts,l=d[d.length-1].dts;var L=Math.round((l-o)/(f-1));if(b<0){if(b<-2*L){c.b.warn("PTS < DTS detected in video samples, offsetting DTS from PTS by "+at(-L,!0)+" ms");for(var A=b,R=0;RL;if(k||_<-1){k?c.b.warn("AVC: "+at(_,!0)+" ms ("+_+"dts) hole between fragments detected, filling it"):c.b.warn("AVC: "+at(-_,!0)+" ms ("+_+"dts) overlapping between fragments detected"),o=g;var x=d[0].pts-_;d[0].dts=o,d[0].pts=x,c.b.log("Video: First PTS/DTS adjusted: "+at(x,!0)+"/"+at(o,!0)+", delta: "+at(_,!0)+" ms")}}lt&&(o=Math.max(0,o));for(var w=0,C=0,O=0;O0?j-1:j].dts;if(Q.stretchShortVideoTrack&&null!==this.nextAudioPts){var Z=Math.floor(Q.maxBufferHole*u),J=(n?m+n*u:this.nextAudioPts)-K.pts;J>Z?((s=J-$)<0&&(s=$),c.b.log("[mp4-remuxer]: It is approximately "+J/90+" ms to the next segment; using duration "+s/90+" ms for the last video frame.")):s=$}else s=$}var tt=Math.round(K.pts-K.dts);h.push(new ht(K.key,s,V,tt))}if(h.length&&st&&st<70){var rt=h[0].flags;rt.dependsOn=2,rt.isNonSync=0}this.nextAvcDts=g=l+s,this.isVideoContiguous=!0;var it={data1:et.moof(t.sequenceNumber++,o,nt({},t,{samples:h})),data2:B,startPTS:m/u,endPTS:(y+s)/u,startDTS:o/u,endDTS:g/u,type:"video",hasAudio:!1,hasVideo:!0,nb:h.length,dropped:t.dropped};return t.samples=[],t.dropped=0,it},e.remuxAudio=function(t,e,r,n,s){var o=t.inputTimeScale,l=o/(t.samplerate?t.samplerate:o),u=t.isAAC?1024:1152,d=u*l,h=this._initPTS,f=!t.isAAC&&this.typeSupported.mpeg,v=[],g=t.samples,p=f?0:8,m=this.nextAudioPts||-1,y=e*o;if(this.isAudioContiguous=r=r||g.length&&m>0&&(n&&Math.abs(y-m)<9e3||Math.abs(dt(g[0].pts-h,y)-m)<20*d),g.forEach((function(t){t.pts=dt(t.pts-h,y)})),!r||m<0){if(!(g=g.filter((function(t){return t.pts>=0}))).length)return;m=0===s?0:n?Math.max(0,y):g[0].pts}if(t.isAAC)for(var b=void 0!==s,T=this.config.maxAudioFramesDrift,E=0,S=m;E=T*d&&D<1e4&&b){var _=Math.round(R/d);(S=A-_*d)<0&&(_--,S+=d),0===E&&(this.nextAudioPts=m=S),c.b.warn("[mp4-remuxer]: Injecting "+_+" audio frame @ "+(S/o).toFixed(3)+"s due to "+Math.round(1e3*R/o)+" ms gap.");for(var k=0;k<_;k++){var x=Math.max(S,0),w=Z.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);w||(c.b.log("[mp4-remuxer]: Unable to get silent frame for given audio codec; duplicating last frame instead."),w=L.unit.subarray()),g.splice(E,0,{unit:w,pts:x}),S+=d,E++}}L.pts=S,S+=d}for(var C,O=null,I=null,P=0,F=g.length;F--;)P+=g[F].unit.byteLength;for(var M=0,N=g.length;M0))return;P+=p;try{C=new Uint8Array(P)}catch(t){return void this.observer.emit(i.a.ERROR,i.a.ERROR,{type:a.b.MUX_ERROR,details:a.a.REMUX_ALLOC_ERROR,fatal:!1,bytes:P,reason:"fail allocating audio mdat "+P})}f||(new DataView(C.buffer).setUint32(0,P),C.set(et.types.mdat,4))}C.set(U,p);var j=U.byteLength;p+=j,v.push(new ht(!0,u,j,0)),I=G}var K=v.length;if(K){var H=v[v.length-1];this.nextAudioPts=m=I+l*H.duration;var V=f?new Uint8Array(0):et.moof(t.sequenceNumber++,O/l,nt({},t,{samples:v}));t.samples=[];var W=O/o,Y=m/o,X={data1:V,data2:C,startPTS:W,endPTS:Y,startDTS:W,endDTS:Y,type:"audio",hasAudio:!0,hasVideo:!1,nb:K};return this.isAudioContiguous=!0,X}},e.remuxEmptyAudio=function(t,e,r,i){var a=t.inputTimeScale,n=a/(t.samplerate?t.samplerate:a),s=this.nextAudioPts,o=(null!==s?s:i.startDTS*a)+this._initDTS,l=i.endDTS*a+this._initDTS,u=1024*n,d=Math.ceil((l-o)/u),h=Z.getSilentFrame(t.manifestCodec||t.codec,t.channelCount);if(c.b.warn("[mp4-remuxer]: remux empty Audio"),h){for(var f=[],v=0;v4294967296;)t+=r;return t}var ht=function(t,e,r,i){this.size=void 0,this.duration=void 0,this.cts=void 0,this.flags=void 0,this.duration=e,this.size=r,this.cts=i,this.flags=new ft(t)},ft=function(t){this.isLeading=0,this.isDependedOn=0,this.hasRedundancy=0,this.degradPrio=0,this.dependsOn=1,this.isNonSync=1,this.dependsOn=t?2:1,this.isNonSync=t?0:1},ct=r(5),vt=function(){function t(){this.emitInitSegment=!1,this.audioCodec=void 0,this.videoCodec=void 0,this.initData=void 0,this.initPTS=void 0,this.initTracks=void 0,this.lastEndDTS=null}var e=t.prototype;return e.destroy=function(){},e.resetTimeStamp=function(t){this.initPTS=t,this.lastEndDTS=null},e.resetNextTimestamp=function(){this.lastEndDTS=null},e.resetInitSegment=function(t,e,r){this.audioCodec=e,this.videoCodec=r,this.generateInitSegment(t),this.emitInitSegment=!0},e.generateInitSegment=function(t){var e=this.audioCodec,r=this.videoCodec;if(!t||!t.byteLength)return this.initTracks=void 0,void(this.initData=void 0);var i=this.initData=Object(l.f)(t);e||(e=pt(i.audio,ct.a.AUDIO)),r||(r=pt(i.video,ct.a.VIDEO));var a={};i.audio&&i.video?a.audiovideo={container:"video/mp4",codec:e+","+r,initSegment:t,id:"main"}:i.audio?a.audio={container:"audio/mp4",codec:e,initSegment:t,id:"audio"}:i.video?a.video={container:"video/mp4",codec:r,initSegment:t,id:"main"}:c.b.warn("[passthrough-remuxer.ts]: initSegment does not contain moov or trak boxes."),this.initTracks=a},e.remux=function(t,e,r,i,a){var n=this.initPTS,o=this.lastEndDTS,u={audio:void 0,video:void 0,text:i,id3:r,initSegment:void 0};Object(s.a)(o)||(o=this.lastEndDTS=a||0);var d=e.samples;if(!d||!d.length)return u;var h={initPTS:void 0,timescale:1},f=this.initData;if(f&&f.length||(this.generateInitSegment(d),f=this.initData),!f||!f.length)return c.b.warn("[passthrough-remuxer.ts]: Failed to generate initSegment."),u;this.emitInitSegment&&(h.tracks=this.initTracks,this.emitInitSegment=!1),Object(s.a)(n)||(this.initPTS=h.initPTS=n=gt(f,d,o));var v=Object(l.c)(d,f),g=o,p=v+g;Object(l.e)(f,d,n),v>0?this.lastEndDTS=p:(c.b.warn("Duration parsed from mp4 should be greater than zero"),this.resetNextTimestamp());var m=!!f.audio,y=!!f.video,b="";m&&(b+="audio"),y&&(b+="video");var T={data1:d,startPTS:g,startDTS:g,endPTS:p,endDTS:p,type:b,hasAudio:m,hasVideo:y,nb:1,dropped:0};return u.audio="audio"===T.type?T:void 0,u.video="audio"!==T.type?T:void 0,u.text=i,u.id3=r,u.initSegment=h,u},t}(),gt=function(t,e,r){return Object(l.d)(t,e)-r};function pt(t,e){var r=null==t?void 0:t.codec;return r&&r.length>4?r:"hvc1"===r?"hvc1.1.c.L120.90":"av01"===r?"av01.0.04M.08":"avc1"===r||e===ct.a.VIDEO?"avc1.42e01e":"mp4a.40.5"}var mt,yt=vt,bt=r(16);try{mt=self.performance.now.bind(self.performance)}catch(t){c.b.debug("Unable to use Performance API on this environment"),mt=self.Date.now}var Tt=[{demux:q,remux:ut},{demux:D,remux:yt},{demux:A,remux:ut},{demux:$,remux:ut}],Et=1024;Tt.forEach((function(t){var e=t.demux;Et=Math.max(Et,e.minProbeByteLength)}));var St=function(){function t(t,e,r,i,a){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.cache=new bt.a,this.observer=t,this.typeSupported=e,this.config=r,this.vendor=i,this.id=a}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 a=this,n=r.transmuxing;n.executeStart=mt();var s=new Uint8Array(t),o=this.cache,u=this.config,d=this.currentTransmuxState,h=this.transmuxConfig;i&&(this.currentTransmuxState=i);var f=function(t,e){var r=null;t.byteLength>0&&null!=e&&null!=e.key&&null!==e.iv&&null!=e.method&&(r=e);return r}(s,e);if(f&&"AES-128"===f.method){var c=this.getDecrypter();if(!u.enableSoftwareAES)return this.decryptionPromise=c.webCryptoDecrypt(s,f.key.buffer,f.iv.buffer).then((function(t){var e=a.push(t,null,r);return a.decryptionPromise=null,e})),this.decryptionPromise;var v=c.softwareDecrypt(s,f.key.buffer,f.iv.buffer);if(!v)return n.executeEnd=mt(),Lt(r);s=new Uint8Array(v)}var g=i||d,p=g.contiguous,m=g.discontinuity,y=g.trackSwitch,b=g.accurateTimeOffset,T=g.timeOffset,E=h.audioCodec,S=h.videoCodec,L=h.defaultInitPts,A=h.duration,R=h.initSegmentData;if((m||y)&&this.resetInitSegment(R,E,S,A),m&&this.resetInitialTimestamp(L),p||this.resetContiguity(),this.needsProbing(s,m,y)){if(o.dataLength){var D=o.flush();s=Object(l.a)(D,s)}this.configureTransmuxer(s,h)}var _=this.transmux(s,f,T,b,r),k=this.currentTransmuxState;return k.contiguous=!0,k.discontinuity=!1,k.trackSwitch=!1,n.executeEnd=mt(),_},e.flush=function(t){var e=this,r=t.transmuxing;r.executeStart=mt();var n=this.decrypter,s=this.cache,o=this.currentTransmuxState,l=this.decryptionPromise;if(l)return l.then((function(){return e.flush(t)}));var u=[],d=o.timeOffset;if(n){var h=n.flush();h&&u.push(this.push(h,null,t))}var f=s.dataLength;s.reset();var c=this.demuxer,v=this.remuxer;if(!c||!v)return f>=Et&&this.observer.emit(i.a.ERROR,i.a.ERROR,{type:a.b.MEDIA_ERROR,details:a.a.FRAG_PARSING_ERROR,fatal:!0,reason:"no demux matching with content found"}),r.executeEnd=mt(),[Lt(t)];var g=c.flush(d);return At(g)?g.then((function(r){return e.flushRemux(u,r,t),u})):(this.flushRemux(u,g,t),u)},e.flushRemux=function(t,e,r){var i=e.audioTrack,a=e.avcTrack,n=e.id3Track,s=e.textTrack,o=this.currentTransmuxState,l=o.accurateTimeOffset,u=o.timeOffset;c.b.log("[transmuxer.ts]: Flushed fragment "+r.sn+(r.part>-1?" p: "+r.part:"")+" of level "+r.level);var d=this.remuxer.remux(i,a,n,s,u,l,!0,this.id);t.push({remuxResult:d,chunkMeta:r}),r.transmuxing.executeEnd=mt()},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){var a=this.demuxer,n=this.remuxer;a&&n&&(a.resetInitSegment(e,r,i),n.resetInitSegment(t,e,r))},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,a){return e&&"SAMPLE-AES"===e.method?this.transmuxSampleAes(t,e,r,i,a):this.transmuxUnencrypted(t,r,i,a)},e.transmuxUnencrypted=function(t,e,r,i){var a=this.demuxer.demux(t,e,!1,!this.config.progressive),n=a.audioTrack,s=a.avcTrack,o=a.id3Track,l=a.textTrack;return{remuxResult:this.remuxer.remux(n,s,o,l,e,r,!1,this.id),chunkMeta:i}},e.transmuxSampleAes=function(t,e,r,i,a){var n=this;return this.demuxer.demuxSampleAes(t,e,r).then((function(t){return{remuxResult:n.remuxer.remux(t.audioTrack,t.avcTrack,t.id3Track,t.textTrack,r,i,!1,n.id),chunkMeta:a}}))},e.configureTransmuxer=function(t,e){for(var r,i=this.config,a=this.observer,n=this.typeSupported,s=this.vendor,o=e.audioCodec,l=e.defaultInitPts,u=e.duration,d=e.initSegmentData,h=e.videoCodec,f=0,v=Tt.length;f>>8^255&p^99,t[c]=p,e[p]=c;var m=f[c],y=f[m],b=f[y],T=257*f[p]^16843008*p;i[c]=T<<24|T>>>8,a[c]=T<<16|T>>>16,n[c]=T<<8|T>>>24,s[c]=T,T=16843009*b^65537*y^257*m^16843008*c,l[p]=T<<24|T>>>8,u[p]=T<<16|T>>>16,d[p]=T<<8|T>>>24,h[p]=T,c?(c=m^f[f[f[b^m]]],v^=f[f[v]]):c=v=1}},e.expandKey=function(t){for(var e=this.uint8ArrayToUint32Array_(t),r=!0,i=0;i0}),!1)}t.exports=function(t,e){e=e||{};var a={main:r.m},o=e.all?{main:Object.keys(a.main)}:function(t,e){for(var r={main:[e]},i={main:[]},a={main:{}};s(r);)for(var o=Object.keys(r),l=0;lt.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},e=t,(r=[{key:"hasProgramDateTime",get:function(){return!!this.fragments.length&&Object(a.a)(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)&&void 0!==t&&t.length?this.partList[this.partList.length-1].end:this.fragmentEnd}},{key:"fragmentEnd",get:function(){var t;return null!==(t=this.fragments)&&void 0!==t&&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)&&void 0!==t&&t.length?this.partList[this.partList.length-1].index:-1}},{key:"lastPartSn",get:function(){var t;return null!==(t=this.partList)&&void 0!==t&&t.length?this.partList[this.partList.length-1].fragment.sn:this.endSN}}])&&d(e.prototype,r),i&&d(e,i),t}(),f=r(15),c=/^(\d+)x(\d+)$/,v=/\s*(.+?)\s*=((?:\".*?\")|.*?)(?:,|$)/g,g=function(){function t(e){for(var r in"string"==typeof e&&(e=t.parseAttrList(e)),e)e.hasOwnProperty(r)&&(this[r]=e[r])}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(v.lastIndex=0;null!==(e=v.exec(t));){var i=e[2];0===i.indexOf('"')&&i.lastIndexOf('"')===i.length-1&&(i=i.slice(1,-1)),r[e[1]]=i}return r},t}(),p={audio:{a3ds:!0,"ac-3":!0,"ac-4":!0,alac:!0,alaw:!0,dra1:!0,"dts+":!0,"dts-":!0,dtsc:!0,dtse:!0,dtsh:!0,"ec-3":!0,enca:!0,g719:!0,g726:!0,m4ae:!0,mha1:!0,mha2:!0,mhm1:!0,mhm2:!0,mlpa:!0,mp4a:!0,"raw ":!0,Opus:!0,samr:!0,sawb:!0,sawp:!0,sevc:!0,sqcp:!0,ssmv:!0,twos:!0,ulaw:!0},video:{avc1:!0,avc2:!0,avc3:!0,avc4:!0,avcp:!0,av01:!0,drac:!0,dvav:!0,dvhe:!0,encv:!0,hev1:!0,hvc1:!0,mjp2:!0,mp4v:!0,mvc1:!0,mvc2:!0,mvc3:!0,mvc4:!0,resv:!0,rv60:!0,s263:!0,svc1:!0,svc2:!0,"vc-1":!0,vp08:!0,vp09:!0},text:{stpp:!0,wvtt:!0}};function m(t,e){return MediaSource.isTypeSupported((e||"video")+'/mp4;codecs="'+t+'"')}var y=/#EXT-X-STREAM-INF:([^\r\n]*)(?:[\r\n](?:#[^\r\n]*)?)*([^\r\n]+)|#EXT-X-SESSION-DATA:([^\r\n]*)[\r\n]+/g,b=/#EXT-X-MEDIA:(.*)/g,T=new RegExp([/#EXTINF:\s*(\d*(?:\.\d+)?)(?:,(.*)\s+)?/.source,/(?!#) *(\S[\S ]*)/.source,/#EXT-X-BYTERANGE:*(.+)/.source,/#EXT-X-PROGRAM-DATE-TIME:(.+)/.source,/#.*/.source].join("|"),"g"),E=new RegExp([/#(EXTM3U)/.source,/#EXT-X-(PLAYLIST-TYPE):(.+)/.source,/#EXT-X-(MEDIA-SEQUENCE): *(\d+)/.source,/#EXT-X-(SKIP):(.+)/.source,/#EXT-X-(TARGETDURATION): *(\d+)/.source,/#EXT-X-(KEY):(.+)/.source,/#EXT-X-(START):(.+)/.source,/#EXT-X-(ENDLIST)/.source,/#EXT-X-(DISCONTINUITY-SEQ)UENCE: *(\d+)/.source,/#EXT-X-(DIS)CONTINUITY/.source,/#EXT-X-(VERSION):(\d+)/.source,/#EXT-X-(MAP):(.+)/.source,/#EXT-X-(SERVER-CONTROL):(.+)/.source,/#EXT-X-(PART-INF):(.+)/.source,/#EXT-X-(GAP)/.source,/#EXT-X-(BITRATE):\s*(\d+)/.source,/#EXT-X-(PART):(.+)/.source,/#EXT-X-(PRELOAD-HINT):(.+)/.source,/#EXT-X-(RENDITION-REPORT):(.+)/.source,/(#)([^:]*):(.*)/.source,/(#)(.*)(?:.*)\r?\n?/.source].join("|")),S=/\.(mp4|m4s|m4v|m4a)$/i;var L=function(){function t(){}return t.findGroup=function(t,e){for(var r=0;r2){var r=e.shift()+".";return r+=parseInt(e.shift()).toString(16),r+=("000"+parseInt(e.shift()).toString(16)).substr(-4)}return t},t.resolve=function(t,e){return i.buildAbsoluteURL(e,t,{alwaysNormalize:!0})},t.parseMasterPlaylist=function(e,r){var i,a=[],n={},s=!1;for(y.lastIndex=0;null!=(i=y.exec(e));)if(i[1]){var o=new g(i[1]),l={attrs:o,bitrate:o.decimalInteger("AVERAGE-BANDWIDTH")||o.decimalInteger("BANDWIDTH"),name:o.NAME,url:t.resolve(i[2],r)},u=o.decimalResolution("RESOLUTION");u&&(l.width=u.width,l.height=u.height),A((o.CODECS||"").split(/[ ,]+/).filter((function(t){return t})),l),l.videoCodec&&-1!==l.videoCodec.indexOf("avc1")&&(l.videoCodec=t.convertAVC1ToAVCOTI(l.videoCodec)),a.push(l)}else if(i[3]){var d=new g(i[3]);d["DATA-ID"]&&(s=!0,n[d["DATA-ID"]]=d)}return{levels:a,sessionData:s?n:null}},t.parseMasterPlaylistMedia=function(e,r,i,a){var n;void 0===a&&(a=[]);var s=[],o=0;for(b.lastIndex=0;null!==(n=b.exec(e));){var l=new g(n[1]);if(l.TYPE===i){var u={attrs:l,bitrate:0,id:o++,groupId:l["GROUP-ID"],instreamId:l["INSTREAM-ID"],name:l.NAME||l.LANGUAGE||"",type:i,default:l.bool("DEFAULT"),autoselect:l.bool("AUTOSELECT"),forced:l.bool("FORCED"),lang:l.LANGUAGE,url:l.URI?t.resolve(l.URI,r):""};if(a.length){var d=t.findGroup(a,u.groupId)||a[0];R(u,d,"audioCodec"),R(u,d,"textCodec")}s.push(u)}}return s},t.parseLevelPlaylist=function(t,e,r,n,s){var l,d,c,v=new h(e),p=v.fragments,m=null,y=0,b=0,L=0,A=0,R=null,_=new u.b(n,e),k=-1,x=!1;for(T.lastIndex=0,v.m3u8=t;null!==(l=T.exec(t));){x&&(x=!1,(_=new u.b(n,e)).start=L,_.sn=y,_.cc=A,_.level=r,m&&(_.initSegment=m,_.rawProgramDateTime=m.rawProgramDateTime));var w=l[1];if(w){_.duration=parseFloat(w);var C=(" "+l[2]).slice(1);_.title=C||null,_.tagList.push(C?["INF",w,C]:["INF",w])}else if(l[3])Object(a.a)(_.duration)&&(_.start=L,c&&(_.levelkey=c),_.sn=y,_.level=r,_.cc=A,_.urlId=s,p.push(_),_.relurl=(" "+l[3]).slice(1),D(_,R),R=_,L+=_.duration,y++,b=0,x=!0);else if(l[4]){var O=(" "+l[4]).slice(1);R?_.setByteRange(O,R):_.setByteRange(O)}else if(l[5])_.rawProgramDateTime=(" "+l[5]).slice(1),_.tagList.push(["PROGRAM-DATE-TIME",_.rawProgramDateTime]),-1===k&&(k=p.length);else{if(!(l=l[0].match(E))){o.b.warn("No matches on slow regex match for level playlist!");continue}for(d=1;d-1){o.b.warn("Keyformat "+X+" is not supported from the manifest");continue}if("identity"!==X)continue;K&&(c=f.a.fromURL(e,H),H&&["AES-128","SAMPLE-AES","SAMPLE-AES-CENC"].indexOf(K)>=0&&(c.method=K,c.keyFormat=X,Y&&(c.keyID=Y),W&&(c.keyFormatVersions=W),c.iv=V));break;case"START":var q=new g(P).decimalFloatingPoint("TIME-OFFSET");Object(a.a)(q)&&(v.startTimeOffset=q);break;case"MAP":var z=new g(P);_.relurl=z.URI,z.BYTERANGE&&_.setByteRange(z.BYTERANGE),_.level=r,_.sn="initSegment",c&&(_.levelkey=c),_.initSegment=null,m=_,x=!0;break;case"SERVER-CONTROL":var Q=new g(P);v.canBlockReload=Q.bool("CAN-BLOCK-RELOAD"),v.canSkipUntil=Q.optionalFloat("CAN-SKIP-UNTIL",0),v.canSkipDateRanges=v.canSkipUntil>0&&Q.bool("CAN-SKIP-DATERANGES"),v.partHoldBack=Q.optionalFloat("PART-HOLD-BACK",0),v.holdBack=Q.optionalFloat("HOLD-BACK",0);break;case"PART-INF":var $=new g(P);v.partTarget=$.decimalFloatingPoint("PART-TARGET");break;case"PART":var Z=v.partList;Z||(Z=v.partList=[]);var J=b>0?Z[Z.length-1]:void 0,tt=b++,et=new u.c(new g(P),_,e,tt,J);Z.push(et),_.duration+=et.duration;break;case"PRELOAD-HINT":var rt=new g(P);v.preloadHint=rt;break;case"RENDITION-REPORT":var it=new g(P);v.renditionReports=v.renditionReports||[],v.renditionReports.push(it);break;default:o.b.warn("line parsed but not handled: "+l)}}}R&&!R.relurl?(p.pop(),L-=R.duration,v.partList&&(v.fragmentHint=R)):v.partList&&(D(_,R),_.cc=A,v.fragmentHint=_);var at=p.length,nt=p[0],st=p[at-1];if((L+=v.skippedSegments*v.targetduration)>0&&at&&st){v.averagetargetduration=L/at;var ot=st.sn;v.endSN="initSegment"!==ot?ot:0,nt&&(v.startCC=nt.cc,nt.initSegment||v.fragments.every((function(t){return t.relurl&&(e=t.relurl,S.test(null!=(r=null===(a=i.parseURL(e))||void 0===a?void 0:a.path)?r:""));var e,r,a}))&&(o.b.warn("MP4 fragments found but no init segment (probably no MAP, incomplete M3U8), trying to fetch SIDX"),(_=new u.b(n,e)).relurl=st.relurl,_.level=r,_.sn="initSegment",nt.initSegment=_,v.needSidxRanges=!0))}else v.endSN=0,v.startCC=0;return v.fragmentHint&&(L+=v.fragmentHint.duration),v.totalduration=L,v.endCC=A,k>0&&function(t,e){for(var r=t[e],i=e;i--;){var a=t[i];if(!a)return;a.programDateTime=r.programDateTime-1e3*a.duration,r=a}}(p,k),v},t}();function A(t,e){["video","audio","text"].forEach((function(r){var i=t.filter((function(t){return function(t,e){var r=p[e];return!!r&&!0===r[t.slice(0,4)]}(t,r)}));if(i.length){var a=i.filter((function(t){return 0===t.lastIndexOf("avc1",0)||0===t.lastIndexOf("mp4a",0)}));e[r+"Codec"]=a.length>0?a[0]:i[0],t=t.filter((function(t){return-1===i.indexOf(t)}))}})),e.unknownCodecs=t}function R(t,e,r){var i=e[r];i&&(t[r]=i)}function D(t,e){t.rawProgramDateTime?t.programDateTime=Date.parse(t.rawProgramDateTime):null!=e&&e.programDateTime&&(t.programDateTime=e.endProgramDateTime),Object(a.a)(t.programDateTime)||(t.programDateTime=null,t.rawProgramDateTime=null)}var _=r(4);function k(t,e){var r=t.url;return void 0!==r&&0!==r.indexOf("data:")||(r=e.url),r}var x=function(){function t(t){this.hls=void 0,this.loaders=Object.create(null),this.hls=t,this.registerListeners()}var e=t.prototype;return e.registerListeners=function(){var t=this.hls;t.on(n.a.MANIFEST_LOADING,this.onManifestLoading,this),t.on(n.a.LEVEL_LOADING,this.onLevelLoading,this),t.on(n.a.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.on(n.a.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.unregisterListeners=function(){var t=this.hls;t.off(n.a.MANIFEST_LOADING,this.onManifestLoading,this),t.off(n.a.LEVEL_LOADING,this.onLevelLoading,this),t.off(n.a.AUDIO_TRACK_LOADING,this.onAudioTrackLoading,this),t.off(n.a.SUBTITLE_TRACK_LOADING,this.onSubtitleTrackLoading,this)},e.createInternalLoader=function(t){var e=this.hls.config,r=e.pLoader,i=e.loader,a=new(r||i)(e);return t.loader=a,this.loaders[t.type]=a,a},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.unregisterListeners(),this.destroyInternalLoaders()},e.onManifestLoading=function(t,e){var r=e.url;this.load({id:null,groupId:null,level:0,responseType:"text",type:_.a.MANIFEST,url:r,deliveryDirectives:null})},e.onLevelLoading=function(t,e){var r=e.id,i=e.level,a=e.url,n=e.deliveryDirectives;this.load({id:r,groupId:null,level:i,responseType:"text",type:_.a.LEVEL,url:a,deliveryDirectives:n})},e.onAudioTrackLoading=function(t,e){var r=e.id,i=e.groupId,a=e.url,n=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:_.a.AUDIO_TRACK,url:a,deliveryDirectives:n})},e.onSubtitleTrackLoading=function(t,e){var r=e.id,i=e.groupId,a=e.url,n=e.deliveryDirectives;this.load({id:r,groupId:i,level:null,responseType:"text",type:_.a.SUBTITLE_TRACK,url:a,deliveryDirectives:n})},e.load=function(t){var e,r,i,a,n,s,l=this.hls.config,u=this.getInternalLoader(t);if(u){var d=u.context;if(d&&d.url===t.url)return void o.b.trace("[playlist-loader]: playlist request ongoing");o.b.log("[playlist-loader]: aborting previous loader for type: "+t.type),u.abort()}switch(t.type){case _.a.MANIFEST:r=l.manifestLoadingMaxRetry,i=l.manifestLoadingTimeOut,a=l.manifestLoadingRetryDelay,n=l.manifestLoadingMaxRetryTimeout;break;case _.a.LEVEL:case _.a.AUDIO_TRACK:case _.a.SUBTITLE_TRACK:r=0,i=l.levelLoadingTimeOut;break;default:r=l.levelLoadingMaxRetry,i=l.levelLoadingTimeOut,a=l.levelLoadingRetryDelay,n=l.levelLoadingMaxRetryTimeout}if((u=this.createInternalLoader(t),null!==(e=t.deliveryDirectives)&&void 0!==e&&e.part)&&(t.type===_.a.LEVEL&&null!==t.level?s=this.hls.levels[t.level].details:t.type===_.a.AUDIO_TRACK&&null!==t.id?s=this.hls.audioTracks[t.id].details:t.type===_.a.SUBTITLE_TRACK&&null!==t.id&&(s=this.hls.subtitleTracks[t.id].details),s)){var h=s.partTarget,f=s.targetduration;h&&f&&(i=Math.min(1e3*Math.max(3*h,.8*f),i))}var c={timeout:i,maxRetry:r,retryDelay:a,maxRetryDelay:n,highWaterMark:0},v={onSuccess:this.loadsuccess.bind(this),onError:this.loaderror.bind(this),onTimeout:this.loadtimeout.bind(this)};u.load(t,c,v)},e.loadsuccess=function(t,e,r,i){if(void 0===i&&(i=null),r.isSidxRequest)return this.handleSidxRequest(t,r),void this.handlePlaylistLoaded(t,e,r,i);this.resetInternalLoader(r.type);var a=t.data;0===a.indexOf("#EXTM3U")?(e.parsing.start=performance.now(),a.indexOf("#EXTINF:")>0||a.indexOf("#EXT-X-TARGETDURATION:")>0?this.handleTrackOrLevelPlaylist(t,e,r,i):this.handleMasterPlaylist(t,e,r,i)):this.handleManifestParsingError(t,r,"no EXTM3U delimiter",i)},e.loaderror=function(t,e,r){void 0===r&&(r=null),this.handleNetworkError(e,r,!1,t)},e.loadtimeout=function(t,e,r){void 0===r&&(r=null),this.handleNetworkError(e,r,!0)},e.handleMasterPlaylist=function(t,e,r,i){var a=this.hls,s=t.data,l=k(t,r),u=L.parseMasterPlaylist(s,l),d=u.levels,h=u.sessionData;if(d.length){var f=d.map((function(t){return{id:t.attrs.AUDIO,audioCodec:t.audioCodec}})),c=d.map((function(t){return{id:t.attrs.SUBTITLES,textCodec:t.textCodec}})),v=L.parseMasterPlaylistMedia(s,l,"AUDIO",f),p=L.parseMasterPlaylistMedia(s,l,"SUBTITLES",c),m=L.parseMasterPlaylistMedia(s,l,"CLOSED-CAPTIONS");if(v.length)v.some((function(t){return!t.url}))||!d[0].audioCodec||d[0].attrs.AUDIO||(o.b.log("[playlist-loader]: audio codec signaled in quality level, but no embedded audio track signaled, create one"),v.unshift({type:"main",name:"main",default:!1,autoselect:!1,forced:!1,id:-1,attrs:new g({}),bitrate:0,url:""}));a.trigger(n.a.MANIFEST_LOADED,{levels:d,audioTracks:v,subtitles:p,captions:m,url:l,stats:e,networkDetails:i,sessionData:h})}else this.handleManifestParsingError(t,r,"no level found in manifest",i)},e.handleTrackOrLevelPlaylist=function(t,e,r,i){var o=this.hls,l=r.id,u=r.level,d=r.type,h=k(t,r),f=Object(a.a)(l)?l:0,c=Object(a.a)(u)?u:f,v=function(t){switch(t.type){case _.a.AUDIO_TRACK:return _.b.AUDIO;case _.a.SUBTITLE_TRACK:return _.b.SUBTITLE;default:return _.b.MAIN}}(r),p=L.parseLevelPlaylist(t.data,h,c,v,f);if(p.fragments.length){if(d===_.a.MANIFEST){var m={attrs:new g({}),bitrate:0,details:p,name:"",url:h};o.trigger(n.a.MANIFEST_LOADED,{levels:[m],audioTracks:[],url:h,stats:e,networkDetails:i,sessionData:null})}if(e.parsing.end=performance.now(),p.needSidxRanges){var y,b=null===(y=p.fragments[0].initSegment)||void 0===y?void 0:y.url;this.load({url:b,isSidxRequest:!0,type:d,level:u,levelDetails:p,id:l,groupId:null,rangeStart:0,rangeEnd:2048,responseType:"arraybuffer",deliveryDirectives:null})}else r.levelDetails=p,this.handlePlaylistLoaded(t,e,r,i)}else o.trigger(n.a.ERROR,{type:s.b.NETWORK_ERROR,details:s.a.LEVEL_EMPTY_ERROR,fatal:!1,url:h,reason:"no fragments found in level",level:"number"==typeof r.level?r.level:void 0})},e.handleSidxRequest=function(t,e){var r=Object(l.g)(new Uint8Array(t.data));if(r){var i=r.references,a=e.levelDetails;i.forEach((function(t,e){var i=t.info,n=a.fragments[e];0===n.byteRange.length&&n.setByteRange(String(1+i.end-i.start)+"@"+String(i.start)),n.initSegment&&n.initSegment.setByteRange(String(r.moovEndOffset)+"@0")}))}},e.handleManifestParsingError=function(t,e,r,i){this.hls.trigger(n.a.ERROR,{type:s.b.NETWORK_ERROR,details:s.a.MANIFEST_PARSING_ERROR,fatal:e.type===_.a.MANIFEST,url:t.url,reason:r,response:t,context:e,networkDetails:i})},e.handleNetworkError=function(t,e,r,i){void 0===r&&(r=!1),o.b.warn("[playlist-loader]: A network "+(r?"timeout":"error")+" occurred while loading "+t.type+" level: "+t.level+" id: "+t.id+' group-id: "'+t.groupId+'"');var a=s.a.UNKNOWN,l=!1,u=this.getInternalLoader(t);switch(t.type){case _.a.MANIFEST:a=r?s.a.MANIFEST_LOAD_TIMEOUT:s.a.MANIFEST_LOAD_ERROR,l=!0;break;case _.a.LEVEL:a=r?s.a.LEVEL_LOAD_TIMEOUT:s.a.LEVEL_LOAD_ERROR,l=!1;break;case _.a.AUDIO_TRACK:a=r?s.a.AUDIO_TRACK_LOAD_TIMEOUT:s.a.AUDIO_TRACK_LOAD_ERROR,l=!1;break;case _.a.SUBTITLE_TRACK:a=r?s.a.SUBTITLE_TRACK_LOAD_TIMEOUT:s.a.SUBTITLE_LOAD_ERROR,l=!1}u&&this.resetInternalLoader(t.type);var d={type:s.b.NETWORK_ERROR,details:a,fatal:l,url:t.url,loader:u,context:t,networkDetails:e};i&&(d.response=i),this.hls.trigger(n.a.ERROR,d)},e.handlePlaylistLoaded=function(t,e,r,i){var a=r.type,s=r.level,o=r.id,l=r.groupId,u=r.loader,d=r.levelDetails,h=r.deliveryDirectives;if(null!=d&&d.targetduration){if(u)switch(d.live&&(u.getCacheAge&&(d.ageHeader=u.getCacheAge()||0),u.getCacheAge&&!isNaN(d.ageHeader)||(d.ageHeader=0)),a){case _.a.MANIFEST:case _.a.LEVEL:this.hls.trigger(n.a.LEVEL_LOADED,{details:d,level:s||0,id:o||0,stats:e,networkDetails:i,deliveryDirectives:h});break;case _.a.AUDIO_TRACK:this.hls.trigger(n.a.AUDIO_TRACK_LOADED,{details:d,id:o||0,groupId:l||"",stats:e,networkDetails:i,deliveryDirectives:h});break;case _.a.SUBTITLE_TRACK:this.hls.trigger(n.a.SUBTITLE_TRACK_LOADED,{details:d,id:o||0,groupId:l||"",stats:e,networkDetails:i,deliveryDirectives:h})}}else this.handleManifestParsingError(t,r,"invalid target duration",i)},t}(),w=function(){function t(t){this.hls=void 0,this.loaders={},this.decryptkey=null,this.decrypturl=null,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){this.hls.on(n.a.KEY_LOADING,this.onKeyLoading,this)},e._unregisterListeners=function(){this.hls.off(n.a.KEY_LOADING,this.onKeyLoading)},e.destroy=function(){for(var t in this._unregisterListeners(),this.loaders){var e=this.loaders[t];e&&e.destroy()}this.loaders={}},e.onKeyLoading=function(t,e){var r=e.frag,i=r.type,a=this.loaders[i];if(r.decryptdata){var s=r.decryptdata.uri;if(s!==this.decrypturl||null===this.decryptkey){var l=this.hls.config;if(a&&(o.b.warn("abort previous key loader for type:"+i),a.abort()),!s)return void o.b.warn("key uri is falsy");var u=l.loader,d=r.loader=this.loaders[i]=new u(l);this.decrypturl=s,this.decryptkey=null;var h={url:s,frag:r,responseType:"arraybuffer"},f={timeout:l.fragLoadingTimeOut,maxRetry:0,retryDelay:l.fragLoadingRetryDelay,maxRetryDelay:l.fragLoadingMaxRetryTimeout,highWaterMark:0},c={onSuccess:this.loadsuccess.bind(this),onError:this.loaderror.bind(this),onTimeout:this.loadtimeout.bind(this)};d.load(h,f,c)}else this.decryptkey&&(r.decryptdata.key=this.decryptkey,this.hls.trigger(n.a.KEY_LOADED,{frag:r}))}else o.b.warn("Missing decryption data on fragment in onKeyLoading")},e.loadsuccess=function(t,e,r){var i=r.frag;i.decryptdata?(this.decryptkey=i.decryptdata.key=new Uint8Array(t.data),i.loader=null,delete this.loaders[i.type],this.hls.trigger(n.a.KEY_LOADED,{frag:i})):o.b.error("after key load, decryptdata unset")},e.loaderror=function(t,e){var r=e.frag,i=r.loader;i&&i.abort(),delete this.loaders[r.type],this.hls.trigger(n.a.ERROR,{type:s.b.NETWORK_ERROR,details:s.a.KEY_LOAD_ERROR,fatal:!1,frag:r,response:t})},e.loadtimeout=function(t,e){var r=e.frag,i=r.loader;i&&i.abort(),delete this.loaders[r.type],this.hls.trigger(n.a.ERROR,{type:s.b.NETWORK_ERROR,details:s.a.KEY_LOAD_TIMEOUT,fatal:!1,frag:r})},t}();function C(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 O(t,e,r){var i=t.mode;if("disabled"===i&&(t.mode="hidden"),t.cues&&t.cues.length>0)for(var a=function(t,e,r){var i=[],a=function(t,e){if(et[r].endTime)return-1;var i=0,a=r;for(;i<=a;){var n=Math.floor((a+i)/2);if(et[n].startTime&&i-1)for(var n=a,s=t.length;n=e&&o.endTime<=r)i.push(o);else if(o.startTime>r)return i}return i}(t.cues,e,r),n=0;n.05&&this.forwardBufferLength>1){var u=Math.min(2,Math.max(1,n)),d=Math.round(2/(1+Math.exp(-.75*o-this.edgeStalled))*20)/20;t.playbackRate=Math.min(u,Math.max(1,d))}else 1!==t.playbackRate&&0!==t.playbackRate&&(t.playbackRate=1)}}}}},a.estimateLiveEdge=function(){var t=this.levelDetails;return null===t?null:t.edge+t.age},a.computeLatency=function(){var t=this.estimateLiveEdge();return null===t?null:t-this.currentTime},e=t,(r=[{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,a=this.config,n=a.liveSyncDuration,s=a.liveSyncDurationCount,o=a.lowLatencyMode,l=this.hls.userConfig,u=o&&r||e;(l.liveSyncDuration||l.liveSyncDurationCount||0===u)&&(u=void 0!==n?n:s*i);var d=i;return u+Math.min(1*this.stallCount,d)}},{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,a=t-e-this.edgeStalled,n=i-r.totalduration,s=i-(this.config.lowLatencyMode&&r.partTarget||r.targetduration);return Math.min(Math.max(n,a),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}}])&&F(e.prototype,r),i&&F(e,i),t}();function B(t,e){for(var r=0;rt.sn?(n=r-t.start,i=t):(n=t.start-r,i=e),i.duration!==n&&(i.duration=n)}else if(e.sn>t.sn){t.cc===e.cc&&t.minEndPTS?e.start=t.start+(t.minEndPTS-t.start):e.start=t.start+t.duration}else e.start=Math.max(t.start-e.duration,0)}function V(t,e,r,i,n,s){i-r<=0&&(o.b.warn("Fragment should have a positive duration",e),i=r+e.duration,s=n+e.duration);var l=r,u=i,d=e.startPTS,h=e.endPTS;if(Object(a.a)(d)){var f=Math.abs(d-r);Object(a.a)(e.deltaPTS)?e.deltaPTS=Math.max(f,e.deltaPTS):e.deltaPTS=f,l=Math.max(r,d),r=Math.min(r,d),n=Math.min(n,e.startDTS),u=Math.min(i,h),i=Math.max(i,h),s=Math.max(s,e.endDTS)}e.duration=i-r;var c=r-e.start;e.appendedPTS=i,e.start=e.startPTS=r,e.maxStartPTS=l,e.startDTS=n,e.endPTS=i,e.minEndPTS=u,e.endDTS=s;var v,g=e.sn;if(!t||gt.endSN)return 0;var p=g-t.startSN,m=t.fragments;for(m[p]=e,v=p;v>0;v--)H(m[v],m[v-1]);for(v=p;v=0;n--){var s=i[n].initSegment;if(s){r=s;break}}t.fragmentHint&&delete t.fragmentHint.endPTS;var l,u=0;if(function(t,e,r){for(var i=e.skippedSegments,a=Math.max(t.startSN,e.startSN)-e.startSN,n=(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=a;u<=n;u++){var d=l[s+u],h=o[u];i&&!h&&u=i.length||function(t,e){if(e){for(var r=t.fragments,i=t.skippedSegments;ie.partTarget&&(l+=1)}if(Object(a.a)(o))return new U(o,Object(a.a)(l)?l:void 0,M.No)}}},e.loadPlaylist=function(t){},e.shouldLoadTrack=function(t){return this.canLoad&&t&&!!t.url&&(!t.details||t.details.live)},e.playlistLoaded=function(t,e,r){var i=this,a=e.details,n=e.stats,s=n.loading.end?Math.max(0,self.performance.now()-n.loading.end):0;if(a.advancedDateTime=Date.now()-s,a.live||null!=r&&r.live){if(a.reloaded(r),r&&this.log("live playlist "+t+" "+(a.advanced?"REFRESHED "+a.lastPartSn+"-"+a.lastPartIndex:"MISSED")),r&&a.fragments.length>0&&W(r,a),!this.canLoad||!a.live)return;var o,l=void 0,u=void 0;if(a.canBlockReload&&a.endSN&&a.advanced){var d=this.hls.config.lowLatencyMode,h=a.lastPartSn,f=a.endSN,c=a.lastPartIndex,v=h===f;-1!==c?(l=v?f+1:h,u=v?d?0:c:c+1):l=f+1;var g=a.age,p=g+a.ageHeader,m=Math.min(p-a.partTarget,1.5*a.targetduration);if(m>0){if(r&&m>r.tuneInGoal)this.warn("CDN Tune-in goal increased from: "+r.tuneInGoal+" to: "+m+" with playlist age: "+a.age),m=0;else{var y=Math.floor(m/a.targetduration);if(l+=y,void 0!==u)u+=Math.round(m%a.targetduration/a.partTarget);this.log("CDN Tune-in age: "+a.ageHeader+"s last advanced "+g.toFixed(2)+"s goal: "+m+" skip sn "+y+" to part "+u)}a.tuneInGoal=m}if(o=this.getDeliveryDirectives(a,e.deliveryDirectives,l,u),d||!v)return void this.loadPlaylist(o)}else o=this.getDeliveryDirectives(a,e.deliveryDirectives,l,u);var b=function(t,e){var r,i=1e3*t.levelTargetDuration,a=i/2,n=t.age,s=n>0&&n<3*i,o=e.loading.end-e.loading.start,l=t.availabilityDelay;if(!1===t.updated)if(s){var u=333*t.misses;r=Math.max(Math.min(a,2*o),u),t.availabilityDelay=(t.availabilityDelay||0)+r}else r=a;else s?(l=Math.min(l||i/2,n),t.availabilityDelay=l,r=l+i-n):r=i-o;return Math.round(r)}(a,n);void 0!==l&&a.canBlockReload&&(b-=a.partTarget||1),this.log("reload live playlist "+t+" in "+Math.round(b)+" ms"),this.timer=self.setTimeout((function(){return i.loadPlaylist(o)}),b)}else this.clearTimer()},e.getDeliveryDirectives=function(t,e,r,i){var a=function(t,e){var r=t.canSkipUntil,i=t.canSkipDateRanges,a=t.endSN;return r&&(void 0!==e?e-a:0)-1&&null!==(e=t.context)&&void 0!==e&&e.deliveryDirectives)this.warn("retry playlist loading #"+this.retryCount+' after "'+t.details+'"'),this.loadPlaylist();else{var n=Math.min(Math.pow(2,this.retryCount)*i.levelLoadingRetryDelay,i.levelLoadingMaxRetryTimeout);this.timer=self.setTimeout((function(){return r.loadPlaylist()}),n),this.warn("retry playlist loading #"+this.retryCount+" in "+n+' ms after "'+t.details+'"')}else this.warn('cannot recover from error "'+t.details+'"'),this.clearTimer(),t.fatal=!0;return a},t}();function q(){return(q=Object.assign||function(t){for(var e=1;e0){r=a[0].bitrate,a.sort((function(t,e){return t.bitrate-e.bitrate})),this._levels=a;for(var c=0;cthis.hls.config.fragLoadingMaxRetry&&(n=r.frag.level)):n=r.frag.level}break;case s.a.LEVEL_LOAD_ERROR:case s.a.LEVEL_LOAD_TIMEOUT:i&&(i.deliveryDirectives&&(l=!1),n=i.level),o=!0;break;case s.a.REMUX_ALLOC_ERROR:n=r.level,o=!0}void 0!==n&&this.recoverLevel(r,n,o,l)}}},u.recoverLevel=function(t,e,r,i){var a=t.details,n=this._levels[e];if(n.loadError++,r){if(!this.retryLoadingOrFail(t))return void(this.currentLevelIndex=-1);t.levelRetry=!0}if(i){var s=n.url.length;if(s>1&&n.loadError1){var i=(e.urlId+1)%r;this.warn("Switching to redundant URL-id "+i),this._levels.forEach((function(t){t.urlId=i})),this.level=t}},u.onFragLoaded=function(t,e){var r=e.frag;if(void 0!==r&&r.type===_.b.MAIN){var i=this._levels[r.level];void 0!==i&&(i.fragmentError=0,i.loadError=0)}},u.onLevelLoaded=function(t,e){var r,i,a=e.level,n=e.details,s=this._levels[a];if(!s)return this.warn("Invalid level index "+a),void(null!==(i=e.deliveryDirectives)&&void 0!==i&&i.skip&&(n.deltaUpdateFailed=!0));a===this.currentLevelIndex?(0===s.fragmentError&&(s.loadError=0,this.retryCount=0),this.playlistLoaded(a,e,s.details)):null!==(r=e.deliveryDirectives)&&void 0!==r&&r.skip&&(n.deltaUpdateFailed=!0)},u.onAudioTrackSwitched=function(t,e){var r=this.hls.levels[this.currentLevelIndex];if(r&&r.audioGroupIds){for(var i=-1,a=this.hls.audioTracks[e.id].groupId,n=0;n0){var i=r.urlId,a=r.url[i];if(t)try{a=t.addDirectives(a)}catch(t){this.warn("Could not construct new URL with HLS Delivery Directives: "+t)}this.log("Attempt loading level index "+e+(t?" at sn "+t.msn+" part "+t.part:"")+" with URL-id "+i+" "+a),this.clearTimer(),this.hls.trigger(n.a.LEVEL_LOADING,{url:a,level:e,id:i,deliveryDirectives:t||null})}},u.removeLevel=function(t,e){var r=function(t,r){return r!==e},i=this._levels.filter((function(i,a){return a!==t||i.url.length>1&&void 0!==e&&(i.url=i.url.filter(r),i.audioGroupIds&&(i.audioGroupIds=i.audioGroupIds.filter(r)),i.textGroupIds&&(i.textGroupIds=i.textGroupIds.filter(r)),i.urlId=0,!0)})).map((function(t,e){var r=t.details;return null!=r&&r.fragments&&r.fragments.forEach((function(t){t.level=e})),t}));this._levels=i,this.hls.trigger(n.a.LEVELS_UPDATED,{levels:i})},a=i,(o=[{key:"levels",get:function(){return 0===this._levels.length?null:this._levels}},{key:"level",get:function(){return this.currentLevelIndex},set:function(t){var e,r=this._levels;if(0!==r.length&&(this.currentLevelIndex!==t||null===(e=r[t])||void 0===e||!e.details)){if(t<0||t>=r.length){var i=t<0;if(this.hls.trigger(n.a.ERROR,{type:s.b.OTHER_ERROR,details:s.a.LEVEL_SWITCH_ERROR,level:t,fatal:i,reason:"invalid level idx"}),i)return;t=Math.min(t,r.length-1)}this.clearTimer();var a=this.currentLevelIndex,o=r[a],l=r[t];this.log("switching to level "+t+" from "+a),this.currentLevelIndex=t;var u=q({},l,{level:t,maxBitrate:l.maxBitrate,uri:l.uri,urlId:l.urlId});delete u._urlId,this.hls.trigger(n.a.LEVEL_SWITCHING,u);var d=l.details;if(!d||d.live){var h=this.switchParams(l.uri,null==o?void 0:o.details);this.loadPlaylist(h)}}}},{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._firstLevel}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)}}])&&z(a.prototype,o),l&&z(a,l),i}(X);!function(t){t.NOT_LOADED="NOT_LOADED",t.BACKTRACKED="BACKTRACKED",t.APPENDING="APPENDING",t.PARTIAL="PARTIAL",t.OK="OK"}($||($={}));var tt=function(){function t(t){this.activeFragment=null,this.activeParts=null,this.fragments=Object.create(null),this.timeRanges=Object.create(null),this.bufferPadding=.2,this.hls=void 0,this.hls=t,this._registerListeners()}var e=t.prototype;return e._registerListeners=function(){var t=this.hls;t.on(n.a.BUFFER_APPENDED,this.onBufferAppended,this),t.on(n.a.FRAG_BUFFERED,this.onFragBuffered,this),t.on(n.a.FRAG_LOADED,this.onFragLoaded,this)},e._unregisterListeners=function(){var t=this.hls;t.off(n.a.BUFFER_APPENDED,this.onBufferAppended,this),t.off(n.a.FRAG_BUFFERED,this.onFragBuffered,this),t.off(n.a.FRAG_LOADED,this.onFragLoaded,this)},e.destroy=function(){this._unregisterListeners(),this.fragments=this.timeRanges=null},e.getAppendedFrag=function(t,e){if(e===_.b.MAIN){var r=this.activeFragment,i=this.activeParts;if(!r)return null;if(i)for(var a=i.length;a--;){var n=i[a],s=n?n.end:r.appendedPTS;if(n.start<=t&&void 0!==s&&t<=s)return a>9&&(this.activeParts=i.slice(a-9)),n}else if(r.start<=t&&void 0!==r.appendedPTS&&t<=r.appendedPTS)return r}return this.getBufferedFrag(t,e)},e.getBufferedFrag=function(t,e){for(var r=this.fragments,i=Object.keys(r),a=i.length;a--;){var n=r[i[a]];if((null==n?void 0:n.body.type)===e&&n.buffered){var s=n.body;if(s.start<=t&&t<=s.end)return s}}return null},e.detectEvictedFragments=function(t,e,r){var i=this;Object.keys(this.fragments).forEach((function(a){var n=i.fragments[a];if(n)if(n.buffered){var s=n.range[t];s&&s.time.some((function(t){var r=!i.isTimeBuffered(t.startPTS,t.endPTS,e);return r&&i.removeFragment(n.body),r}))}else n.body.type===r&&i.removeFragment(n.body)}))},e.detectPartialFragments=function(t){var e=this,r=this.timeRanges,i=t.frag,a=t.part;if(r&&"initSegment"!==i.sn){var n=rt(i),s=this.fragments[n];s&&(Object.keys(r).forEach((function(t){var n=i.elementaryStreams[t];if(n){var o=r[t],l=null!==a||!0===n.partial;s.range[t]=e.getBufferedTimes(i,a,l,o)}})),s.backtrack=s.loaded=null,Object.keys(s.range).length?s.buffered=!0:this.removeFragment(s.body))}},e.fragBuffered=function(t){var e=rt(t),r=this.fragments[e];r&&(r.backtrack=r.loaded=null,r.buffered=!0)},e.getBufferedTimes=function(t,e,r,i){for(var a={time:[],partial:r},n=e?e.start:t.start,s=e?e.end:t.end,o=t.minEndPTS||s,l=t.maxStartPTS||n,u=0;u=d&&o<=h){a.time.push({startPTS:Math.max(n,i.start(u)),endPTS:Math.min(s,i.end(u))});break}if(nd)a.partial=!0,a.time.push({startPTS:Math.max(n,i.start(u)),endPTS:Math.min(s,i.end(u))});else if(s<=d)break}return a},e.getPartialFragment=function(t){var e,r,i,a=null,n=0,s=this.bufferPadding,o=this.fragments;return Object.keys(o).forEach((function(l){var u=o[l];u&&et(u)&&(r=u.body.start-s,i=u.body.end+s,t>=r&&t<=i&&(e=Math.min(t-r,i-t),n<=e&&(a=u.body,n=e)))})),a},e.getState=function(t){var e=rt(t),r=this.fragments[e];return r?r.buffered?et(r)?$.PARTIAL:$.OK:r.backtrack?$.BACKTRACKED:$.APPENDING:$.NOT_LOADED},e.backtrack=function(t,e){var r=rt(t),i=this.fragments[r];if(!i||i.backtrack)return null;var a=i.backtrack=e||i.loaded;return i.loaded=null,a},e.getBacktrackData=function(t){var e=rt(t),r=this.fragments[e];if(r){var i,a=r.backtrack;if(null!=a&&null!==(i=a.payload)&&void 0!==i&&i.byteLength)return a;this.removeFragment(t)}return null},e.isTimeBuffered=function(t,e,r){for(var i,a,n=0;n=i&&e<=a)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&&!i){var a=rt(r);this.fragments[a]={body:r,loaded:e,backtrack:null,buffered:!1,range:Object.create(null)}}},e.onBufferAppended=function(t,e){var r=this,i=e.frag,a=e.part,n=e.timeRanges;if(i.type===_.b.MAIN)if(this.activeFragment=i,a){var s=this.activeParts;s||(this.activeParts=s=[]),s.push(a)}else this.activeParts=null;this.timeRanges=n,Object.keys(n).forEach((function(t){var e=n[t];if(r.detectEvictedFragments(t,e),!a)for(var s=0;st&&i.removeFragment(s)}}))},e.removeFragment=function(t){var e=rt(t);t.stats.loaded=0,t.clearElementaryStreamInfo(),delete this.fragments[e]},e.removeAllFragments=function(){this.fragments=Object.create(null),this.activeFragment=null,this.activeParts=null},t}();function et(t){var e,r;return t.buffered&&((null===(e=t.range.video)||void 0===e?void 0:e.partial)||(null===(r=t.range.audio)||void 0===r?void 0:r.partial))}function rt(t){return t.type+"_"+t.level+"_"+t.urlId+"_"+t.sn}var it=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._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}(),at={length:0,start:function(){return 0},end:function(){return 0}},nt=function(){function t(){}return t.isBuffered=function(e,r){try{if(e)for(var i=t.getBuffered(e),a=0;a=i.start(a)&&r<=i.end(a))return!0}catch(t){}return!1},t.bufferInfo=function(e,r,i){try{if(e){var a,n=t.getBuffered(e),s=[];for(a=0;as&&(i[n-1].end=t[a].end):i.push(t[a])}else i.push(t[a])}else i=t;for(var o,l=0,u=e,d=e,h=0;h=f&&er.startCC||t&&t.cc0)r=a+1;else{if(!(s<0))return n;i=a-1}}return null}};function ht(t,e,r){void 0===t&&(t=0),void 0===e&&(e=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 ft(t,e,r){var i=1e3*Math.min(e,r.duration+(r.deltaPTS?r.deltaPTS:0));return(r.endProgramDateTime||0)-i>t}function ct(t){var e="function"==typeof Map?new Map:void 0;return(ct=function(t){if(null===t||(r=t,-1===Function.toString.call(r).indexOf("[native code]")))return t;var r;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,i)}function i(){return vt(t,arguments,mt(this).constructor)}return i.prototype=Object.create(t.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}),pt(i,t)})(t)}function vt(t,e,r){return(vt=gt()?Reflect.construct:function(t,e,r){var i=[null];i.push.apply(i,e);var a=new(Function.bind.apply(t,i));return r&&pt(a,r.prototype),a}).apply(null,arguments)}function gt(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}function pt(t,e){return(pt=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function mt(t){return(mt=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var yt=Math.pow(2,17),bt=function(){function t(t){this.config=void 0,this.loader=null,this.partLoadTimeout=-1,this.config=t}var e=t.prototype;return e.destroy=function(){this.loader&&(this.loader.destroy(),this.loader=null)},e.abort=function(){this.loader&&this.loader.abort()},e.load=function(t,e){var r=this,i=t.url;if(!i)return Promise.reject(new Et({type:s.b.NETWORK_ERROR,details:s.a.FRAG_LOAD_ERROR,fatal:!1,frag:t,networkDetails:null},"Fragment does not have a "+(i?"part list":"url")));this.abort();var a=this.config,n=a.fLoader,o=a.loader;return new Promise((function(i,l){r.loader&&r.loader.destroy();var u=r.loader=t.loader=n?new n(a):new o(a),d=Tt(t),h={timeout:a.fragLoadingTimeOut,maxRetry:0,retryDelay:0,maxRetryDelay:a.fragLoadingMaxRetryTimeout,highWaterMark:yt};t.stats=u.stats,u.load(d,h,{onSuccess:function(e,a,n,s){r.resetLoader(t,u),i({frag:t,part:null,payload:e.data,networkDetails:s})},onError:function(e,i,a){r.resetLoader(t,u),l(new Et({type:s.b.NETWORK_ERROR,details:s.a.FRAG_LOAD_ERROR,fatal:!1,frag:t,response:e,networkDetails:a}))},onAbort:function(e,i,a){r.resetLoader(t,u),l(new Et({type:s.b.NETWORK_ERROR,details:s.a.INTERNAL_ABORTED,fatal:!1,frag:t,networkDetails:a}))},onTimeout:function(e,i,a){r.resetLoader(t,u),l(new Et({type:s.b.NETWORK_ERROR,details:s.a.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,networkDetails:a}))},onProgress:function(r,i,a,n){e&&e({frag:t,part:null,payload:a,networkDetails:n})}})}))},e.loadPart=function(t,e,r){var i=this;this.abort();var a=this.config,n=a.fLoader,o=a.loader;return new Promise((function(l,u){i.loader&&i.loader.destroy();var d=i.loader=t.loader=n?new n(a):new o(a),h=Tt(t,e),f={timeout:a.fragLoadingTimeOut,maxRetry:0,retryDelay:0,maxRetryDelay:a.fragLoadingMaxRetryTimeout,highWaterMark:yt};e.stats=d.stats,d.load(h,f,{onSuccess:function(a,n,s,o){i.resetLoader(t,d),i.updateStatsFromPart(t,e);var u={frag:t,part:e,payload:a.data,networkDetails:o};r(u),l(u)},onError:function(r,a,n){i.resetLoader(t,d),u(new Et({type:s.b.NETWORK_ERROR,details:s.a.FRAG_LOAD_ERROR,fatal:!1,frag:t,part:e,response:r,networkDetails:n}))},onAbort:function(r,a,n){t.stats.aborted=e.stats.aborted,i.resetLoader(t,d),u(new Et({type:s.b.NETWORK_ERROR,details:s.a.INTERNAL_ABORTED,fatal:!1,frag:t,part:e,networkDetails:n}))},onTimeout:function(r,a,n){i.resetLoader(t,d),u(new Et({type:s.b.NETWORK_ERROR,details:s.a.FRAG_LOAD_TIMEOUT,fatal:!1,frag:t,part:e,networkDetails:n}))}})}))},e.updateStatsFromPart=function(t,e){var r=t.stats,i=e.stats,a=i.total;if(r.loaded+=i.loaded,a){var n=Math.round(t.duration/e.duration),s=Math.min(Math.round(r.loaded/a),n),o=(n-s)*Math.round(r.loaded/s);r.total=r.loaded+o}else r.total=Math.max(r.loaded,r.total);var l=r.loading,u=i.loading;l.start?l.first+=u.first-u.start:(l.start=u.start,l.first=u.first),l.end=u.end},e.resetLoader=function(t,e){t.loader=null,this.loader===e&&(self.clearTimeout(this.partLoadTimeout),this.loader=null),e.destroy()},t}();function Tt(t,e){void 0===e&&(e=null);var r=e||t,i={frag:t,part:e,responseType:"arraybuffer",url:r.url,rangeStart:0,rangeEnd:0},n=r.byteRangeStartOffset,s=r.byteRangeEndOffset;return Object(a.a)(n)&&Object(a.a)(s)&&(i.rangeStart=n,i.rangeEnd=s),i}var Et=function(t){var e,r;function i(e){for(var r,i=arguments.length,a=new Array(i>1?i-1:0),n=1;ne.start+e.duration+l;(s0&&s&&s.key&&s.iv&&"AES-128"===s.method){var o=self.performance.now();return e.decrypter.webCryptoDecrypt(new Uint8Array(a),s.key.buffer,s.iv.buffer).then((function(e){var a=self.performance.now();return i.trigger(n.a.FRAG_DECRYPTED,{frag:t,payload:e,stats:{tstart:o,tdecrypt:a}}),r.payload=e,r}))}return r})).then((function(r){var i=e.fragCurrent,a=e.hls,s=e.levels;if(!s)throw new Error("init load aborted, missing levels");s[t.level].details;var o=t.stats;e.state=_t,e.fragLoadError=0,t.data=new Uint8Array(r.payload),o.parsing.start=o.buffering.start=self.performance.now(),o.parsing.end=o.buffering.end=self.performance.now(),r.frag===i&&a.trigger(n.a.FRAG_BUFFERED,{stats:o,frag:i,part:null,id:t.type}),e.tick()})).catch((function(r){e.warn(r),e.resetFragmentLoading(t)}))},f.fragContextChanged=function(t){var e=this.fragCurrent;return!t||!e||t.level!==e.level||t.sn!==e.sn||t.urlId!==e.urlId},f.fragBufferedComplete=function(t,e){var r=this.mediaBuffer?this.mediaBuffer:this.media;this.log("Buffered "+t.type+" sn: "+t.sn+(e?" part: "+e.index:"")+" of "+("[stream-controller]"===this.logPrefix?"level":"track")+" "+t.level+" "+Lt.toString(nt.getBuffered(r))),this.state=_t,this.tick()},f._handleFragmentLoadComplete=function(t){var e=this.transmuxer;if(e){var r=t.frag,i=t.part,a=t.partsLoaded,n=!a||0===a.length||a.some((function(t){return!t})),s=new st(r.level,r.sn,r.stats.chunkCount+1,0,i?i.index:-1,!n);e.flush(s)}},f._handleFragmentLoadProgress=function(t){},f._doFragLoad=function(t,e,r,i){var s=this;if(void 0===r&&(r=null),!this.levels)throw new Error("frag load aborted, missing levels");if(r=Math.max(t.start,r||0),this.config.lowLatencyMode&&e){var o=e.partList;if(o&&i){r>t.end&&e.fragmentHint&&(t=e.fragmentHint);var l=this.getNextPart(o,t,r);if(l>-1){var u=o[l];return this.log("Loading part sn: "+t.sn+" p: "+u.index+" cc: "+t.cc+" of playlist ["+e.startSN+"-"+e.endSN+"] parts [0-"+l+"-"+(o.length-1)+"] "+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),this.nextLoadPosition=u.start+u.duration,this.state=xt,this.hls.trigger(n.a.FRAG_LOADING,{frag:t,part:o[l],targetBufferTime:r}),this.doFragPartsLoad(t,o,l,i).catch((function(t){return s.handleFragLoadError(t)}))}if(!t.url||this.loadedEndOfParts(o,r))return Promise.resolve(null)}}return this.log("Loading fragment "+t.sn+" cc: "+t.cc+" "+(e?"of ["+e.startSN+"-"+e.endSN+"] ":"")+("[stream-controller]"===this.logPrefix?"level":"track")+": "+t.level+", target: "+parseFloat(r.toFixed(3))),Object(a.a)(t.sn)&&!this.bitrateTest&&(this.nextLoadPosition=t.start+t.duration),this.state=xt,this.hls.trigger(n.a.FRAG_LOADING,{frag:t,targetBufferTime:r}),this.fragmentLoader.load(t,i).catch((function(t){return s.handleFragLoadError(t)}))},f.doFragPartsLoad=function(t,e,r,i){var a=this;return new Promise((function(s,o){var l=[];!function r(u){var d=e[u];a.fragmentLoader.loadPart(t,d,i).then((function(i){l[d.index]=i;var o=i.part;a.hls.trigger(n.a.FRAG_LOADED,i);var h=e[u+1];if(!h||h.fragment!==t)return s({frag:t,part:o,partsLoaded:l});r(u+1)})).catch(o)}(r)}))},f.handleFragLoadError=function(t){var e=t.data;return e&&e.details===s.a.INTERNAL_ABORTED?this.handleFragLoadAborted(e.frag,e.part):this.hls.trigger(n.a.ERROR,e),null},f._handleTransmuxerFlush=function(t){var e=this.getCurrentContext(t);if(e&&this.state===Ct){var r=e.frag,i=e.part,a=e.level,n=self.performance.now();r.stats.parsing.end=n,i&&(i.stats.parsing.end=n),this.updateLevelTiming(r,i,a,t.partial)}else this.fragCurrent||(this.state=_t)},f.getCurrentContext=function(t){var e=this.levels,r=t.level,i=t.sn,a=t.part;if(!e||!e[r])return this.warn("Levels object was unset while buffering fragment "+i+" of level "+r+". The current chunk will not be buffered."),null;var n=e[r],s=a>-1?function(t,e,r){if(!t||!t.details)return null;var i=t.details.partList;if(i)for(var a=i.length;a--;){var n=i[a];if(n.index===r&&n.fragment.sn===e)return n}return null}(n,i,a):null,o=s?s.fragment:function(t,e,r){if(!t||!t.details)return null;var i=t.details,a=i.fragments[e-i.startSN];return a||((a=i.fragmentHint)&&a.sn===e?a:en&&this.flushMainBuffer(s,t.start)}else this.flushMainBuffer(0,t.start)},f.getFwdBufferInfo=function(t,e){var r=this.config,i=this.getLoadPosition();if(!Object(a.a)(i))return null;var n=nt.bufferInfo(t,i,r.maxBufferHole);if(0===n.len&&void 0!==n.nextStart){var s=this.fragmentTracker.getBufferedFrag(i,e);if(s&&n.nextStart=r&&(e.maxMaxBufferLength/=2,this.warn("Reduce max buffer length to "+e.maxMaxBufferLength+"s"),!0)},f.getNextFragment=function(t,e){var r,i,a=e.fragments,n=a.length;if(!n)return null;var s,o=this.config,l=a[0].start;if(e.live){var u=o.initialLiveManifestSize;if(n-1&&rr.start&&r.loaded},f.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||!Object(a.a)(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 s=e[n-t.startSN];r.cc===s.cc&&(i=s,this.log("Live playlist, switching playlist, load frag with next SN: "+i.sn))}i||(i=function(t,e){return dt.search(t,(function(t){return t.cce?-1:0}))}(e,r.cc))&&this.log("Live playlist, switching playlist, load frag with same CC: "+i.sn)}}else{var o=this.hls.liveSyncPosition;null!==o&&(i=this.getFragmentAtPosition(o,this.bitrateTest?t.fragmentEnd:t.edge,t))}return i},f.getFragmentAtPosition=function(t,e,r){var i,a=this.config,n=this.fragPrevious,s=r.fragments,o=r.endSN,l=r.fragmentHint,u=a.maxFragLookUpTolerance,d=!!(a.lowLatencyMode&&r.partList&&l);(d&&l&&!this.bitrateTest&&(s=s.concat(l),o=l.sn),te-u?0:u):i=s[s.length-1];if(i){var h=i.sn-r.startSN,f=n&&i.level===n.level,c=s[h+1];if(this.fragmentTracker.getState(i)===$.BACKTRACKED){i=null;for(var v=h;s[v]&&this.fragmentTracker.getState(s[v])===$.BACKTRACKED;)i=n?s[v--]:s[--v];i||(i=c)}else n&&i.sn===n.sn&&!d&&f&&(i.sn=n-e.maxFragLookUpTolerance&&a<=s;if(null!==i&&r.duration>i&&(a"+t.startSN+" prev-sn: "+(n?n.sn:"na")+" fragments: "+o),h}return l},f.waitForCdnTuneIn=function(t){return t.live&&t.canBlockReload&&t.tuneInGoal>Math.max(t.partHoldBack,3*t.partTarget)},f.setStartPosition=function(t,e){var r=this.startPosition;if(r"+t))}}])&&At(u.prototype,d),h&&At(u,h),i}(it);function Bt(){return self.MediaSource||self.WebKitMediaSource}function Ut(){return self.SourceBuffer||self.WebKitSourceBuffer}var Gt=r(17),jt=r(9),Kt=r(13),Ht=Bt()||{isTypeSupported:function(){return!1}},Vt=function(){function t(t,e,r,i){var a=this;this.hls=void 0,this.id=void 0,this.observer=void 0,this.frag=null,this.part=null,this.worker=void 0,this.onwmsg=void 0,this.transmuxer=null,this.onTransmuxComplete=void 0,this.onFlush=void 0,this.hls=t,this.id=e,this.onTransmuxComplete=r,this.onFlush=i;var l=t.config,u=function(e,r){(r=r||{}).frag=a.frag,r.id=a.id,t.trigger(e,r)};this.observer=new Kt.EventEmitter,this.observer.on(n.a.FRAG_DECRYPTED,u),this.observer.on(n.a.ERROR,u);var d={mp4:Ht.isTypeSupported("video/mp4"),mpeg:Ht.isTypeSupported("audio/mpeg"),mp3:Ht.isTypeSupported('audio/mp4; codecs="mp3"')},h=navigator.vendor;if(l.enableWorker&&"undefined"!=typeof Worker){var f;o.b.log("demuxing in webworker");try{f=this.worker=Gt(18),this.onwmsg=this.onWorkerMessage.bind(this),f.addEventListener("message",this.onwmsg),f.onerror=function(e){t.trigger(n.a.ERROR,{type:s.b.OTHER_ERROR,details:s.a.INTERNAL_EXCEPTION,fatal:!0,event:"demuxerWorker",error:new Error(e.message+" ("+e.filename+":"+e.lineno+")")})},f.postMessage({cmd:"init",typeSupported:d,vendor:h,id:e,config:JSON.stringify(l)})}catch(t){o.b.warn("Error in worker:",t),o.b.error("Error while initializing DemuxerWorker, fallback to inline"),f&&self.URL.revokeObjectURL(f.objectURL),this.transmuxer=new jt.c(this.observer,d,l,h,e),this.worker=null}}else this.transmuxer=new jt.c(this.observer,d,l,h,e)}var e=t.prototype;return e.destroy=function(){var t=this.worker;if(t)t.removeEventListener("message",this.onwmsg),t.terminate(),this.worker=null;else{var e=this.transmuxer;e&&(e.destroy(),this.transmuxer=null)}var r=this.observer;r&&r.removeAllListeners(),this.observer=null},e.push=function(t,e,r,i,a,n,s,l,u,d){var h=this;u.transmuxing.start=self.performance.now();var f=this.transmuxer,c=this.worker,v=n?n.start:a.start,g=a.decryptdata,p=this.frag,m=!(p&&a.cc===p.cc),y=!(p&&u.level===p.level),b=p?u.sn-p.sn:-1,T=this.part?u.part-this.part.index:1,E=!y&&(1===b||0===b&&1===T),S=self.performance.now();(y||b||0===a.stats.parsing.start)&&(a.stats.parsing.start=S),!n||!T&&E||(n.stats.parsing.start=S);var L=new jt.b(m,E,l,y,v);if(!E||m){o.b.log("[transmuxer-interface, "+a.type+"]: Starting new transmux session for sn: "+u.sn+" p: "+u.part+" level: "+u.level+" id: "+u.id+"\n discontinuity: "+m+"\n trackSwitch: "+y+"\n contiguous: "+E+"\n accurateTimeOffset: "+l+"\n timeOffset: "+v);var A=new jt.a(r,i,e,s,d);this.configureTransmuxer(A)}if(this.frag=a,this.part=n,c)c.postMessage({cmd:"demux",data:t,decryptdata:g,chunkMeta:u,state:L},t instanceof ArrayBuffer?[t]:[]);else if(f){var R=f.push(t,g,u,L);Object(jt.d)(R)?R.then((function(t){h.handleTransmuxComplete(t)})):this.handleTransmuxComplete(R)}},e.flush=function(t){var e=this;t.transmuxing.start=self.performance.now();var r=this.transmuxer,i=this.worker;if(i)i.postMessage({cmd:"flush",chunkMeta:t});else if(r){var a=r.flush(t);Object(jt.d)(a)?a.then((function(r){e.handleFlushResult(r,t)})):this.handleFlushResult(a,t)}},e.handleFlushResult=function(t,e){var r=this;t.forEach((function(t){r.handleTransmuxComplete(t)})),this.onFlush(e)},e.onWorkerMessage=function(t){var e=t.data,r=this.hls;switch(e.event){case"init":self.URL.revokeObjectURL(this.worker.objectURL);break;case"transmuxComplete":this.handleTransmuxComplete(e.data);break;case"flush":this.onFlush(e.data);break;default:e.data=e.data||{},e.data.frag=this.frag,e.data.id=this.id,r.trigger(e.event,e.data)}},e.configureTransmuxer=function(t){var e=this.worker,r=this.transmuxer;e?e.postMessage({cmd:"configure",config:t}):r&&r.configure(t)},e.handleTransmuxComplete=function(t){t.chunkMeta.transmuxing.end=self.performance.now(),this.onTransmuxComplete(t)},t}(),Wt=function(){function t(t,e,r,i){this.config=void 0,this.media=void 0,this.fragmentTracker=void 0,this.hls=void 0,this.nudgeRetry=0,this.stallReported=!1,this.stalled=null,this.moved=!1,this.seeking=!1,this.config=t,this.media=e,this.fragmentTracker=r,this.hls=i}var e=t.prototype;return e.destroy=function(){this.hls=this.fragmentTracker=this.media=null},e.poll=function(t){var e=this.config,r=this.media,i=this.stalled,a=r.currentTime,n=r.seeking,s=this.seeking&&!n,l=!this.seeking&&n;if(this.seeking=n,a===t){if((l||s)&&(this.stalled=null),!r.paused&&!r.ended&&0!==r.playbackRate&&nt.getBuffered(r).length){var u=nt.bufferInfo(r,a,0),d=u.len>0,h=u.nextStart||0;if(d||h){if(n){var f=u.len>2,c=!h||h-a>2&&!this.fragmentTracker.getPartialFragment(a);if(f||c)return;this.moved=!1}if(!this.moved&&null!==this.stalled){var v,g=Math.max(h,u.start||0)-a,p=this.hls.levels?this.hls.levels[this.hls.currentLevel]:null,m=(null==p||null===(v=p.details)||void 0===v?void 0:v.live)?2*p.details.targetduration:2;if(g>0&&g<=m)return void this._trySkipBufferHole(null)}var y=self.performance.now();if(null!==i){var b=y-i;!n&&b>=250&&this._reportStall(u.len);var T=nt.bufferInfo(r,a,e.maxBufferHole);this._tryFixBufferStall(T,b)}else this.stalled=y}}}else if(this.moved=!0,null!==i){if(this.stallReported){var E=self.performance.now()-i;o.b.warn("playback not stuck anymore @"+a+", after "+Math.round(E)+"ms"),this.stallReported=!1}this.stalled=null,this.nudgeRetry=0}},e._tryFixBufferStall=function(t,e){var r=this.config,i=this.fragmentTracker,a=this.media.currentTime,n=i.getPartialFragment(a);if(n&&this._trySkipBufferHole(n))return;t.len>r.maxBufferHole&&e>1e3*r.highBufferWatchdogPeriod&&(o.b.warn("Trying to nudge playhead over buffer-hole"),this.stalled=null,this._tryNudgeBuffer())},e._reportStall=function(t){var e=this.hls,r=this.media;this.stallReported||(this.stallReported=!0,o.b.warn("Playback stalling at @"+r.currentTime+" due to low buffer (buffer="+t+")"),e.trigger(n.a.ERROR,{type:s.b.MEDIA_ERROR,details:s.a.BUFFER_STALLED_ERROR,fatal:!1,buffer:t}))},e._trySkipBufferHole=function(t){for(var e=this.config,r=this.hls,i=this.media,a=i.currentTime,l=0,u=nt.getBuffered(i),d=0;d=l&&a0&&-1===t&&(this.log("Override startPosition with lastCurrentTime @"+e.toFixed(3)),t=e),this.state=_t,this.nextLoadPosition=this.startPosition=this.lastCurrentTime=t,this.tick()}else this._forceStartLoad=!0,this.state=Dt},f.stopLoad=function(){this._forceStartLoad=!1,t.prototype.stopLoad.call(this)},f.doTick=function(){switch(this.state){case _t:this.doTickIdle();break;case Mt:var t,e=this.levels,r=this.level,i=null==e||null===(t=e[r])||void 0===t?void 0:t.details;if(i&&(!i.live||this.levelLastLoaded===this.level)){if(this.waitForCdnTuneIn(i))break;this.state=_t;break}break;case wt:var a,n=self.performance.now(),s=this.retryDate;(!s||n>=s||null!==(a=this.media)&&void 0!==a&&a.seeking)&&(this.log("retryDate reached, switch back to IDLE state"),this.state=_t)}this.onTickEnd()},f.onTickEnd=function(){t.prototype.onTickEnd.call(this),this.checkBuffer(),this.checkFragmentChanged()},f.doTickIdle=function(){var t,e,r=this.hls,i=this.levelLastLoaded,a=this.levels,s=this.media,o=r.config,l=r.nextLoadLevel;if(null!==i&&(s||!this.startFragRequested&&o.startFragPrefetch)&&(!this.altAudio||!this.audioOnly)&&a&&a[l]){var d=a[l];this.level=r.nextLoadLevel=l;var h=d.details;if(!h||this.state===Mt||h.live&&this.levelLastLoaded!==l)this.state=Mt;else{var f=this.getFwdBufferInfo(this.mediaBuffer?this.mediaBuffer:s,_.b.MAIN);if(null!==f)if(!(f.len>=this.getMaxBufferLength(d.maxBitrate))){if(this._streamEnded(f,h)){var c={};return this.altAudio&&(c.type="video"),this.hls.trigger(n.a.BUFFER_EOS,c),void(this.state=Pt)}var v=f.end,g=this.getNextFragment(v,h);if(this.couldBacktrack&&!this.fragPrevious&&g&&"initSegment"!==g.sn){var p=g.sn-h.startSN;p>1&&(g=h.fragments[p-1],this.fragmentTracker.removeFragment(g))}if(g&&this.fragmentTracker.getState(g)===$.OK&&this.nextLoadPosition>v){var m=this.audioOnly&&!this.altAudio?u.a.AUDIO:u.a.VIDEO;this.afterBufferFlushed(s,m,_.b.MAIN),g=this.getNextFragment(this.nextLoadPosition,h)}g&&(!g.initSegment||g.initSegment.data||this.bitrateTest||(g=g.initSegment),"identity"!==(null===(t=g.decryptdata)||void 0===t?void 0:t.keyFormat)||null!==(e=g.decryptdata)&&void 0!==e&&e.key?this.loadFragment(g,h,v):this.loadKey(g,h))}}}},f.loadFragment=function(e,r,i){var a,n=this.fragmentTracker.getState(e);if(this.fragCurrent=e,n===$.BACKTRACKED){var s=this.fragmentTracker.getBacktrackData(e);if(s)return this._handleFragmentLoadProgress(s),void this._handleFragmentLoadComplete(s);n=$.NOT_LOADED}n===$.NOT_LOADED||n===$.PARTIAL?"initSegment"===e.sn?this._loadInitSegment(e):this.bitrateTest?(e.bitrateTest=!0,this.log("Fragment "+e.sn+" of level "+e.level+" is being downloaded to test bitrate and will not be buffered"),this._loadBitrateTestFrag(e)):(this.startFragRequested=!0,t.prototype.loadFragment.call(this,e,r,i)):n===$.APPENDING?this.reduceMaxBufferLength(e.duration)&&this.fragmentTracker.removeFragment(e):0===(null===(a=this.media)||void 0===a?void 0:a.buffered.length)&&this.fragmentTracker.removeAllFragments()},f.getAppendedFrag=function(t){var e=this.fragmentTracker.getAppendedFrag(t,_.b.MAIN);return e&&"fragment"in e?e.fragment:e},f.getBufferedFrag=function(t){return this.fragmentTracker.getBufferedFrag(t,_.b.MAIN)},f.followingBufferedFrag=function(t){return t?this.getBufferedFrag(t.end+.5):null},f.immediateLevelSwitch=function(){this.abortCurrentFrag(),this.flushMainBuffer(0,Number.POSITIVE_INFINITY)},f.nextLevelSwitch=function(){var t=this.levels,e=this.media;if(null!=e&&e.readyState){var r,i=this.getAppendedFrag(e.currentTime);if(i&&i.start>1&&this.flushMainBuffer(0,i.start-1),!e.paused&&t){var a=t[this.hls.nextLoadLevel],n=this.fragLastKbps;r=n&&this.fragCurrent?this.fragCurrent.duration*a.maxBitrate/(1e3*n)+1:0}else r=0;var s=this.getBufferedFrag(e.currentTime+r);if(s){var o=this.followingBufferedFrag(s);if(o){this.abortCurrentFrag();var l=o.maxStartPTS?o.maxStartPTS:o.start,u=o.duration,d=Math.max(s.end,l+Math.min(Math.max(u-this.config.maxFragLookUpTolerance,.5*u),.75*u));this.flushMainBuffer(d,Number.POSITIVE_INFINITY)}}}},f.abortCurrentFrag=function(){var t=this.fragCurrent;this.fragCurrent=null,null!=t&&t.loader&&t.loader.abort(),this.state===kt&&(this.state=_t),this.nextLoadPosition=this.getLoadPosition()},f.flushMainBuffer=function(e,r){t.prototype.flushMainBuffer.call(this,e,r,this.altAudio?"video":null)},f.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 Wt(this.config,i,this.fragmentTracker,this.hls)},f.onMediaDetaching=function(){var e=this.media;e&&(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)},f.onMediaPlaying=function(){this.tick()},f.onMediaSeeked=function(){var t=this.media,e=t?t.currentTime:null;Object(a.a)(e)&&this.log("Media seeked to "+e.toFixed(3)),this.tick()},f.onManifestLoading=function(){this.log("Trigger BUFFER_RESET"),this.hls.trigger(n.a.BUFFER_RESET,void 0),this.fragmentTracker.removeAllFragments(),this.couldBacktrack=this.stalled=!1,this.startPosition=this.lastCurrentTime=0,this.fragPlaying=null},f.onManifestParsed=function(t,e){var r,i,a,n=!1,s=!1;e.levels.forEach((function(t){(r=t.audioCodec)&&(-1!==r.indexOf("mp4a.40.2")&&(n=!0),-1!==r.indexOf("mp4a.40.5")&&(s=!0))})),this.audioCodecSwitch=n&&s&&!("function"==typeof(null==(a=Ut())||null===(i=a.prototype)||void 0===i?void 0:i.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},f.onLevelLoading=function(t,e){var r=this.levels;if(r&&this.state===_t){var i=r[e.level];(!i.details||i.details.live&&this.levelLastLoaded!==e.level||this.waitForCdnTuneIn(i.details))&&(this.state=Mt)}},f.onLevelLoaded=function(t,e){var r,i=this.levels,a=e.level,s=e.details,o=s.totalduration;if(i){this.log("Level "+a+" loaded ["+s.startSN+","+s.endSN+"], cc ["+s.startCC+", "+s.endCC+"] duration:"+o);var l=this.fragCurrent;!l||this.state!==xt&&this.state!==wt||l.level!==e.level&&l.loader&&(this.state=_t,l.loader.abort());var u=i[a],d=0;if(s.live||null!==(r=u.details)&&void 0!==r&&r.live){if(s.fragments[0]||(s.deltaUpdateFailed=!0),s.deltaUpdateFailed)return;d=this.alignPlaylists(s,u.details)}if(u.details=s,this.levelLastLoaded=a,this.hls.trigger(n.a.LEVEL_UPDATED,{details:s,level:a}),this.state===Mt){if(this.waitForCdnTuneIn(s))return;this.state=_t}this.startFragRequested?s.live&&this.synchronizeToLiveEdge(s):this.setStartPosition(s,d),this.tick()}else this.warn("Levels were reset while loading level "+a)},f._handleFragmentLoadProgress=function(t){var e,r=t.frag,i=t.part,a=t.payload,n=this.levels;if(n){var s=n[r.level],o=s.details;if(o){var l=s.videoCodec,u=o.PTSKnown||!o.live,d=null===(e=r.initSegment)||void 0===e?void 0:e.data,h=this._getAudioCodec(s),f=this.transmuxer=this.transmuxer||new Vt(this.hls,_.b.MAIN,this._handleTransmuxComplete.bind(this),this._handleTransmuxerFlush.bind(this)),c=i?i.index:-1,v=-1!==c,g=new st(r.level,r.sn,r.stats.chunkCount,a.byteLength,c,v),p=this.initPTS[r.cc];f.push(a,d,h,l,r,i,o.totalduration,u,g,p)}else this.warn("Dropping fragment "+r.sn+" of level "+r.level+" after level details were reset")}else this.warn("Levels were reset while fragment load was in progress. Fragment "+r.sn+" of level "+r.level+" will not be buffered")},f.onAudioTrackSwitching=function(t,e){var r=this.altAudio,i=!!e.url,a=e.id;if(!i){if(this.mediaBuffer!==this.media){this.log("Switching on main audio, use media.buffered to schedule main fragment loading"),this.mediaBuffer=this.media;var s=this.fragCurrent;null!=s&&s.loader&&(this.log("Switching to main audio track, cancel main fragment load"),s.loader.abort()),this.resetTransmuxer(),this.resetLoadingState()}else this.audioOnly&&this.resetTransmuxer();var o=this.hls;r&&o.trigger(n.a.BUFFER_FLUSHING,{startOffset:0,endOffset:Number.POSITIVE_INFINITY,type:"audio"}),o.trigger(n.a.AUDIO_TRACK_SWITCHED,{id:a})}},f.onAudioTrackSwitched=function(t,e){var r=e.id,i=!!this.hls.audioTracks[r].url;if(i){var a=this.videoBuffer;a&&this.mediaBuffer!==a&&(this.log("Switching on alternate audio, use video.buffered to schedule main fragment loading"),this.mediaBuffer=a)}this.altAudio=i,this.tick()},f.onBufferCreated=function(t,e){var r,i,a=e.tracks,n=!1;for(var s in a){var o=a[s];if("main"===o.id){if(i=s,r=o,"video"===s){var l=a[s];l&&(this.videoBuffer=l.buffer)}}else n=!0}n&&r?(this.log("Alternate track found, use "+i+".buffered to schedule main fragment loading"),this.mediaBuffer=r.buffer):this.mediaBuffer=this.media},f.onFragBuffered=function(t,e){var r=e.frag,i=e.part;if(!r||r.type===_.b.MAIN){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===Ot&&(this.state=_t));var a=i?i.stats:r.stats;this.fragLastKbps=Math.round(8*a.total/(a.buffering.end-a.loading.first)),"initSegment"!==r.sn&&(this.fragPrevious=r),this.fragBufferedComplete(r,i)}},f.onError=function(t,e){switch(e.details){case s.a.FRAG_LOAD_ERROR:case s.a.FRAG_LOAD_TIMEOUT:case s.a.KEY_LOAD_ERROR:case s.a.KEY_LOAD_TIMEOUT:this.onFragmentOrKeyLoadError(_.b.MAIN,e);break;case s.a.LEVEL_LOAD_ERROR:case s.a.LEVEL_LOAD_TIMEOUT:this.state!==Ft&&(e.fatal?(this.warn(""+e.details),this.state=Ft):e.levelRetry||this.state!==Mt||(this.state=_t));break;case s.a.BUFFER_FULL_ERROR:if("main"===e.parent&&(this.state===Ct||this.state===Ot)){var r=!0,i=this.getFwdBufferInfo(this.media,_.b.MAIN);i&&i.len>.5&&(r=!this.reduceMaxBufferLength(i.len)),r&&(this.warn("buffer full error also media.currentTime is not buffered, flush main"),this.immediateLevelSwitch()),this.resetLoadingState()}}},f.checkBuffer=function(){var t=this.media,e=this.gapController;if(t&&e&&t.readyState){var r=nt.getBuffered(t);!this.loadedmetadata&&r.length?(this.loadedmetadata=!0,this.seekToStartPos()):e.poll(this.lastCurrentTime),this.lastCurrentTime=t.currentTime}},f.onFragLoadEmergencyAborted=function(){this.state=_t,this.loadedmetadata||(this.startFragRequested=!1,this.nextLoadPosition=this.startPosition),this.tickImmediate()},f.onBufferFlushed=function(t,e){var r=e.type;if(r!==u.a.AUDIO||this.audioOnly&&!this.altAudio){var i=(r===u.a.VIDEO?this.videoBuffer:this.mediaBuffer)||this.media;this.afterBufferFlushed(i,r,_.b.MAIN)}},f.onLevelsUpdated=function(t,e){this.levels=e.levels},f.swapAudioCodec=function(){this.audioCodecSwap=!this.audioCodecSwap},f.seekToStartPos=function(){var t=this.media,e=t.currentTime,r=this.startPosition;if(r>=0&&e0&&a1&&!1===t.seeking){var r=t.currentTime;if(nt.isBuffered(t,r)?e=this.getAppendedFrag(r):nt.isBuffered(t,r+.1)&&(e=this.getAppendedFrag(r+.1)),e){var i=this.fragPlaying,a=e.level;i&&e.sn===i.sn&&i.level===a&&e.urlId===i.urlId||(this.hls.trigger(n.a.FRAG_CHANGED,{frag:e}),i&&i.level===a||this.hls.trigger(n.a.LEVEL_SWITCHED,{level:a}),this.fragPlaying=e)}}},l=i,(d=[{key:"nextLevel",get:function(){var t=this.nextBufferedFrag;return t?t.level:-1}},{key:"currentLevel",get:function(){var t=this.media;if(t){var e=this.getAppendedFrag(t.currentTime);if(e)return e.level}return-1}},{key:"nextBufferedFrag",get:function(){var t=this.media;if(t){var e=this.getAppendedFrag(t.currentTime);return this.followingBufferedFrag(e)}return null}},{key:"forceStartLoad",get:function(){return this._forceStartLoad}}])&&Yt(l.prototype,d),h&&Yt(l,h),i}(Nt),zt=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}(),Qt=function(){function t(t,e,r){this.defaultEstimate_=void 0,this.minWeight_=void 0,this.minDelayMs_=void 0,this.slow_=void 0,this.fast_=void 0,this.defaultEstimate_=r,this.minWeight_=.001,this.minDelayMs_=50,this.slow_=new zt(t),this.fast_=new zt(e)}var e=t.prototype;return e.update=function(t,e){var r=this.slow_,i=this.fast_;this.slow_.halfLife!==t&&(this.slow_=new zt(t,r.getEstimate(),r.getTotalWeight())),this.fast_.halfLife!==e&&(this.fast_=new zt(e,i.getEstimate(),i.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.canEstimate=function(){var t=this.fast_;return t&&t.getTotalWeight()>=this.minWeight_},e.getEstimate=function(){return this.canEstimate()?Math.min(this.fast_.getEstimate(),this.slow_.getEstimate()):this.defaultEstimate_},e.destroy=function(){},t}();function $t(t,e){for(var r=0;r=2*d/f||y<=T)){var E,S=Number.POSITIVE_INFINITY;for(E=t.level-1;E>v;E--){if((S=d*c[E].maxBitrate/(6.4*m))=y)){var L=this.bwEstimator.getEstimate();o.b.warn("Fragment "+t.sn+(e?" part "+e.index:"")+" of level "+t.level+" is loading too slowly and will cause an underbuffer; aborting and switching to level "+E+"\n Current BW estimate: "+(Object(a.a)(L)?(L/1024).toFixed(3):"Unknown")+" Kb/s\n Estimated load time for current fragment: "+y.toFixed(3)+" s\n Estimated load time for the next fragment: "+S.toFixed(3)+" s\n Time to underbuffer: "+T.toFixed(3)+" s"),r.nextLoadLevel=E,this.bwEstimator.sample(h,u.loaded),this.clearTimer(),t.loader&&(this.fragCurrent=this.partCurrent=null,t.loader.abort()),r.trigger(n.a.FRAG_LOAD_EMERGENCY_ABORTED,{frag:t,part:e,stats:u})}}}}}},l.onFragLoaded=function(t,e){var r=e.frag,i=e.part;if(r.type===_.b.MAIN&&Object(a.a)(r.sn)){var s=i?i.stats:r.stats,o=i?i.duration:r.duration;if(this.clearTimer(),this.lastLoadedFragLevel=r.level,this._nextAutoLevel=-1,this.hls.config.abrMaxWithRealBitrate){var l=this.hls.levels[r.level],u=(l.loaded?l.loaded.bytes:0)+s.loaded,d=(l.loaded?l.loaded.duration:0)+o;l.loaded={bytes:u,duration:d},l.realBitrate=Math.round(8*u/d)}if(r.bitrateTest){var h={stats:s,frag:r,part:i,id:r.type};this.onFragBuffered(n.a.FRAG_BUFFERED,h),r.bitrateTest=!1}}},l.onFragBuffered=function(t,e){var r=e.frag,i=e.part,a=i?i.stats:r.stats;if(!a.aborted&&r.type===_.b.MAIN&&"initSegment"!==r.sn){var n=a.parsing.end-a.loading.start;this.bwEstimator.sample(n,a.loaded),a.bwEstimate=this.bwEstimator.getEstimate(),r.bitrateTest?this.bitrateTestDelay=n/1e3:this.bitrateTestDelay=0}},l.onError=function(t,e){switch(e.details){case s.a.FRAG_LOAD_ERROR:case s.a.FRAG_LOAD_TIMEOUT:this.clearTimer()}},l.clearTimer=function(){self.clearInterval(this.timer),this.timer=void 0},l.getNextABRAutoLevel=function(){var t=this.fragCurrent,e=this.partCurrent,r=this.hls,i=r.maxAutoLevel,a=r.config,n=r.minAutoLevel,s=r.media,l=e?e.duration:t?t.duration:0,u=s?s.currentTime:0,d=s&&0!==s.playbackRate?Math.abs(s.playbackRate):1,h=this.bwEstimator?this.bwEstimator.getEstimate():a.abrEwmaDefaultEstimate,f=(nt.bufferInfo(s,u,a.maxBufferHole).end-u)/d,c=this.findBestLevel(h,n,i,f,a.abrBandWidthFactor,a.abrBandWidthUpFactor);if(c>=0)return c;o.b.trace((f?"rebuffering expected":"buffer is empty")+", finding optimal quality level");var v=l?Math.min(l,a.maxStarvationDelay):a.maxStarvationDelay,g=a.abrBandWidthFactor,p=a.abrBandWidthUpFactor;if(!f){var m=this.bitrateTestDelay;if(m)v=(l?Math.min(l,a.maxLoadingDelay):a.maxLoadingDelay)-m,o.b.trace("bitrate test took "+Math.round(1e3*m)+"ms, set first fragment max fetchDuration to "+Math.round(1e3*v)+" ms"),g=p=1}return c=this.findBestLevel(h,n,i,f+v,g,p),Math.max(c,0)},l.findBestLevel=function(t,e,r,i,a,n){for(var s,l=this.fragCurrent,u=this.partCurrent,d=this.lastLoadedFragLevel,h=this.hls.levels,f=h[d],c=!(null==f||null===(s=f.details)||void 0===s||!s.live),v=null==f?void 0:f.codecSet,g=u?u.duration:l?l.duration:0,p=r;p>=e;p--){var m=h[p];if(m&&(!v||m.codecSet===v)){var y=m.details,b=(u?null==y?void 0:y.partTarget:null==y?void 0:y.averagetargetduration)||g,T=void 0;T=p<=d?a*t:n*t;var E=h[p].maxBitrate,S=E*b/T;if(o.b.trace("level/adjustedbw/bitrate/avgDuration/maxFetchDuration/fetchDuration: "+p+"/"+Math.round(T)+"/"+E+"/"+b+"/"+i+"/"+S),T>E&&(!S||c&&!this.bitrateTestDelay||S0||Object.keys(this.pendingTracks).length>0},e.destroy=function(){this.unregisterListeners(),this.details=null},e.registerListeners=function(){var t=this.hls;t.on(n.a.MEDIA_ATTACHING,this.onMediaAttaching,this),t.on(n.a.MEDIA_DETACHING,this.onMediaDetaching,this),t.on(n.a.MANIFEST_PARSED,this.onManifestParsed,this),t.on(n.a.BUFFER_RESET,this.onBufferReset,this),t.on(n.a.BUFFER_APPENDING,this.onBufferAppending,this),t.on(n.a.BUFFER_CODECS,this.onBufferCodecs,this),t.on(n.a.BUFFER_EOS,this.onBufferEos,this),t.on(n.a.BUFFER_FLUSHING,this.onBufferFlushing,this),t.on(n.a.LEVEL_UPDATED,this.onLevelUpdated,this),t.on(n.a.FRAG_PARSED,this.onFragParsed,this),t.on(n.a.FRAG_CHANGED,this.onFragChanged,this)},e.unregisterListeners=function(){var t=this.hls;t.off(n.a.MEDIA_ATTACHING,this.onMediaAttaching,this),t.off(n.a.MEDIA_DETACHING,this.onMediaDetaching,this),t.off(n.a.MANIFEST_PARSED,this.onManifestParsed,this),t.off(n.a.BUFFER_RESET,this.onBufferReset,this),t.off(n.a.BUFFER_APPENDING,this.onBufferAppending,this),t.off(n.a.BUFFER_CODECS,this.onBufferCodecs,this),t.off(n.a.BUFFER_EOS,this.onBufferEos,this),t.off(n.a.BUFFER_FLUSHING,this.onBufferFlushing,this),t.off(n.a.LEVEL_UPDATED,this.onLevelUpdated,this),t.off(n.a.FRAG_PARSED,this.onFragParsed,this),t.off(n.a.FRAG_CHANGED,this.onFragChanged,this)},e._initSourceBuffer=function(){this.sourceBuffer={},this.operationQueue=new ee(this.sourceBuffer),this.listeners={audio:[],video:[],audiovideo:[]}},e.onManifestParsed=function(t,e){var r=2;(e.audio&&!e.video||!e.altAudio)&&(r=1),this.bufferCodecEventsExpected=this._bufferCodecEventsTotal=r,this.details=null,o.b.log(this.bufferCodecEventsExpected+" bufferCodec event(s) expected")},e.onMediaAttaching=function(t,e){var r=this.media=e.media;if(r&&re){var i=this.mediaSource=new re;i.addEventListener("sourceopen",this._onMediaSourceOpen),i.addEventListener("sourceended",this._onMediaSourceEnded),i.addEventListener("sourceclose",this._onMediaSourceClose),r.src=self.URL.createObjectURL(i),this._objectUrl=r.src}},e.onMediaDetaching=function(){var t=this.media,e=this.mediaSource,r=this._objectUrl;if(e){if(o.b.log("[buffer-controller]: media source detaching"),"open"===e.readyState)try{e.endOfStream()}catch(t){o.b.warn("[buffer-controller]: onMediaDetaching: "+t.message+" while calling endOfStream")}this.onBufferReset(),e.removeEventListener("sourceopen",this._onMediaSourceOpen),e.removeEventListener("sourceended",this._onMediaSourceEnded),e.removeEventListener("sourceclose",this._onMediaSourceClose),t&&(r&&self.URL.revokeObjectURL(r),t.src===r?(t.removeAttribute("src"),t.load()):o.b.warn("[buffer-controller]: media.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(n.a.MEDIA_DETACHED,void 0)},e.onBufferReset=function(){var t=this;this.getSourceBufferTypes().forEach((function(e){var r=t.sourceBuffer[e];try{r&&(t.removeBufferListeners(e),t.mediaSource&&t.mediaSource.removeSourceBuffer(r),t.sourceBuffer[e]=void 0)}catch(t){o.b.warn("[buffer-controller]: Failed to reset the "+e+" buffer",t)}})),this._initSourceBuffer()},e.onBufferCodecs=function(t,e){var r=this,i=this.getSourceBufferTypes().length;Object.keys(e).forEach((function(t){if(i){var a=r.tracks[t];if(a&&"function"==typeof a.buffer.changeType){var n=e[t],s=n.codec,o=n.levelCodec,l=n.container;if((a.levelCodec||a.codec).replace(ie,"$1")!==(o||s).replace(ie,"$1")){var u=l+";codecs="+(o||s);r.appendChangeType(t,u)}}}else r.pendingTracks[t]=e[t]})),i||(this.bufferCodecEventsExpected=Math.max(this.bufferCodecEventsExpected-1,0),this.mediaSource&&"open"===this.mediaSource.readyState&&this.checkPendingTracks())},e.appendChangeType=function(t,e){var r=this,i=this.operationQueue,a={execute:function(){var a=r.sourceBuffer[t];a&&(o.b.log("[buffer-controller]: changing "+t+" sourceBuffer type to "+e),a.changeType(e)),i.shiftAndExecuteNext(t)},onStart:function(){},onComplete:function(){},onError:function(e){o.b.warn("[buffer-controller]: Failed to change "+t+" SourceBuffer type",e)}};i.append(a,t)},e.onBufferAppending=function(t,e){var r=this,i=this.hls,a=this.operationQueue,l=this.tracks,u=e.data,d=e.type,h=e.frag,f=e.part,c=e.chunkMeta,v=c.buffering[d],g=self.performance.now();v.start=g;var p=h.stats.buffering,m=f?f.stats.buffering:null;0===p.start&&(p.start=g),m&&0===m.start&&(m.start=g);var y=l.audio,b="audio"===d&&1===c.id&&"audio/mpeg"===(null==y?void 0:y.container),T={execute:function(){if(v.executeStart=self.performance.now(),b){var t=r.sourceBuffer[d];if(t){var e=h.start-t.timestampOffset;Math.abs(e)>=.1&&(o.b.log("[buffer-controller]: Updating audio SourceBuffer timestampOffset to "+h.start+" (delta: "+e+") sn: "+h.sn+")"),t.timestampOffset=h.start)}}r.appendExecutor(u,d)},onStart:function(){},onComplete:function(){var t=self.performance.now();v.executeEnd=v.end=t,0===p.first&&(p.first=t),m&&0===m.first&&(m.first=t);var e=r.sourceBuffer,i={};for(var a in e)i[a]=nt.getBuffered(e[a]);r.appendError=0,r.hls.trigger(n.a.BUFFER_APPENDED,{type:d,frag:h,part:f,chunkMeta:c,parent:h.type,timeRanges:i})},onError:function(t){o.b.error("[buffer-controller]: Error encountered while trying to append to the "+d+" SourceBuffer",t);var e={type:s.b.MEDIA_ERROR,parent:h.type,details:s.a.BUFFER_APPEND_ERROR,err:t,fatal:!1};t.code===DOMException.QUOTA_EXCEEDED_ERR?e.details=s.a.BUFFER_FULL_ERROR:(r.appendError++,e.details=s.a.BUFFER_APPEND_ERROR,r.appendError>i.config.appendErrorMaxRetry&&(o.b.error("[buffer-controller]: Failed "+i.config.appendErrorMaxRetry+" times to append segment in sourceBuffer"),e.fatal=!0)),i.trigger(n.a.ERROR,e)}};a.append(T,d)},e.onBufferFlushing=function(t,e){var r=this,i=this.operationQueue,a=function(t){return{execute:r.removeExecutor.bind(r,t,e.startOffset,e.endOffset),onStart:function(){},onComplete:function(){r.hls.trigger(n.a.BUFFER_FLUSHED,{type:t})},onError:function(e){o.b.warn("[buffer-controller]: Failed to remove from "+t+" SourceBuffer",e)}}};e.type?i.append(a(e.type),e.type):this.getSourceBufferTypes().forEach((function(t){i.append(a(t),t)}))},e.onFragParsed=function(t,e){var r=this,i=e.frag,a=e.part,s=[],l=a?a.elementaryStreams:i.elementaryStreams;l[u.a.AUDIOVIDEO]?s.push("audiovideo"):(l[u.a.AUDIO]&&s.push("audio"),l[u.a.VIDEO]&&s.push("video"));0===s.length&&o.b.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,a&&(a.stats.buffering.end=t);var e=a?a.stats:i.stats;r.hls.trigger(n.a.FRAG_BUFFERED,{frag:i,part:a,stats:e,id:i.type})}),s)},e.onFragChanged=function(t,e){this.flushBackBuffer()},e.onBufferEos=function(t,e){var r=this;this.getSourceBufferTypes().reduce((function(t,i){var a=r.sourceBuffer[i];return e.type&&e.type!==i||a&&!a.ended&&(a.ended=!0,o.b.log("[buffer-controller]: "+i+" sourceBuffer now EOS")),t&&!(a&&!a.ended)}),!0)&&this.blockBuffers((function(){var t=r.mediaSource;t&&"open"===t.readyState&&t.endOfStream()}))},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.flushBackBuffer=function(){var t=this.hls,e=this.details,r=this.media,i=this.sourceBuffer;if(r&&null!==e){var s=this.getSourceBufferTypes();if(s.length){var o=e.live&&null!==t.config.liveBackBufferLength?t.config.liveBackBufferLength:t.config.backBufferLength;if(Object(a.a)(o)&&!(o<0)){var l=r.currentTime,u=e.levelTargetDuration,d=Math.max(o,u),h=Math.floor(l/u)*u-d;s.forEach((function(r){var a=i[r];if(a){var s=nt.getBuffered(a);s.length>0&&h>s.start(0)&&(t.trigger(n.a.BACK_BUFFER_REACHED,{bufferEnd:h}),e.live&&t.trigger(n.a.LIVE_BACK_BUFFER_REACHED,{bufferEnd:h}),t.trigger(n.a.BUFFER_FLUSHING,{startOffset:0,endOffset:h,type:r}))}}))}}}},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,s=r.duration,l=Object(a.a)(i.duration)?i.duration:0;t.live&&e.config.liveDurationInfinity?(o.b.log("[buffer-controller]: Media Source duration is set to Infinity"),i.duration=1/0,this.updateSeekableRange(t)):(n>l&&n>s||!Object(a.a)(s))&&(o.b.log("[buffer-controller]: 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),a=Math.max(i,i+t.totalduration);e.setLiveSeekableRange(i,a)}},e.checkPendingTracks=function(){var t=this.bufferCodecEventsExpected,e=this.operationQueue,r=this.pendingTracks,i=Object.keys(r).length;if(i&&!t||2===i){this.createSourceBuffers(r),this.pendingTracks={};var a=this.getSourceBufferTypes();if(0===a.length)return void this.hls.trigger(n.a.ERROR,{type:s.b.MEDIA_ERROR,details:s.a.BUFFER_INCOMPATIBLE_CODECS_ERROR,fatal:!0,reason:"could not create source buffer for media codec(s)"});a.forEach((function(t){e.executeNext(t)}))}},e.createSourceBuffers=function(t){var e=this.sourceBuffer,r=this.mediaSource;if(!r)throw Error("createSourceBuffers called when mediaSource was null");var i=0;for(var a in t)if(!e[a]){var l=t[a];if(!l)throw Error("source buffer exists for track "+a+", however track does not");var u=l.levelCodec||l.codec,d=l.container+";codecs="+u;o.b.log("[buffer-controller]: creating sourceBuffer("+d+")");try{var h=e[a]=r.addSourceBuffer(d),f=a;this.addBufferListener(f,"updatestart",this._onSBUpdateStart),this.addBufferListener(f,"updateend",this._onSBUpdateEnd),this.addBufferListener(f,"error",this._onSBUpdateError),this.tracks[a]={buffer:h,codec:u,container:l.container,levelCodec:l.levelCodec,id:l.id},i++}catch(t){o.b.error("[buffer-controller]: error while trying to add sourceBuffer: "+t.message),this.hls.trigger(n.a.ERROR,{type:s.b.MEDIA_ERROR,details:s.a.BUFFER_ADD_CODEC_ERROR,fatal:!1,error:t,mimeType:d})}}i&&this.hls.trigger(n.a.BUFFER_CREATED,{tracks:this.tracks})},e._onSBUpdateStart=function(t){this.operationQueue.current(t).onStart()},e._onSBUpdateEnd=function(t){var e=this.operationQueue;e.current(t).onComplete(),e.shiftAndExecuteNext(t)},e._onSBUpdateError=function(t,e){o.b.error("[buffer-controller]: "+t+" SourceBuffer error",e),this.hls.trigger(n.a.ERROR,{type:s.b.MEDIA_ERROR,details:s.a.BUFFER_APPENDING_ERROR,fatal:!1});var r=this.operationQueue.current(t);r&&r.onError(e)},e.removeExecutor=function(t,e,r){var i=this.media,n=this.mediaSource,s=this.operationQueue,l=this.sourceBuffer[t];if(!i||!n||!l)return o.b.warn("[buffer-controller]: Attempting to remove from the "+t+" SourceBuffer, but it does not exist"),void s.shiftAndExecuteNext(t);var u=Object(a.a)(i.duration)?i.duration:1/0,d=Object(a.a)(n.duration)?n.duration:1/0,h=Math.max(0,e),f=Math.min(r,u,d);f>h?(o.b.log("[buffer-controller]: Removing ["+h+","+f+"] from the "+t+" SourceBuffer"),l.remove(h,f)):s.shiftAndExecuteNext(t)},e.appendExecutor=function(t,e){var r=this.operationQueue,i=this.sourceBuffer[e];if(!i)return o.b.warn("[buffer-controller]: Attempting to append to the "+e+" SourceBuffer, but it does not exist"),void r.shiftAndExecuteNext(e);i.ended=!1,i.appendBuffer(t)},e.blockBuffers=function(t,e){var r=this;if(void 0===e&&(e=this.getSourceBufferTypes()),!e.length)return o.b.log("[buffer-controller]: Blocking operation requested, but no SourceBuffers exist"),void Promise.resolve(t);var i=this.operationQueue,a=e.map((function(t){return i.appendBlocker(t)}));Promise.all(a).then((function(){t(),e.forEach((function(t){var e=r.sourceBuffer[t];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 a=r.bind(this,t);this.listeners[t].push({event:e,listener:a}),i.addEventListener(e,a)}},e.removeBufferListeners=function(t){var e=this.sourceBuffer[t];e&&this.listeners[t].forEach((function(t){e.removeEventListener(t.event,t.listener)}))},t}();function ne(t,e){for(var r=0;r0&&this.mediaWidth>0){var t=this.hls.levels;if(t.length){var e=this.hls;e.autoLevelCapping=this.getMaxLevel(t.length-1),e.autoLevelCapping>this.autoLevelCapping&&this.streamController&&this.streamController.nextLevelSwitch(),this.autoLevelCapping=e.autoLevelCapping}}},a.getMaxLevel=function(e){var r=this,i=this.hls.levels;if(!i.length)return-1;var a=i.filter((function(i,a){return t.isLevelAllowed(a,r.restrictedLevels)&&a<=e}));return this.clientRect=null,t.getMaxLevelByMediaSize(a,this.mediaWidth,this.mediaHeight)},a.startCapping=function(){this.timer||(this.autoLevelCapping=Number.POSITIVE_INFINITY,this.hls.firstLevel=this.getMaxLevel(this.firstLevel),self.clearInterval(this.timer),this.timer=self.setInterval(this.detectPlayerSize.bind(this),1e3),this.detectPlayerSize())},a.stopCapping=function(){this.restrictedLevels=[],this.firstLevel=-1,this.autoLevelCapping=Number.POSITIVE_INFINITY,this.timer&&(self.clearInterval(this.timer),this.timer=void 0)},a.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},t.isLevelAllowed=function(t,e){return void 0===e&&(e=[]),-1===e.indexOf(t)},t.getMaxLevelByMediaSize=function(t,e,r){if(!t||!t.length)return-1;for(var i,a,n=t.length-1,s=0;s=e||o.height>=r)&&(i=o,!(a=t[s+1])||i.width!==a.width||i.height!==a.height)){n=s;break}}return n},e=t,i=[{key:"contentScaleFactor",get:function(){var t=1;try{t=self.devicePixelRatio}catch(t){}return t}}],(r=[{key:"mediaWidth",get:function(){return this.getDimensions().width*t.contentScaleFactor}},{key:"mediaHeight",get:function(){return this.getDimensions().height*t.contentScaleFactor}}])&&ne(e.prototype,r),i&&ne(e,i),t}(),oe=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(n.a.MEDIA_ATTACHING,this.onMediaAttaching,this)},e.unregisterListeners=function(){this.hls.off(n.a.MEDIA_ATTACHING,this.onMediaAttaching)},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 a=i-this.lastTime,s=r-this.lastDroppedFrames,l=e-this.lastDecodedFrames,u=1e3*s/a,d=this.hls;if(d.trigger(n.a.FPS_DROP,{currentDropped:s,currentDecoded:l,totalDroppedFrames:r}),u>0&&s>d.config.fpsDroppedMonitoringThreshold*l){var h=d.currentLevel;o.b.warn("drop FPS ratio greater than max allowed value for currentLevel: "+h),h>0&&(-1===d.autoLevelCapping||d.autoLevelCapping>=h)&&(h-=1,d.trigger(n.a.FPS_DROP_LEVEL_CAPPING,{level:h,droppedLevel:d.currentLevel}),d.autoLevelCapping=h,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}(),le=r(12),ue=/^age:\s*[\d.]+\s*$/m,de=function(){function t(t){this.xhrSetup=void 0,this.requestTimeout=void 0,this.retryTimeout=void 0,this.retryDelay=void 0,this.config=null,this.callbacks=null,this.context=void 0,this.loader=null,this.stats=void 0,this.xhrSetup=t?t.xhrSetup:null,this.stats=new le.a,this.retryDelay=0}var e=t.prototype;return e.destroy=function(){this.callbacks=null,this.abortInternal(),this.loader=null,this.config=null},e.abortInternal=function(){var t=this.loader;self.clearTimeout(this.requestTimeout),self.clearTimeout(this.retryTimeout),t&&(t.onreadystatechange=null,t.onprogress=null,4!==t.readyState&&(this.stats.aborted=!0,t.abort()))},e.abort=function(){var t;this.abortInternal(),null!==(t=this.callbacks)&&void 0!==t&&t.onAbort&&this.callbacks.onAbort(this.stats,this.context,this.loader)},e.load=function(t,e,r){if(this.stats.loading.start)throw new Error("Loader can only be used once.");this.stats.loading.start=self.performance.now(),this.context=t,this.config=e,this.callbacks=r,this.retryDelay=e.retryDelay,this.loadInternal()},e.loadInternal=function(){var t=this.config,e=this.context;if(t){var r=this.loader=new self.XMLHttpRequest,i=this.stats;i.loading.first=0,i.loaded=0;var a=this.xhrSetup;try{if(a)try{a(r,e.url)}catch(t){r.open("GET",e.url,!0),a(r,e.url)}r.readyState||r.open("GET",e.url,!0)}catch(t){return void this.callbacks.onError({code:r.status,text:t.message},e,r)}e.rangeEnd&&r.setRequestHeader("Range","bytes="+e.rangeStart+"-"+(e.rangeEnd-1)),r.onreadystatechange=this.readystatechange.bind(this),r.onprogress=this.loadprogress.bind(this),r.responseType=e.responseType,self.clearTimeout(this.requestTimeout),this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),t.timeout),r.send()}},e.readystatechange=function(){var t=this.context,e=this.loader,r=this.stats;if(t&&e){var i=e.readyState,a=this.config;if(!r.aborted&&i>=2)if(self.clearTimeout(this.requestTimeout),0===r.loading.first&&(r.loading.first=Math.max(self.performance.now(),r.loading.start)),4===i){e.onreadystatechange=null,e.onprogress=null;var n=e.status;if(n>=200&&n<300){var s,l;if(r.loading.end=Math.max(self.performance.now(),r.loading.first),l="arraybuffer"===t.responseType?(s=e.response).byteLength:(s=e.responseText).length,r.loaded=r.total=l,!this.callbacks)return;var u=this.callbacks.onProgress;if(u&&u(r,t,s,e),!this.callbacks)return;var d={url:e.responseURL,data:s};this.callbacks.onSuccess(d,r,t,e)}else r.retry>=a.maxRetry||n>=400&&n<499?(o.b.error(n+" while loading "+t.url),this.callbacks.onError({code:n,text:e.statusText},t,e)):(o.b.warn(n+" while loading "+t.url+", retrying in "+this.retryDelay+"..."),this.abortInternal(),this.loader=null,self.clearTimeout(this.retryTimeout),this.retryTimeout=self.setTimeout(this.loadInternal.bind(this),this.retryDelay),this.retryDelay=Math.min(2*this.retryDelay,a.maxRetryDelay),r.retry++)}else self.clearTimeout(this.requestTimeout),this.requestTimeout=self.setTimeout(this.loadtimeout.bind(this),a.timeout)}},e.loadtimeout=function(){o.b.warn("timeout while loading "+this.context.url);var t=this.callbacks;t&&(this.abortInternal(),t.onTimeout(this.stats,this.context,this.loader))},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&&ue.test(this.loader.getAllResponseHeaders())){var e=this.loader.getResponseHeader("age");t=e?parseFloat(e):null}return t},t}(),he=r(16);function fe(t){var e="function"==typeof Map?new Map:void 0;return(fe=function(t){if(null===t||(r=t,-1===Function.toString.call(r).indexOf("[native code]")))return t;var r;if("function"!=typeof t)throw new TypeError("Super expression must either be null or a function");if(void 0!==e){if(e.has(t))return e.get(t);e.set(t,i)}function i(){return ce(t,arguments,pe(this).constructor)}return i.prototype=Object.create(t.prototype,{constructor:{value:i,enumerable:!1,writable:!0,configurable:!0}}),ge(i,t)})(t)}function ce(t,e,r){return(ce=ve()?Reflect.construct:function(t,e,r){var i=[null];i.push.apply(i,e);var a=new(Function.bind.apply(t,i));return r&&ge(a,r.prototype),a}).apply(null,arguments)}function ve(){if("undefined"==typeof Reflect||!Reflect.construct)return!1;if(Reflect.construct.sham)return!1;if("function"==typeof Proxy)return!0;try{return Boolean.prototype.valueOf.call(Reflect.construct(Boolean,[],(function(){}))),!0}catch(t){return!1}}function ge(t,e){return(ge=Object.setPrototypeOf||function(t,e){return t.__proto__=e,t})(t,e)}function pe(t){return(pe=Object.setPrototypeOf?Object.getPrototypeOf:function(t){return t.__proto__||Object.getPrototypeOf(t)})(t)}var me=function(){function t(t){this.fetchSetup=void 0,this.requestTimeout=void 0,this.request=void 0,this.response=void 0,this.controller=void 0,this.context=void 0,this.config=null,this.callbacks=null,this.stats=void 0,this.loader=null,this.fetchSetup=t.fetchSetup||ye,this.controller=new self.AbortController,this.stats=new le.a}var e=t.prototype;return e.destroy=function(){this.loader=this.callbacks=null,this.abortInternal()},e.abortInternal=function(){var t=this.response;t&&t.ok||(this.stats.aborted=!0,this.controller.abort())},e.abort=function(){var t;this.abortInternal(),null!==(t=this.callbacks)&&void 0!==t&&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 s=function(t,e){var r={method:"GET",mode:"cors",credentials:"same-origin",signal:e};t.rangeEnd&&(r.headers=new self.Headers({Range:"bytes="+t.rangeStart+"-"+String(t.rangeEnd-1)}));return r}(t,this.controller.signal),o=r.onProgress,l="arraybuffer"===t.responseType,u=l?"byteLength":"length";this.context=t,this.config=e,this.callbacks=r,this.request=this.fetchSetup(t,s),self.clearTimeout(this.requestTimeout),this.requestTimeout=self.setTimeout((function(){i.abortInternal(),r.onTimeout(n,t,i.response)}),e.timeout),self.fetch(this.request).then((function(r){if(i.response=i.loader=r,!r.ok){var s=r.status,u=r.statusText;throw new Te(u||"fetch, bad network response",s,r)}return n.loading.first=Math.max(self.performance.now(),n.loading.start),n.total=parseInt(r.headers.get("Content-Length")||"0"),o&&Object(a.a)(e.highWaterMark)?i.loadProgressively(r,n,t,e.highWaterMark,o):l?r.arrayBuffer():r.text()})).then((function(s){var l=i.response;self.clearTimeout(i.requestTimeout),n.loading.end=Math.max(self.performance.now(),n.loading.first),n.loaded=n.total=s[u];var d={url:l.url,data:s};o&&!Object(a.a)(e.highWaterMark)&&o(n,t,s,l),r.onSuccess(d,n,t,l)})).catch((function(e){if(self.clearTimeout(i.requestTimeout),!n.aborted){var a=e.code||0;r.onError({code:a,text:e.message},t,e.details)}}))},e.getCacheAge=function(){var t=null;if(this.response){var e=this.response.headers.get("age");t=e?parseFloat(e):null}return t},e.loadProgressively=function(t,e,r,i,a){void 0===i&&(i=0);var n=new he.a,s=t.body.getReader();return function o(){return s.read().then((function(s){if(s.done)return n.dataLength&&a(e,r,n.flush(),t),Promise.resolve(new ArrayBuffer(0));var l=s.value,u=l.length;return e.loaded+=u,u=i&&a(e,r,n.flush(),t)):a(e,r,l,t),o()})).catch((function(){return Promise.reject()}))}()},t}();function ye(t,e){return new self.Request(t.url,e)}var be,Te=function(t){var e,r;function i(e,r,i){var a;return(a=t.call(this,e)||this).code=void 0,a.details=void 0,a.code=r,a.details=i,a}return r=t,(e=i).prototype=Object.create(r.prototype),e.prototype.constructor=e,ge(e,r),i}(fe(Error)),Ee=me;!function(t){t.WIDEVINE="com.widevine.alpha",t.PLAYREADY="com.microsoft.playready"}(be||(be={}));var Se="undefined"!=typeof self&&self.navigator&&self.navigator.requestMediaKeySystemAccess?self.navigator.requestMediaKeySystemAccess.bind(self.navigator):null;function Le(){return(Le=Object.assign||function(t){for(var e=1;ee)return i;return 0}},{key:"maxAutoLevel",get:function(){var t=this.levels,e=this.autoLevelCapping;return-1===e&&t&&t.length?t.length-1:e}},{key:"nextAutoLevel",get:function(){return Math.min(Math.max(this.abrController.nextAutoLevel,this.minAutoLevel),this.maxAutoLevel)},set:function(t){this.abrController.nextAutoLevel=Math.max(this.minAutoLevel,t)}},{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:"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}}])&&xe(e.prototype,r),a&&xe(e,a),t}();we.defaultConfig=void 0}]).default})); +// @license-end \ No newline at end of file diff --git a/public/js/hls.min.js b/public/js/hls.min.js deleted file mode 100644 index b9098c5..0000000 --- a/public/js/hls.min.js +++ /dev/null @@ -1,5 +0,0 @@ -// @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 index 5970011..5cd46a6 100644 --- a/public/js/hlsPlayback.js +++ b/public/js/hlsPlayback.js @@ -1,30 +1,25 @@ // @license http://www.gnu.org/licenses/agpl-3.0.html AGPL-3.0 // SPDX-License-Identifier: AGPL-3.0-only -function playMedia(overlay, tagName) { - const media = overlay.parentElement.querySelector(tagName); - const url = media.getAttribute("data-url"); - const startTime = parseFloat(media.getAttribute("data-start") || "0"); - media.setAttribute("controls", ""); +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(media); + hls.attachMedia(video); hls.on(Hls.Events.MANIFEST_PARSED, function () { hls.loadLevel = hls.levels.length - 1; - hls.startLoad(startTime); - media.play(); + hls.startLoad(); + video.play(); }); - } else if (media.canPlayType('application/vnd.apple.mpegurl')) { - media.src = url; - media.addEventListener('canplay', function() { - if (startTime > 0) media.currentTime = startTime; - media.play(); + } else if (video.canPlayType('application/vnd.apple.mpegurl')) { + video.src = url; + video.addEventListener('canplay', function() { + video.play(); }); } } - -function playVideo(overlay) { playMedia(overlay, 'video'); } -function playAudio(overlay) { playMedia(overlay, 'audio'); } // @license-end diff --git a/public/js/infiniteScroll.js b/public/js/infiniteScroll.js index f79912f..9939c03 100644 --- a/public/js/infiniteScroll.js +++ b/public/js/infiniteScroll.js @@ -1,225 +1,66 @@ // @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]); + node.insertBefore(elem, node.childNodes[node.childNodes.length - 2]); } function getLoadMore(doc) { - return doc.querySelector(".show-more:not(.timeline-item)"); + return doc.querySelector('.show-more:not(.timeline-item)'); } -function getHrefs(selector) { - return new Set([...document.querySelectorAll(selector)].map(el => el.getAttribute("href"))); +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; } -function getTweetId(item) { - const m = item.querySelector(".tweet-link")?.getAttribute("href")?.match(/\/status\/(\d+)/); - return m ? m[1] : ""; -} +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)'; -function isDuplicate(item, hrefs) { - return hrefs.has(item.querySelector(".tweet-link")?.getAttribute("href")); -} + var html = document.querySelector("html"); + var container = document.querySelector(containerClass); + var loading = false; -const GAP = 10; + window.addEventListener('scroll', function() { + if (loading) return; + if (html.scrollTop + html.clientHeight >= html.scrollHeight - 3000) { + loading = true; + var loadMore = getLoadMore(document); + if (loadMore == null) return; -class Masonry { - constructor(container) { - this.container = container; - const colSizes = { - small: w => Math.max(130, w * 0.11), - medium: w => Math.max(190, Math.min(350, w * 0.22)), - large: w => Math.max(350, Math.min(480, w * 0.22)), - }; - const size = container.dataset.colSize || "medium"; - this._targetWidth = colSizes[size] || colSizes.medium; - this.colHeights = []; - this.colCounts = []; - this.colCount = 0; - this._lastWidth = 0; - this._colWidthCache = 0; - this._items = []; - this._revealTimer = null; - this.container.classList.add("masonry-active"); + loadMore.children[0].text = "Loading..."; - let resizeTimer; - window.addEventListener("resize", () => { - clearTimeout(resizeTimer); - resizeTimer = setTimeout(() => this._rebuild(), 50); - }); + var url = new URL(loadMore.children[0].href); + url.searchParams.append('scroll', 'true'); - // Re-sync positions whenever images finish loading and items grow taller. - // Must be set up before _rebuild() so initial items get observed on first pass. - let syncTimer; - this._observer = window.ResizeObserver ? new ResizeObserver(() => { - clearTimeout(syncTimer); - syncTimer = setTimeout(() => this.syncHeights(), 100); - }) : null; + fetch(url.toString()).then(function (response) { + return response.text(); + }).then(function (html) { + var parser = new DOMParser(); + var doc = parser.parseFromString(html, 'text/html'); + loadMore.remove(); - this._rebuild(); - } + for (var item of doc.querySelectorAll(itemClass)) { + if (item.className == "timeline-item show-more") continue; + if (isDuplicate(item, itemClass)) continue; + if (isTweet) container.appendChild(item); + else insertBeforeLast(container, item); + } - // Reveal all items and gallery siblings (show-more, top-ref). Idempotent. - _revealAll() { - clearTimeout(this._revealTimer); - for (const item of this._items) item.classList.add("masonry-visible"); - for (const el of this.container.parentElement.querySelectorAll(":scope > .show-more, :scope > .top-ref, :scope > .timeline-footer")) - el.classList.add("masonry-visible"); - } - - // Height-primary, count-as-tiebreaker: handles both tall tweets and unloaded images. - _pickCol() { - return this.colHeights.reduce((min, h, i) => { - const m = this.colHeights[min]; - return (h < m || (h === m && this.colCounts[i] < this.colCounts[min])) ? i : min; - }, 0); - } - - // Position items using current column state. Updates colHeights, colCounts, container height. - _position(items, heights, colWidth) { - for (let i = 0; i < items.length; i++) { - const col = this._pickCol(); - items[i].style.left = `${col * (colWidth + GAP)}px`; - items[i].style.top = `${this.colHeights[col]}px`; - this.colHeights[col] += heights[i] + GAP; - this.colCounts[col]++; - } - this.container.style.height = `${Math.max(0, ...this.colHeights)}px`; - } - - // Full reset and re-place all items. - _place(items, heights, n, colWidth) { - this.colHeights = new Array(n).fill(0); - this.colCounts = new Array(n).fill(0); - this.colCount = n; - this._position(items, heights, colWidth); - } - - _rebuild() { - const w = this.container.clientWidth; - const n = Math.max(1, Math.floor(w / this._targetWidth(w))); - if (n === this.colCount && w === this._lastWidth) return; - - const isFirst = this.colCount === 0; - - if (isFirst) { - this._items = [...this.container.querySelectorAll(".timeline-item")]; - } - - // Sort newest-first by tweet ID (snowflake IDs exceed Number precision, compare as strings). - this._items.sort((a, b) => { - const idA = getTweetId(a), idB = getTweetId(b); - if (idA.length !== idB.length) return idB.length - idA.length; - return idB < idA ? -1 : idB > idA ? 1 : 0; - }); - - // Pre-set widths BEFORE reading heights so measurements reflect the new column width. - const colWidth = this._colWidthCache = Math.floor((w - GAP * (n - 1)) / n); - for (const item of this._items) item.style.width = `${colWidth}px`; - - this._place(this._items, this._items.map(item => item.offsetHeight), n, colWidth); - this._lastWidth = w; - - if (isFirst) { - if (this._observer) this._items.forEach(item => this._observer.observe(item)); - // Reveal immediately if all images are cached, else wait for syncHeights. - const hasUnloaded = this._items.some(item => - [...item.querySelectorAll("img")].some(img => !img.complete)); - if (hasUnloaded) { - this._revealTimer = setTimeout(() => this._revealAll(), 1000); - } else { - this._revealAll(); - } - } - } - - // Re-read actual heights and re-place all items. Fixes drift after images load. - syncHeights() { - this._place(this._items, this._items.map(item => item.offsetHeight), this.colCount, this._colWidthCache); - this._revealAll(); - } - - // Batch-add items in three phases to avoid O(N) reflows: - // 1. writes: set widths, append all — no reads, no reflows - // 2. one read: batch offsetHeight - // 3. writes: assign columns, set left/top - addAll(newItems) { - if (!newItems.length) return; - const colWidth = this._colWidthCache; - - for (const item of newItems) { - item.style.width = `${colWidth}px`; - this.container.appendChild(item); - } - - this._position(newItems, newItems.map(item => item.offsetHeight), colWidth); - this._items.push(...newItems); - - if (this._observer) newItems.forEach(item => this._observer.observe(item)); - } -} - -document.addEventListener("DOMContentLoaded", function () { - const isTweet = location.pathname.includes("/status/"); - const containerClass = isTweet ? ".replies" : ".timeline"; - const itemClass = containerClass + " > div:not(.top-ref)"; - const html = document.documentElement; - const container = document.querySelector(containerClass); - const masonryEl = container?.querySelector(".gallery-masonry"); - const masonry = masonryEl ? new Masonry(masonryEl) : null; - let loading = false; - - function handleScroll(failed) { - if (loading || html.scrollTop + html.clientHeight < html.scrollHeight - 3000) return; - - const loadMore = getLoadMore(document); - if (!loadMore) return; - loading = true; - loadMore.children[0].text = "Loading..."; - - const url = new URL(loadMore.children[0].href); - url.searchParams.append("scroll", "true"); - - fetch(url) - .then(r => { - if (r.status > 299) throw new Error("error"); - return r.text(); - }) - .then(responseText => { - const doc = new DOMParser().parseFromString(responseText, "text/html"); - loadMore.remove(); - - if (masonry) { - masonry.syncHeights(); - const newMasonry = doc.querySelector(".gallery-masonry"); - if (newMasonry) { - const knownHrefs = getHrefs(".gallery-masonry .tweet-link"); - masonry.addAll([...newMasonry.querySelectorAll(".timeline-item")].filter(item => !isDuplicate(item, knownHrefs))); - } - } else { - const knownHrefs = getHrefs(`${itemClass} .tweet-link`); - for (const item of doc.querySelectorAll(itemClass)) { - if (item.className === "timeline-item show-more" || isDuplicate(item, knownHrefs)) continue; - isTweet ? container.appendChild(item) : insertBeforeLast(container, item); - } + loading = false; + const newLoadMore = getLoadMore(doc); + if (newLoadMore == null) return; + if (isTweet) container.appendChild(newLoadMore); + else insertBeforeLast(container, newLoadMore); + }).catch(function (err) { + console.warn('Something went wrong.', err); + loading = true; + }); } - - loading = false; - const newLoadMore = getLoadMore(doc); - if (newLoadMore) { - isTweet ? container.appendChild(newLoadMore) : insertBeforeLast(container, newLoadMore); - if (masonry) newLoadMore.classList.add("masonry-visible"); - } - }) - .catch(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/js/widgets.js b/public/js/widgets.js deleted file mode 100644 index 7bb283a..0000000 --- a/public/js/widgets.js +++ /dev/null @@ -1,221 +0,0 @@ -/** - * Drop-in replacement for Twitter's widgets.js - * Redirects twitter-tweet blockquotes to Nitter embeds - */ -(function () { - "use strict"; - - if (window.__nitterWidgets) return; - window.__nitterWidgets = true; - - var NITTER = new URL(document.currentScript.src).origin; - - var TWEET_RE = /(?:twitter\.com|x\.com)\/([^\/]+)\/status\/(\d+)/i; - var SELECTOR = "blockquote.twitter-tweet, blockquote.twitter-video"; - - var readyCallbacks = []; - var eventCallbacks = {}; - var isReady = false; - - function safeCall(fn, arg) { - try { - fn(arg); - } catch (e) { } - } - - function fireEvent(name, data) { - (eventCallbacks[name] || []).forEach(function (cb) { - safeCall(cb, data); - }); - } - - function parseTweetUrl(url) { - if (!url) return null; - var m = TWEET_RE.exec(url); - if (m) return { user: m[1], id: m[2] }; - m = url.match(/(\d{15,})/); - return m ? { user: null, id: m[1] } : null; - } - - function createIframe(tweet, opts) { - var url; - if (opts.videoOnly) { - url = NITTER + "/i/videos/tweet/" + tweet.id; - } else { - var path = tweet.user ? "/" + tweet.user : "/i"; - url = NITTER + path + "/status/" + tweet.id + "/embed"; - if (opts.theme) { - var theme = - opts.theme === "dark" - ? "nitter" - : opts.theme === "light" - ? "twitter" - : opts.theme; - url += "?theme=" + encodeURIComponent(theme); - } - } - - var iframe = document.createElement("iframe"); - iframe.src = url; - iframe.className = "nitter-embed-frame"; - iframe.loading = "lazy"; - iframe.setAttribute("allowtransparency", "true"); - iframe.setAttribute("frameborder", "0"); - iframe.setAttribute("scrolling", "no"); - if (opts.videoOnly) iframe.setAttribute("allowfullscreen", "true"); - - var width = opts.width || 550; - var margin = - opts.align === "center" - ? "10px auto" - : opts.align === "right" - ? "10px 0 10px auto" - : "10px 0"; - iframe.style.cssText = - "width:100%;max-width:" + - width + - "px;height:300px;" + - "border:none;display:block;margin:" + - margin; - - iframe.addEventListener("load", function () { - fireEvent("rendered", { target: iframe }); - }); - - return iframe; - } - - function processBlockquote(bq) { - if (bq.dataset.nitterProcessed) return false; - bq.dataset.nitterProcessed = "true"; - - var tweet = null; - var links = bq.querySelectorAll("a[href]"); - for (var i = 0; i < links.length && !tweet; i++) { - tweet = parseTweetUrl(links[i].href); - } - if (!tweet) return false; - - var d = bq.dataset; - var iframe = createIframe(tweet, { - width: d.mediaMaxWidth || d.width, - align: d.align, - theme: d.theme, - videoOnly: d.mediaMaxWidth !== undefined, - }); - - bq.style.display = "none"; - bq.parentNode.insertBefore(iframe, bq.nextSibling); - return true; - } - - function processEmbeds(root) { - var bqs = (root || document).querySelectorAll( - SELECTOR + ":not([data-nitter-processed])", - ); - for (var i = 0; i < bqs.length; i++) processBlockquote(bqs[i]); - } - - function handleResize(e) { - if (!Array.isArray(e.data) || e.data[0] !== "resizeIframe") return; - var h = e.data[1] && e.data[1].h; - if (!h || h <= 0 || h > 10000) return; // Cap at 10000px for sanity - - var frames = document.querySelectorAll("iframe.nitter-embed-frame"); - for (var i = 0; i < frames.length; i++) { - if (frames[i].contentWindow === e.source) { - frames[i].style.height = h + "px"; - return; - } - } - } - - function observeDOM() { - if (!window.MutationObserver || !document.body) return; - - function matches(el) { - return el.matches(SELECTOR) || el.querySelector(SELECTOR); - } - - new MutationObserver(function (muts) { - var found = muts.some(function (mut) { - return Array.prototype.some.call(mut.addedNodes, function (n) { - return n.nodeType === 1 && matches(n); - }); - }); - if (found) processEmbeds(); - }).observe(document.body, { childList: true, subtree: true }); - } - - function embedTweet(id, container, opts) { - if (!container) return Promise.reject("No container"); - var iframe = createIframe({ id: id, user: null }, opts || {}); - container.appendChild(iframe); - return Promise.resolve(iframe); - } - - var prevTwttr = window.twttr; - window.twttr = { - widgets: { - load: processEmbeds, - createTweet: embedTweet, - createTweetEmbed: embedTweet, - createVideo: embedTweet, - loaded: true, - }, - events: { - bind: function (name, cb) { - if (typeof cb !== "function") return; - if (!eventCallbacks[name]) eventCallbacks[name] = []; - eventCallbacks[name].push(cb); - }, - unbind: function (name, cb) { - if (!eventCallbacks[name]) return; - eventCallbacks[name] = cb - ? eventCallbacks[name].filter(function (f) { - return f !== cb; - }) - : []; - }, - }, - ready: function (cb) { - if (typeof cb !== "function") return; - if (isReady) cb(window.twttr); - else readyCallbacks.push(cb); - }, - _e: [], - }; - - // Process callbacks queued before load (twttr._e pattern) - if (prevTwttr && prevTwttr._e) { - prevTwttr._e.forEach(function (cb) { - safeCall(cb); - }); - } - - // Remove any Twitter scripts that snuck through - document - .querySelectorAll( - 'script[src*="platform.twitter.com"], script[src*="platform.x.com"]', - ) - .forEach(function (s) { - s.remove(); - }); - - function init() { - window.addEventListener("message", handleResize); - processEmbeds(); - observeDOM(); - isReady = true; - readyCallbacks.forEach(function (cb) { - safeCall(cb, window.twttr); - }); - readyCallbacks = []; - } - - if (document.readyState === "loading") { - document.addEventListener("DOMContentLoaded", init); - } else { - init(); - } -})(); diff --git a/public/md/about.md b/public/md/about.md index 3825e8f..c0adda9 100644 --- a/public/md/about.md +++ b/public/md/about.md @@ -4,15 +4,15 @@ Nitter is a free and open source alternative Twitter front-end focused on privacy and performance. The source is available on GitHub at -- No JavaScript or ads -- All requests go through the backend, client never talks to Twitter -- Prevents Twitter from tracking your IP or JavaScript fingerprint -- Uses Twitter's unofficial API (no developer account required) -- Lightweight (for [@nim_lang](/nim_lang), 60KB vs 784KB from twitter.com) -- RSS feeds -- Themes -- Mobile support (responsive design) -- AGPLv3 licensed, no proprietary instances permitted +* No JavaScript or ads +* All requests go through the backend, client never talks to Twitter +* Prevents Twitter from tracking your IP or JavaScript fingerprint +* Uses Twitter's unofficial API (no rate limits or developer account required) +* Lightweight (for [@nim_lang](/nim_lang), 60KB vs 784KB from twitter.com) +* RSS feeds +* Themes +* Mobile support (responsive design) +* AGPLv3 licensed, no proprietary instances permitted Nitter's GitHub wiki contains [instances](https://github.com/zedeus/nitter/wiki/Instances) and @@ -21,13 +21,12 @@ maintained by the community. ## Why use Nitter? -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 [browser's -fingerprint](https://restoreprivacy.com/browser-fingerprinting/), [no -JavaScript required](https://noscriptfingerprint.com/). This all became +It's impossible to use Twitter without JavaScript enabled. 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 +[browser's fingerprint](https://restoreprivacy.com/browser-fingerprinting/), +[no JavaScript required](https://noscriptfingerprint.com/). This all became particularly important after Twitter [removed the ability](https://www.eff.org/deeplinks/2020/04/twitter-removes-privacy-option-and-shows-why-we-need-strong-privacy-laws) for users to control whether their data gets sent to advertisers. @@ -43,13 +42,12 @@ Twitter account. ## Donating -Liberapay: https://liberapay.com/zedeus \ -Patreon: https://patreon.com/nitter \ -BTC: bc1qpqpzjkcpgluhzf7x9yqe7jfe8gpfm5v08mdr55 \ -ETH: 0x24a0DB59A923B588c7A5EBd0dBDFDD1bCe9c4460 \ -XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL \ -SOL: ANsyGNXFo6osuFwr1YnUqif2RdoYRhc27WdyQNmmETSW \ -ZEC: u1vndfqtzyy6qkzhkapxelel7ams38wmfeccu3fdpy2wkuc4erxyjm8ncjhnyg747x6t0kf0faqhh2hxyplgaum08d2wnj4n7cyu9s6zhxkqw2aef4hgd4s6vh5hpqvfken98rg80kgtgn64ff70djy7s8f839z00hwhuzlcggvefhdlyszkvwy3c7yw623vw3rvar6q6evd3xcvveypt +Liberapay: \ +Patreon: \ +BTC: bc1qp7q4qz0fgfvftm5hwz3vy284nue6jedt44kxya \ +ETH: 0x66d84bc3fd031b62857ad18c62f1ba072b011925 \ +LTC: ltc1qhsz5nxw6jw9rdtw9qssjeq2h8hqk2f85rdgpkr \ +XMR: 42hKayRoEAw4D6G6t8mQHPJHQcXqofjFuVfavqKeNMNUZfeJLJAcNU19i1bGdDvcdN6romiSscWGWJCczFLe9RFhM3d1zpL ## Contact diff --git a/public/robots.txt b/public/robots.txt deleted file mode 100644 index 812656a..0000000 --- a/public/robots.txt +++ /dev/null @@ -1,5 +0,0 @@ -User-agent: * -Disallow: / -Crawl-delay: 1 -User-agent: Twitterbot -Disallow: diff --git a/src/api.nim b/src/api.nim index 555a699..433bca7 100644 --- a/src/api.nim +++ b/src/api.nim @@ -1,365 +1,124 @@ # SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, httpclient, strutils, sequtils, sugar +import asyncdispatch, httpclient, uri, strutils, sequtils, sugar import packedjson -import types, query, formatters, consts, apiutils, parser, utils -import experimental/parser +import types, query, formatters, consts, apiutils, parser +import experimental/parser as newParser -# Helper to generate params object for GraphQL requests -proc genParams(variables: string; fieldToggles = ""): seq[(string, string)] = - result.add ("variables", variables) - result.add ("features", gqlFeatures) - if fieldToggles.len > 0: - result.add ("fieldToggles", fieldToggles) - -proc apiUrl(endpoint, variables: string; fieldToggles = ""; skipTid = false): ApiUrl = - return ApiUrl(endpoint: endpoint, params: genParams(variables, fieldToggles), skipTid: skipTid) - -proc apiReq(endpoint, variables: string; fieldToggles = ""; skipTid = false): ApiReq = - let url = apiUrl(endpoint, variables, fieldToggles, skipTid) - return ApiReq(cookie: url, oauth: url) - -proc cursorParam(after: string): string = - ## JSON-escape the user-supplied cursor so it cannot break out of the GraphQL - ## variables object (same input-validation class as the #1411 media SSRF). - if after.len > 0: "\"cursor\":" & $(%after) & "," else: "" - -proc mediaUrl(id, cursor: string): ApiReq = - result = ApiReq( - cookie: apiUrl(graphUserMedia, userMediaVars % [id, cursor, "100"]), - oauth: apiUrl(graphUserMediaV2, restIdVars % [id, cursor, "100"]) - ) - -proc userTweetsUrl(id: string; cursor: string): ApiReq = - return apiReq(graphUserTweetsV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles) - -proc userTweetsAndRepliesUrl(id: string; cursor: string): ApiReq = - result = ApiReq( - cookie: apiUrl(graphUserTweetsAndReplies, userTweetsAndRepliesVars % [id, cursor], userTweetsFieldToggles), - oauth: apiUrl(graphUserTweetsAndRepliesV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles, skipTid=true) - ) - -proc userArticlesUrl(id: string; cursor: string): ApiReq = - result = ApiReq( - cookie: apiUrl(graphUserArticles, userArticlesVars % [id, cursor], userTweetsFieldToggles), - oauth: apiUrl(graphUserArticlesV2, restIdVars % [id, cursor, "20"], userTweetsFieldToggles) - ) - -proc tweetDetailUrl(id, cursor: string; mode = Relevance): ApiReq = - return apiReq(graphTweet, tweetVars % [id, cursor, $mode]) - # let cookieVars = tweetDetailVars % [id, cursor] - # result = ApiReq( - # cookie: apiUrl(graphTweetDetail, cookieVars, tweetDetailFieldToggles), - # oauth: apiUrl(graphTweet, tweetVars % [id, cursor]) - # ) - -proc userUrl(username: string): ApiReq = - let cookieVars = $(%*{"screen_name": username, "withGrokTranslatedBio": false}) - result = ApiReq( - cookie: apiUrl(graphUser, cookieVars, tweetDetailFieldToggles), - oauth: apiUrl(graphUserV2, $(%*{"screen_name": username})) - ) - -proc getGraphUser*(username: string): Future[User] {.async.} = - if username.len == 0: return - let js = await fetchRaw(userUrl(username)) - result = parseGraphUser(js) - -proc getGraphUserById*(id: string): Future[User] {.async.} = +proc getGraphUser*(id: string): Future[User] {.async.} = if id.len == 0 or id.any(c => not c.isDigit): return let - url = apiReq(graphUserById, userByRestIdVars % id) - js = await fetchRaw(url) + variables = %*{"userId": id, "withSuperFollowsUserFields": true} + js = await fetchRaw(graphUser ? {"variables": $variables}, Api.userRestId) result = parseGraphUser(js) -proc getAboutAccount*(username: string): Future[AccountInfo] {.async.} = - if username.len == 0: return - let - url = apiReq(graphAboutAccount, $(%*{"screenName": username})) - js = await fetch(url) - result = parseAboutAccount(js) - -proc restReq(endpoint: string; params: seq[(string, string)] = @[]): ApiReq = - let url = ApiUrl(endpoint: endpoint, params: params) - ApiReq(cookie: url, oauth: url) - -proc getBroadcastInfo*(id: string): Future[Broadcast] {.async.} = - if id.len == 0: return - let - req = apiReq(graphBroadcast, $(%*{"id": id})) - js = await fetch(req) - result = parseBroadcastInfo(js) - -proc fetchBroadcastStream*(mediaKey: string): Future[string] {.async.} = - if mediaKey.len == 0: return - let - streamReq = restReq(restLiveStream & mediaKey) - streamJs = await fetch(streamReq) - result = streamJs{"source", "noRedirectPlaybackUrl"}.getStr( - streamJs{"source", "location"}.getStr) - -proc getAudioSpace*(id: string): Future[AudioSpace] {.async.} = - if id.len == 0: return - let - variables = %*{ - "id": id, - "isMetatagsQuery": false, - "withReplays": true, - "withListeners": true - } - req = apiReq(graphAudioSpace, $variables) - js = await fetch(req) - result = parseAudioSpace(js) - -proc getGraphUserTweets*(id: string; kind: TimelineKind; after=""): Future[Profile] {.async.} = - if id.len == 0: return - let - cursor = cursorParam(after) - url = case kind - of TimelineKind.tweets: userTweetsUrl(id, cursor) - of TimelineKind.replies: userTweetsAndRepliesUrl(id, cursor) - of TimelineKind.media: mediaUrl(id, cursor) - of TimelineKind.articles: userArticlesUrl(id, cursor) - js = await fetch(url) - result = parseGraphTimeline(js, after) - -proc getGraphCommunity*(id: string): Future[Community] {.async.} = - if id.len == 0: return - let - url = apiReq(graphCommunity, $(%*{"communityId": id})) - js = await fetch(url) - result = parseGraphCommunity(js) - -proc getGraphCommunityTweets*(id: string; rankingMode: string; after=""): Future[Timeline] {.async.} = - if id.len == 0: return - let - cursor = cursorParam(after) - url = apiReq(graphCommunityTweets, communityTweetsVars % [id, cursor, rankingMode]) - js = await fetch(url) - result = parseGraphCommunityTimeline(js, after) - -proc getGraphCommunityMedia*(id: string; after=""): Future[Timeline] {.async.} = - if id.len == 0: return - let - cursor = cursorParam(after) - url = apiReq(graphCommunityMedia, communityMediaVars % [id, cursor]) - js = await fetch(url) - result = parseGraphCommunityTimeline(js, after) - -proc communitySliceReq(endpoint, variables: string): ApiReq = - let url = ApiUrl(endpoint: endpoint, params: @[("variables", variables)]) - ApiReq(cookie: url, oauth: url) - -proc getGraphCommunityMembers*(id: string; after=""): Future[Result[User]] {.async.} = - if id.len == 0: return - let - cursor = if after.len > 0: $(%after) else: "null" - url = communitySliceReq(graphCommunityMembers, communityMembersVars % [id, cursor]) - js = await fetch(url) - result = parseGraphCommunityMembers(js, after) - -proc getGraphCommunityModerators*(id: string): Future[Result[User]] {.async.} = - if id.len == 0: return - let - url = communitySliceReq(graphCommunityModerators, communityMembersVars % [id, "null"]) - js = await fetch(url) - result = parseGraphCommunityMembers(js) - -proc getGraphCommunityHashtags*(id, hashtag: string; after=""): Future[Timeline] {.async.} = - if id.len == 0 or hashtag.len == 0: return - let - safeTag = multiReplace(hashtag, ("\"", ""), ("\\", "")) - cursor = cursorParam(after) - url = apiReq(graphCommunityHashtags, communityHashtagsVars % [id, cursor, safeTag]) - js = await fetch(url) - result = parseGraphCommunityTimeline(js, after) - -proc getGraphListTweets*(id: string; after=""): Future[Timeline] {.async.} = - if id.len == 0: return - let - cursor = cursorParam(after) - url = apiReq(graphListTweets, restIdVars % [id, cursor, "20"]) - js = await fetch(url) - result = parseGraphTimeline(js, after).tweets - proc getGraphListBySlug*(name, list: string): Future[List] {.async.} = let - variables = %*{"screenName": name, "listSlug": list} - url = apiReq(graphListBySlug, $variables) - js = await fetch(url) - result = parseGraphList(js) + variables = %*{"screenName": name, "listSlug": list, "withHighlightedLabel": false} + url = graphListBySlug ? {"variables": $variables} + result = parseGraphList(await fetch(url, Api.listBySlug)) proc getGraphList*(id: string): Future[List] {.async.} = - let - url = apiReq(graphListById, $(%*{"listId": id})) - js = await fetch(url) - result = parseGraphList(js) + let + variables = %*{"listId": id, "withHighlightedLabel": false} + url = graphList ? {"variables": $variables} + result = parseGraphList(await fetch(url, Api.list)) proc getGraphListMembers*(list: List; after=""): Future[Result[User]] {.async.} = if list.id.len == 0: return - var + let variables = %*{ "listId": list.id, + "cursor": after, + "withSuperFollowsUserFields": false, "withBirdwatchPivots": false, "withDownvotePerspective": false, "withReactionsMetadata": false, - "withReactionsPerspective": false + "withReactionsPerspective": false, + "withSuperFollowsTweetFields": false } - if after.len > 0: - variables["cursor"] = % after - let - url = apiReq(graphListMembers, $variables) - js = await fetchRaw(url) - result = parseGraphListMembers(js, after) + url = graphListMembers ? {"variables": $variables} + result = parseGraphListMembers(await fetchRaw(url, Api.listMembers), after) -proc getGraphUserConnections(userId: string; endpoint: string; kind: QueryKind; - after=""): Future[Result[User]] {.async.} = +proc getListTimeline*(id: string; after=""): Future[Timeline] {.async.} = + if id.len == 0: return + let + ps = genParams({"list_id": id, "ranking_mode": "reverse_chronological"}, after) + url = listTimeline ? ps + result = parseTimeline(await fetch(url, Api.timeline), after) + +proc getUser*(username: string): Future[User] {.async.} = + if username.len == 0: return + let + ps = genParams({"screen_name": username}) + json = await fetchRaw(userShow ? ps, Api.userShow) + result = parseUser(json, username) + +proc getUserById*(userId: string): Future[User] {.async.} = if userId.len == 0: return - var variables = %*{ - "userId": userId, - "count": 20, - "includePromotedContent": false, - "withGrokTranslatedBio": true - } - if after.len > 0: - variables["cursor"] = %after let - url = apiReq(endpoint, $variables) - js = await fetchRaw(url) - result = parseGraphFollowers(js, after, kind) + ps = genParams({"user_id": userId}) + json = await fetchRaw(userShow ? ps, Api.userShow) + result = parseUser(json) -proc getGraphFollowers*(userId: string; after=""): Future[Result[User]] {.async.} = - result = await getGraphUserConnections(userId, graphFollowers, followers, after) - -proc getGraphFollowing*(userId: string; after=""): Future[Result[User]] {.async.} = - result = await getGraphUserConnections(userId, graphFollowing, following, after) - -proc getGraphTweetResult*(id: string): Future[Tweet] {.async.} = +proc getTimeline*(id: string; after=""; replies=false): Future[Timeline] {.async.} = if id.len == 0: return let - url = apiReq(graphTweetResult, $(%*{"rest_id": id})) - js = await fetch(url) - result = parseGraphTweetResult(js) + ps = genParams({"userId": id, "include_tweet_replies": $replies}, after) + url = timeline / (id & ".json") ? ps + result = parseTimeline(await fetch(url, Api.timeline), after) -proc getTweetByRestId*(id: string): Future[Tweet] {.async.} = +proc getMediaTimeline*(id: string; after=""): Future[Timeline] {.async.} = if id.len == 0: return - let - url = apiReq(graphTweetResultByRestId, tweetByRestIdVars % id, articleFieldToggles) - js = await fetch(url) - result = parseTweetByRestId(js) + let url = mediaTimeline / (id & ".json") ? genParams(cursor=after) + result = parseTimeline(await fetch(url, Api.timeline), after) -proc getGraphTweet(id: string; after=""; mode = Relevance): Future[Conversation] {.async.} = - if id.len == 0: return +proc getPhotoRail*(name: string): Future[PhotoRail] {.async.} = + if name.len == 0: return let - cursor = cursorParam(after) - js = await fetch(tweetDetailUrl(id, cursor, mode)) - result = parseGraphConversation(js, id) + ps = genParams({"screen_name": name, "trim_user": "true"}, + count="18", ext=false) + url = photoRail ? ps + result = parsePhotoRail(await fetch(url, Api.timeline)) -proc getReplies*(id, after: string; mode = Relevance): Future[Result[Chain]] {.async.} = - result = (await getGraphTweet(id, after, mode)).replies +proc getSearch*[T](query: Query; after=""): Future[Result[T]] {.async.} = + when T is User: + const + searchMode = ("result_filter", "user") + parse = parseUsers + fetchFunc = fetchRaw + else: + const + searchMode = ("tweet_search_mode", "live") + parse = parseTweets + fetchFunc = fetchRaw + + let q = genQueryParam(query) + if q.len == 0 or q == emptyQuery: + return Result[T](beginning: true, query: query) + + let url = search ? genParams(searchParams & @[("q", q), searchMode], after) + try: + result = parse(await fetchFunc(url, Api.search), after) + result.query = query + except InternalError: + return Result[T](beginning: true, query: query) + +proc getTweetImpl(id: string; after=""): Future[Conversation] {.async.} = + let url = tweet / (id & ".json") ? genParams(cursor=after) + result = parseConversation(await fetch(url, Api.tweet), id) + +proc getReplies*(id, after: string): Future[Result[Chain]] {.async.} = + result = (await getTweetImpl(id, after)).replies result.beginning = after.len == 0 -proc getTweet*(id: string; after=""; mode = Relevance): Future[Conversation] {.async.} = - result = await getGraphTweet(id, mode=mode) +proc getTweet*(id: string; after=""): Future[Conversation] {.async.} = + result = await getTweetImpl(id) if after.len > 0: - result.replies = await getReplies(id, after, mode) + result.replies = await getReplies(id, after) -proc getGraphEditHistory*(id: string): Future[EditHistory] {.async.} = - if id.len == 0: return - let - url = apiReq(graphTweetEditHistory, tweetEditHistoryVars % id) - js = await fetch(url) - result = parseGraphEditHistory(js, id) - -proc getGraphTweetSearch*(query: Query; after=""): Future[Timeline] {.async.} = - # workaround for #1372 - let maxId = - if not after.startsWith("maxid:"): "" - else: validateNumber(after[6..^1]) - - let q = genQueryParam(query, maxId) - if q.len == 0 or q == emptyQuery: - return Timeline(query: query, beginning: true) - - let product = - case query.kind - of top: "Top" - # profile media feeds (RSS, multi-user timelines) must stay chronological - of media: (if query.fromUser.len == 0: "Media" else: "Latest") - else: "Latest" - - var - variables = %*{ - "rawQuery": q, - "count": 20, - "querySource": "typed_query", - "product": product, - "withGrokTranslatedBio":true, - "withQuickPromoteEligibilityTweetFields":false - } - - if after.len > 0 and maxId.len == 0: - variables["cursor"] = % after - let - url = apiReq(graphSearchTimeline, $variables) - js = await fetch(url) - result = parseGraphSearch[Tweets](js, after) - result.query = query - - # when no more items are available the API just returns the last page in - # full. this detects that and clears the page instead. - let prefix = min(64, min(after.len, result.bottom.len)) - if prefix > 0 and maxId.len == 0 and - after[0.. 0: - variables["cursor"] = % after - - let - url = apiReq(graphSearchTimeline, $variables) - js = await fetch(url) - result = parseGraphSearch[T](js, after) - result.query = query - -proc getGraphUserSearch*(query: Query; after=""): Future[Result[User]] = - getGraphProductSearch[User](query, "People", after) - -proc getGraphListSearch*(query: Query; after=""): Future[Result[ListSearchResult]] = - getGraphProductSearch[ListSearchResult](query, "Lists", after) - -proc getPhotoRail*(id: string): Future[PhotoRail] {.async.} = - if id.len == 0: return - let js = await fetch(mediaUrl(id, "")) - result = parseGraphPhotoRail(js) - -proc getGraphArticle*(id: string): Future[Article] {.async.} = - if id.len == 0: return - let - url = apiReq(graphTweetResultByRestId, articleVars % id, articleFieldToggles) - json = await fetchRaw(url) - result = parseGraphArticle(json) - -proc getGraphTweetResults*(ids: seq[string]): Future[seq[Tweet]] {.async.} = - if ids.len == 0: return - let - idsJson = "[" & ids.mapIt("\"" & it & "\"").join(",") & "]" - url = apiReq(graphTweetResultsByRestIds, articleBatchVars % idsJson, articleFieldToggles) - js = await fetch(url) - result = parseGraphTweetResults(js) +proc getStatus*(id: string): Future[Tweet] {.async.} = + let url = status / (id & ".json") ? genParams() + result = parseStatus(await fetch(url, Api.status)) proc resolve*(url: string; prefs: Prefs): Future[string] {.async.} = let client = newAsyncHttpClient(maxRedirects=0) diff --git a/src/apiutils.nim b/src/apiutils.nim index b2aad58..fd6960f 100644 --- a/src/apiutils.nim +++ b/src/apiutils.nim @@ -1,258 +1,121 @@ # SPDX-License-Identifier: AGPL-3.0-only -import httpclient, asyncdispatch, options, strutils, uri, times, math, tables -import jsony, packedjson, zippy, oauth/oauth1 -import types, auth, consts, parserutils, http_pool, tid +import httpclient, asyncdispatch, options, strutils, uri +import jsony, packedjson, zippy +import types, tokens, consts, parserutils, http_pool import experimental/types/common const rlRemaining = "x-rate-limit-remaining" rlReset = "x-rate-limit-reset" - rlLimit = "x-rate-limit-limit" - npCache = "x-np-cache" - errorsToSkip = {null, doesntExist, tweetNotFound, timeout, unauthorized, badRequest} -proc isCloudflareHtml*(body: string): bool = - ## Detect Cloudflare HTML error pages returned instead of JSON - if body.len < 14 or body[0] != '<': return false - body[0 ..< 14].toLowerAscii() == " from Cloudflare HTML for log diagnostics - let start = body.find("") - if start < 0: return "unknown" - let contentStart = start + 7 - let stop = body.find("", contentStart) - if stop < 0: return "unknown" - body[contentStart ..< stop].splitWhitespace().join(" ") +proc genParams*(pars: openArray[(string, string)] = @[]; cursor=""; + count="20"; ext=true): seq[(string, string)] = + result = timelineParams + for p in pars: + result &= p + if ext: + result &= ("ext", "mediaStats") + result &= ("include_ext_alt_text", "true") + result &= ("include_ext_media_availability", "true") + if count.len > 0: + result &= ("count", count) + if cursor.len > 0: + # The raw cursor often has plus signs, which sometimes get turned into spaces, + # so we need to them back into a plus + if " " in cursor: + result &= ("cursor", cursor.replace(" ", "+")) + else: + result &= ("cursor", cursor) -var - pool: HttpPool - disableTid: bool - apiProxy: string - maxRetries: int - retryDelayMs: int - -proc setDisableTid*(disable: bool) = - disableTid = disable - -proc setMaxRetries*(n: int) = - maxRetries = n - -proc setRetryDelayMs*(ms: int) = - retryDelayMs = ms - -proc setApiProxy*(url: string) = - apiProxy = "" - if url.len > 0: - apiProxy = url.strip(chars={'/'}) & "/" - if "http" notin apiProxy: - apiProxy = "http://" & apiProxy - -proc toUrl*(req: ApiReq; sessionKind: SessionKind): Uri = - let url = case sessionKind - of oauth: req.oauth - of cookie: req.cookie - let base = case sessionKind - of oauth: "https://api.x.com" - of cookie: "https://x.com/i/api" - let prefix = if url.endpoint.startsWith("1.1/"): "" else: "graphql/" - parseUri(base) / (prefix & url.endpoint) ? url.params - -proc getOauthHeader(url, oauthToken, oauthTokenSecret: string): string = - let - encodedUrl = url.replace(",", "%2C").replace("+", "%20") - params = OAuth1Parameters( - consumerKey: consumerKey, - signatureMethod: "HMAC-SHA1", - timestamp: $int(round(epochTime())), - nonce: "0", - isIncludeVersionToHeader: true, - token: oauthToken - ) - signature = getSignature(HttpGet, encodedUrl, "", params, consumerSecret, oauthTokenSecret) - - params.signature = percentEncode(signature) - - return getOauth1RequestHeader(params)["authorization"] - -proc getCookieHeader(authToken, ct0: string): string = - "auth_token=" & authToken & "; ct0=" & ct0 - -proc genHeaders*(session: Session, url: Uri, skipTid: bool): Future[HttpHeaders] {.async.} = +proc genHeaders*(token: Token = nil): HttpHeaders = result = newHttpHeaders({ - "accept": "*/*", + "connection": "keep-alive", + "authorization": auth, + "content-type": "application/json", + "x-guest-token": if token == nil: "" else: token.tok, + "x-twitter-active-user": "yes", + "authority": "api.twitter.com", "accept-encoding": "gzip", "accept-language": "en-US,en;q=0.9", - "content-type": "application/json", - "origin": "https://x.com", - "user-agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/142.0.0.0 Safari/537.36", - "x-twitter-active-user": "yes", - "x-twitter-client-language": "en", - "priority": "u=1, i" - }, titleCase=true) + "accept": "*/*", + "DNT": "1" + }) - case session.kind - of SessionKind.oauth: - result["authorization"] = getOauthHeader($url, session.oauthToken, session.oauthSecret) - of SessionKind.cookie: - result["x-twitter-auth-type"] = "OAuth2Session" - result["x-csrf-token"] = session.ct0 - result["cookie"] = getCookieHeader(session.authToken, session.ct0) - result["referer"] = "https://x.com/" - result["sec-ch-ua"] = """"Google Chrome";v="142", "Chromium";v="142", "Not A(Brand";v="24"""" - result["sec-ch-ua-mobile"] = "?0" - result["sec-ch-ua-platform"] = "Windows" - result["sec-fetch-dest"] = "empty" - result["sec-fetch-mode"] = "cors" - result["sec-fetch-site"] = "same-origin" - if disableTid or skipTid or "/1.1/" in url.path: - result["authorization"] = bearerToken2 - else: - result["authorization"] = bearerToken - result["x-client-transaction-id"] = await genTid(url.path) - -proc getAndValidateSession*(req: ApiReq): Future[Session] {.async.} = - result = await getSession(req) - case result.kind - of SessionKind.oauth: - if result.oauthToken.len == 0: - echo "[sessions] Empty oauth token, session: ", result.pretty - raise rateLimitError() - of SessionKind.cookie: - if result.authToken.len == 0 or result.ct0.len == 0: - echo "[sessions] Empty cookie credentials, session: ", result.pretty - raise rateLimitError() +template updateToken() = + if api != Api.search and resp.headers.hasKey(rlRemaining): + let + remaining = parseInt(resp.headers[rlRemaining]) + reset = parseInt(resp.headers[rlReset]) + token.setRateLimit(api, remaining, reset) template fetchImpl(result, fetchBody) {.dirty.} = once: pool = HttpPool() + var token = await getToken(api) + if token.tok.len == 0: + raise rateLimitError() + try: var resp: AsyncResponse - let skipTid = case session.kind - of oauth: req.oauth.skipTid - of cookie: req.cookie.skipTid - let headers = await genHeaders(session, url, skipTid) - - pool.use(headers): - template getContent = - # TODO: this is a temporary simple implementation - if apiProxy.len > 0 and "/1.1/" notin url.path: - resp = await c.get(($url).replace("https://", apiProxy)) - else: - resp = await c.get($url) - result = await resp.body - - getContent() + pool.use(genHeaders(token)): + resp = await c.get($url) + result = await resp.body if resp.status == $Http503: badClient = true - raise newException(BadClientError, "Bad client") - - if resp.status == $Http404 and result.len == 0: - echo "[sessions] transient 404 (empty body), retrying: ", url.path, ", session: ", session.pretty - raise rateLimitError() - - let cacheStatus = resp.headers.getOrDefault(npCache) - if cacheStatus notin ["HIT", "STALE"] and resp.headers.hasKey(rlRemaining): - let - remaining = parseInt(resp.headers[rlRemaining]) - reset = parseInt(resp.headers[rlReset]) - limit = parseInt(resp.headers[rlLimit]) - session.setRateLimit(req, remaining, reset, limit) + raise newException(InternalError, result) if result.len > 0: if resp.headers.getOrDefault("content-encoding") == "gzip": result = uncompress(result, dfGzip) - - if isCloudflareHtml(result): - echo "[cloudflare] ", resp.status, " (", cfTitle(result), "), API: ", url.path, ", session: ", session.pretty - raise rateLimitError() - - if result.startsWith("{\"errors"): - let errors = result.fromJson(Errors) - if errors notin errorsToSkip: - echo "Fetch error, API: ", url.path, ", errors: ", errors, ", session: ", session.pretty - if errors in {expiredToken, badToken, locked}: - invalidate(session) - raise rateLimitError() - elif errors in {rateLimited}: - # rate limit hit, resets after 24 hours - setLimited(session, req) - raise rateLimitError() - elif result.startsWith("429 Too Many Requests"): - echo "[sessions] 429 error, API: ", url.path, ", session: ", session.pretty - raise rateLimitError() + else: + echo "non-gzip body, url: ", url, ", body: ", result fetchBody + release(token, used=true) + if resp.status == $Http400: - echo "ERROR 400, ", url.path, ": ", result, ", session: ", session.pretty raise newException(InternalError, $url) except InternalError as e: raise e - except BadClientError as e: - raise e - except OSError as e: - raise e except Exception as e: - let s = session.pretty - echo "error: ", e.name, ", msg: ", e.msg, ", session: ", s, ", url: ", url - raise rateLimitError() - finally: - release(session) - -template retry(bod) {.dirty.} = - var session: Session - var retrySuccess = false - for i in 0 ..< maxRetries: - try: - session = nil - bod - retrySuccess = true - break - except RateLimitError: - let api = if session.isNil: req.cookie.endpoint - else: req.endpoint(session) - if session.isNil: - echo "[sessions] Rate limited, retrying ", api, - " request (", i, "/", maxRetries, ")..." - else: - echo "[sessions] Rate limited, retrying ", api, - " request (", i, "/", maxRetries, ")..., session: ", session.pretty - session = nil - if retryDelayMs > 0: - await sleepAsync(retryDelayMs) - if not retrySuccess: + echo "error: ", e.name, ", msg: ", e.msg, ", token: ", token[], ", url: ", url + if "length" notin e.msg and "descriptor" notin e.msg: + release(token, invalid=true) raise rateLimitError() -proc fetch*(req: ApiReq): Future[JsonNode] {.async.} = - retry: - var body: string - session = await getAndValidateSession(req) +proc fetch*(url: Uri; api: Api): Future[JsonNode] {.async.} = + var body: string + fetchImpl body: + if body.startsWith('{') or body.startsWith('['): + result = parseJson(body) + else: + echo resp.status, ": ", body, " --- url: ", url + result = newJNull() - let url = req.toUrl(session.kind) + updateToken() - fetchImpl body: - if body.startsWith('{') or body.startsWith('['): - result = parseJson(body) - else: - echo resp.status, ": ", body, " --- url: ", url, ", session: ", session.pretty - result = newJNull() + let error = result.getError + if error in {invalidToken, forbidden, badToken}: + echo "fetch error: ", result.getError + release(token, invalid=true) + raise rateLimitError() - let error = result.getError - if error != null and error notin errorsToSkip: - echo "Fetch error, API: ", url.path, ", error: ", error, ", session: ", session.pretty - if error in {expiredToken, badToken, locked}: - invalidate(session) - raise rateLimitError() +proc fetchRaw*(url: Uri; api: Api): Future[string] {.async.} = + fetchImpl result: + if not (result.startsWith('{') or result.startsWith('[')): + echo resp.status, ": ", result, " --- url: ", url + result.setLen(0) -proc fetchRaw*(req: ApiReq): Future[string] {.async.} = - retry: - session = await getAndValidateSession(req) - let url = req.toUrl(session.kind) + updateToken() - fetchImpl result: - if not (result.startsWith('{') or result.startsWith('[')): - echo resp.status, ": ", result, " --- url: ", url, ", session: ", session.pretty - result.setLen(0) + if result.startsWith("{\"errors"): + let errors = result.fromJson(Errors) + if errors in {invalidToken, forbidden, badToken}: + echo "fetch error: ", errors + release(token, invalid=true) + raise rateLimitError() diff --git a/src/auth.nim b/src/auth.nim deleted file mode 100644 index 259c360..0000000 --- a/src/auth.nim +++ /dev/null @@ -1,228 +0,0 @@ -#SPDX-License-Identifier: AGPL-3.0-only -import std/[asyncdispatch, times, json, random, strutils, tables, packedsets, os] -import types, consts -import experimental/parser/session - -const hourInSeconds = 60 * 60 - -var - sessionPool: seq[Session] - enableLogging = false - # max requests at a time per session to avoid race conditions - maxConcurrentReqs = 2 - -proc setMaxConcurrentReqs*(reqs: int) = - if reqs > 0: - maxConcurrentReqs = reqs - -template log(str: varargs[string, `$`]) = - echo "[sessions] ", str.join("") - -proc endpoint*(req: ApiReq; session: Session): string = - case session.kind - of oauth: req.oauth.endpoint - of cookie: req.cookie.endpoint - -proc pretty*(session: Session): string = - if session.isNil: - return "" - - if session.id > 0 and session.username.len > 0: - result = $session.id & " (" & session.username & ")" - elif session.username.len > 0: - result = session.username - elif session.id > 0: - result = $session.id - else: - result = "" - result = $session.kind & " " & result - -proc snowflakeToEpoch(flake: int64): int64 = - int64(((flake shr 22) + 1288834974657) div 1000) - -proc getSessionPoolHealth*(): JsonNode = - let now = epochTime().int - - var - totalReqs = 0 - limited: PackedSet[int64] - reqsPerApi: Table[string, int] - oldest = now.int64 - newest = 0'i64 - average = 0'i64 - oauthTotal, cookieTotal = 0 - oauthLimited, cookieLimited = 0 - - for session in sessionPool: - let created = snowflakeToEpoch(session.id) - if created > newest: - newest = created - if created < oldest: - oldest = created - average += created - - case session.kind - of oauth: inc oauthTotal - of cookie: inc cookieTotal - - if session.limited: - limited.incl session.id - case session.kind - of oauth: inc oauthLimited - of cookie: inc cookieLimited - - for api in session.apis.keys: - let - apiStatus = session.apis[api] - reqs = apiStatus.limit - apiStatus.remaining - - # no requests made with this session and endpoint since the limit reset - if apiStatus.reset < now: - continue - - reqsPerApi.mgetOrPut($api, 0).inc reqs - totalReqs.inc reqs - - if sessionPool.len > 0: - average = average div sessionPool.len - else: - oldest = 0 - average = 0 - - return %*{ - "sessions": %*{ - "total": sessionPool.len, - "limited": limited.card, - "oauth": %*{"total": oauthTotal, "limited": oauthLimited}, - "cookie": %*{"total": cookieTotal, "limited": cookieLimited}, - "oldest": $fromUnix(oldest), - "newest": $fromUnix(newest), - "average": $fromUnix(average) - }, - "requests": %*{ - "total": totalReqs, - "apis": reqsPerApi - } - } - -proc getSessionPoolDebug*(): JsonNode = - let now = epochTime().int - var list = newJObject() - - for session in sessionPool: - let sessionJson = %*{ - "kind": $session.kind, - "apis": newJObject(), - "pending": session.pending, - } - - if session.limited: - sessionJson["limited"] = %true - - for api in session.apis.keys: - let - apiStatus = session.apis[api] - obj = %*{} - - if apiStatus.reset > now.int: - obj["remaining"] = %apiStatus.remaining - obj["reset"] = %apiStatus.reset - - if "remaining" notin obj: - continue - - sessionJson{"apis", $api} = obj - list[$session.id] = sessionJson - - return %list - -proc rateLimitError*(): ref RateLimitError = - newException(RateLimitError, "rate limited") - -proc noSessionsError*(): ref NoSessionsError = - newException(NoSessionsError, "no sessions available") - -proc isLimited(session: Session; req: ApiReq): bool = - if session.isNil: - return true - - let api = req.endpoint(session) - if session.limited and api != graphUserTweetsV2: - if (epochTime().int - session.limitedAt) > hourInSeconds: - session.limited = false - log "resetting limit: ", session.pretty - return false - else: - return true - - if api in session.apis: - let limit = session.apis[api] - return limit.remaining <= 10 and limit.reset > epochTime().int - else: - return false - -proc isReady(session: Session; req: ApiReq): bool = - not (session.isNil or session.pending > maxConcurrentReqs or session.isLimited(req)) - -proc invalidate*(session: var Session) = - if session.isNil: return - log "invalidating: ", session.pretty - - # TODO: This isn't sufficient, but it works for now - let idx = sessionPool.find(session) - if idx > -1: sessionPool.delete(idx) - session = nil - -proc release*(session: Session) = - if session.isNil: return - dec session.pending - -proc getSession*(req: ApiReq): Future[Session] {.async.} = - for i in 0 ..< sessionPool.len: - if result.isReady(req): break - result = sessionPool.sample() - - if not result.isNil and result.isReady(req): - inc result.pending - else: - if result.isNil: - log "no sessions available for API: ", req.cookie.endpoint - else: - log "no sessions available for API: ", req.endpoint(result), ", last tried: ", result.pretty - raise noSessionsError() - -proc setLimited*(session: Session; req: ApiReq) = - let api = req.endpoint(session) - session.limited = true - session.limitedAt = epochTime().int - log "rate limited by api: ", api, ", reqs left: ", session.apis[api].remaining, ", ", session.pretty - -proc setRateLimit*(session: Session; req: ApiReq; remaining, reset, limit: int) = - # avoid undefined behavior in race conditions - let api = req.endpoint(session) - if api in session.apis: - let rateLimit = session.apis[api] - if rateLimit.reset >= reset and rateLimit.remaining < remaining: - return - if rateLimit.reset == reset and rateLimit.remaining >= remaining: - session.apis[api].remaining = remaining - return - - session.apis[api] = RateLimit(limit: limit, remaining: remaining, reset: reset) - -proc initSessionPool*(cfg: Config; path: string) = - enableLogging = cfg.enableDebug - - if path.endsWith(".json"): - log "ERROR: .json is not supported, the file must be a valid JSONL file ending in .jsonl" - quit 1 - - if not fileExists(path): - log "ERROR: ", path, " not found. This file is required to authenticate API requests." - quit 1 - - log "parsing JSONL account sessions file: ", path - for line in path.lines: - sessionPool.add parseSession(line) - - log "successfully added ", sessionPool.len, " valid account sessions" diff --git a/src/config.nim b/src/config.nim index b46a979..1b05ffe 100644 --- a/src/config.nim +++ b/src/config.nim @@ -13,8 +13,6 @@ proc get*[T](config: parseCfg.Config; section, key: string; default: T): T = proc getConfig*(path: string): (Config, parseCfg.Config) = var cfg = loadConfig(path) - let masterRss = cfg.get("Config", "enableRSS", true) - let conf = Config( # Server address: cfg.get("Server", "address", "0.0.0.0"), @@ -39,20 +37,10 @@ proc getConfig*(path: string): (Config, parseCfg.Config) = hmacKey: cfg.get("Config", "hmacKey", "secretkey"), base64Media: cfg.get("Config", "base64Media", false), minTokens: cfg.get("Config", "tokenCount", 10), - enableRSSUserTweets: masterRss and cfg.get("Config", "enableRSSUserTweets", true), - enableRSSUserReplies: masterRss and cfg.get("Config", "enableRSSUserReplies", true), - enableRSSUserMedia: masterRss and cfg.get("Config", "enableRSSUserMedia", true), - enableRSSUserArticles: masterRss and cfg.get("Config", "enableRSSUserArticles", true), - enableRSSSearch: masterRss and cfg.get("Config", "enableRSSSearch", true), - enableRSSList: masterRss and cfg.get("Config", "enableRSSList", true), + enableRss: cfg.get("Config", "enableRSS", true), enableDebug: cfg.get("Config", "enableDebug", false), proxy: cfg.get("Config", "proxy", ""), - proxyAuth: cfg.get("Config", "proxyAuth", ""), - apiProxy: cfg.get("Config", "apiProxy", ""), - disableTid: cfg.get("Config", "disableTid", false), - maxConcurrentReqs: cfg.get("Config", "maxConcurrentReqs", 2), - maxRetries: cfg.get("Config", "maxRetries", 1), - retryDelayMs: cfg.get("Config", "retryDelayMs", 150) + proxyAuth: cfg.get("Config", "proxyAuth", "") ) return (conf, cfg) diff --git a/src/consts.nim b/src/consts.nim index c88ae68..3687a54 100644 --- a/src/consts.nim +++ b/src/consts.nim @@ -1,219 +1,59 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils +import uri, sequtils const - consumerKey* = "3nVuSoBZnx6U4vzUxf5w" - consumerSecret* = "Bcs59EFbbsdF6Sl9Ng71smgStWEGwXXKSjYvPVt7qys" - bearerToken* = "Bearer AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8xnZz4puTs%3D1Zv7ttfk8LF81IUq16cHjhLTvJu4FA33AGWWjCpTnA" - bearerToken2* = "Bearer AAAAAAAAAAAAAAAAAAAAAFXzAwAAAAAAMHCxpeSDG1gLNLghVe8d74hl6k4%3DRUMF4xAQLsbeBhTSRrCiQpJtxoGWeyHrDb5te2jpGskWDFW82F" + auth* = "Bearer AAAAAAAAAAAAAAAAAAAAAPYXBAAAAAAACLXUNDekMxqa8h%2F40K4moUkGsoc%3DTYfbDKbT3jJPCEVnMYqilB28NHfOPqkca3qaAxGfsyKCs0wRbw" - graphUser* = "Gb-d6r0vxPOADdG62OEBpQ/UserByScreenName" - graphUserV2* = "-ZzAG_Bckx16LMbEvHC3lg/UserResultByScreenNameQuery" - graphUserById* = "xvmVfRLmnr1alc5f2dib0Q/UserByRestId" - graphUserTweetsV2* = "LE3eTyeqhBh2g-fX85O2eQ/UserWithProfileTweetsQueryV2" - graphUserTweetsAndRepliesV2* = "AcYHjc_YAx-9_rKWdMsKvA/UserWithProfileTweetsAndRepliesQueryV2" - graphUserTweets* = "SXVCYB8XHSS25nzIljNtZA/UserTweets" - graphUserTweetsAndReplies* = "qUpkZU6eN8MbtQb7rC_pYg/UserTweetsAndReplies" - graphUserMedia* = "VyudDWQnr9vJNw7GasFz2g/UserMedia" - graphUserMediaV2* = "WK111rbR0vM0ZX4lyZCYjw/MediaTimelineV2" - graphUserArticles* = "ZmMjUyrTpwYfTGAdylEyMw/UserArticlesTweets" - graphUserArticlesV2* = "PsGixN38UZz2RheyayNB5Q/UserProfileArticlesTimelineQuery" - graphTweet* = "OZMbEnEa96AN8Pq6HyTWdw/ConversationTimeline" - graphTweetDetail* = "XMOz5h24KAZ86qKffKTLdQ/TweetDetail" - graphTweetResult* = "xYOrBQoTlfKJJPsX76MZEw/TweetResultByIdQuery" - graphTweetEditHistory* = "1izbuOcH_QpuMcyCxOXkAg/TweetEditHistory" - graphSearchTimeline* = "hyPfJYJ_XAtDYoslQc-Rgg/SearchTimeline" + api = parseUri("https://api.twitter.com") + activate* = $(api / "1.1/guest/activate.json") - graphListById* = "niz0TtOxL2zIcbq6_NQiNw/ListByRestId" - graphListBySlug* = "RqkWNDQpOntlxNtJa4RIoQ/ListBySlug" - graphListMembers* = "8rYmkvWQe9jRRZdy_-vkGA/ListMembers" - graphListTweets* = "0QJtcuMzVywHGAWD6Dtjlw/ListTimeline" - graphAboutAccount* = "TzOG2twZEfhr9KmClvVVqA/AboutAccountQuery" + userShow* = api / "1.1/users/show.json" + photoRail* = api / "1.1/statuses/media_timeline.json" + status* = api / "1.1/statuses/show" + search* = api / "2/search/adaptive.json" - graphCommunity* = "-ElI1vg3dYbttVMhBhGdLw/CommunityQuery" - graphCommunityTweets* = "EwftYyqQemkckQ0wzGM6uw/CommunityTweetsTimeline" - graphCommunityMedia* = "ESJtwnI_apuGesbJncpc0Q/CommunityMediaTimeline" - graphCommunityMembers* = "woAp_YdzAdqnWDrqLTNpAw/membersSliceTimeline_Query" - graphCommunityModerators* = "0oYT9GRiWUhrz5xoqFE9uw/moderatorsSliceTimeline_Query" - graphCommunityHashtags* = "D5EqomOIWeJnSkMhL-FLew/CommunityHashtagsTimeline" + timelineApi = api / "2/timeline" + timeline* = timelineApi / "profile" + mediaTimeline* = timelineApi / "media" + listTimeline* = timelineApi / "list.json" + tweet* = timelineApi / "conversation" - graphTweetResultByRestId* = "GZsN2Pc4knAoit6pXa4HSA/TweetResultByRestId" - graphTweetResultsByRestIds* = "Pho4sg8jLcrVlMeclMayrg/TweetResultsByRestIds" + graphql = api / "graphql" + graphUser* = graphql / "I5nvpI91ljifos1Y3Lltyg/UserByRestId" + graphList* = graphql / "JADTh6cjebfgetzvF3tQvQ/List" + graphListBySlug* = graphql / "ErWsz9cObLel1BF-HjuBlA/ListBySlug" + graphListMembers* = graphql / "Ke6urWMeCV2UlKXGRy4sow/ListMembers" - graphBroadcast* = "RG6wSogandh6WPIzxW9aag/BroadcastQuery" - graphAudioSpace* = "Bh0L6azTQoMs9rJKeCF4wQ/AudioSpaceById" - restLiveStream* = "1.1/live_video_stream/status/" + timelineParams* = { + "include_profile_interstitial_type": "0", + "include_blocking": "0", + "include_blocked_by": "0", + "include_followed_by": "0", + "include_want_retweets": "0", + "include_mute_edge": "0", + "include_can_dm": "0", + "include_can_media_tag": "1", + "skip_status": "1", + "cards_platform": "Web-12", + "include_cards": "1", + "include_composer_source": "false", + "include_reply_count": "1", + "tweet_mode": "extended", + "include_entities": "true", + "include_user_entities": "true", + "include_ext_media_color": "false", + "send_error_codes": "true", + "simple_quoted_tweet": "true", + "include_quote_count": "true" + }.toSeq - graphFollowers* = "JNyQdTISpzCkj_1fqxDvFg/Followers" - graphFollowing* = "qGZZDF3mp91q7X22s3HxpA/Following" - - gqlFeatures* = """{ - "rweb_video_screen_enabled": false, - "rweb_cashtags_enabled": true, - "profile_label_improvements_pcf_label_in_post_enabled": true, - "responsive_web_profile_redirect_enabled": false, - "rweb_tipjar_consumption_enabled": false, - "verified_phone_label_enabled": false, - "creator_subscriptions_tweet_preview_api_enabled": true, - "responsive_web_graphql_timeline_navigation_enabled": true, - "responsive_web_graphql_skip_user_profile_image_extensions_enabled": false, - "premium_content_api_read_enabled": false, - "communities_web_enable_tweet_community_results_fetch": true, - "c9s_tweet_anatomy_moderator_badge_enabled": true, - "c9s_list_members_action_api_enabled": false, - "c9s_superc9s_indication_enabled": false, - "responsive_web_grok_analyze_button_fetch_trends_enabled": false, - "responsive_web_grok_analyze_post_followups_enabled": true, - "rweb_cashtags_composer_attachment_enabled": true, - "responsive_web_jetfuel_frame": true, - "responsive_web_grok_share_attachment_enabled": true, - "responsive_web_grok_annotations_enabled": true, - "articles_preview_enabled": true, - "responsive_web_edit_tweet_api_enabled": true, - "rweb_conversational_replies_downvote_enabled": false, - "graphql_is_translatable_rweb_tweet_is_translatable_enabled": true, - "view_counts_everywhere_api_enabled": true, - "longform_notetweets_consumption_enabled": true, - "responsive_web_twitter_article_tweet_consumption_enabled": true, - "content_disclosure_indicator_enabled": true, - "content_disclosure_ai_generated_indicator_enabled": true, - "responsive_web_grok_show_grok_translated_post": true, - "responsive_web_grok_analysis_button_from_backend": true, - "post_ctas_fetch_enabled": true, - "freedom_of_speech_not_reach_fetch_enabled": true, - "standardized_nudges_misinfo": true, - "tweet_with_visibility_results_prefer_gql_limited_actions_policy_enabled": true, - "longform_notetweets_rich_text_read_enabled": true, - "longform_notetweets_inline_media_enabled": false, - "responsive_web_grok_image_annotation_enabled": true, - "responsive_web_grok_imagine_annotation_enabled": true, - "responsive_web_grok_community_note_auto_translation_is_enabled": true, - "responsive_web_enhance_cards_enabled": false -}""".replace(" ", "").replace("\n", "") - - tweetVars* = """{ - "postId": "$1", - $2 - "ranking_mode": "$3", - "includeHasBirdwatchNotes": false, - "includePromotedContent": false, - "withBirdwatchNotes": true, - "withVoice": false, - "withV2Timeline": true -}""".replace(" ", "").replace("\n", "") - - tweetDetailVars* = """{ - "focalTweetId": "$1", - $2 - "referrer": "profile", - "with_rux_injections": false, - "rankingMode": "Relevance", - "includePromotedContent": true, - "withCommunity": true, - "withQuickPromoteEligibilityTweetFields": true, - "withBirdwatchNotes": true, - "withVoice": true -}""".replace(" ", "").replace("\n", "") - - tweetEditHistoryVars* = """{ - "tweetId": "$1", - "withQuickPromoteEligibilityTweetFields": true -}""".replace(" ", "").replace("\n", "") - - restIdVars* = """{ - "rest_id": "$1", $2 - "count": $3 -}""".replace(" ", "").replace("\n", "") - - userMediaVars* = """{ - "userId": "$1", $2 - "count": $3, - "includePromotedContent": false, - "withClientEventToken": false, - "withBirdwatchNotes": false, - "withVoice": true -}""".replace(" ", "").replace("\n", "") - - userTweetsVars* = """{ - "userId": "$1", $2 - "count": 20, - "includePromotedContent": false, - "withQuickPromoteEligibilityTweetFields": true, - "withVoice": true -}""".replace(" ", "").replace("\n", "") - - userTweetsAndRepliesVars* = """{ - "userId": "$1", $2 - "count": 20, - "includePromotedContent": false, - "withCommunity": true, - "withVoice": true -}""".replace(" ", "").replace("\n", "") - - userArticlesVars* = """{ - "userId": "$1", $2 - "count": 20, - "includePromotedContent": false, - "withVoice": true -}""".replace(" ", "").replace("\n", "") - - articleVars* = """{ - "tweetId": "$1", - "includePromotedContent": false, - "withBirdwatchNotes": true, - "withVoice": true, - "withCommunity": true -}""".replace(" ", "").replace("\n", "") - - articleBatchVars* = """{ - "tweetIds": $1, - "includePromotedContent": false, - "withBirdwatchNotes": true, - "withVoice": true, - "withCommunity": true -}""".replace(" ", "").replace("\n", "") - - articleFieldToggles* = """{"withArticleRichContentState":true,"withArticlePlainText":false,"withArticleSummaryText":true,"withArticleVoiceOver":true}""" - - tweetByRestIdVars* = """{ - "tweetId": "$1", - "includePromotedContent": false, - "withBirdwatchNotes": false, - "withVoice": false, - "withCommunity": false -}""".replace(" ", "").replace("\n", "") - - userByRestIdVars* = """{ - "userId": "$1", - "withSafetyModeUserFields": true -}""".replace(" ", "").replace("\n", "") - - communityTweetsVars* = """{ - "communityId": "$1", $2 - "count": 20, - "displayLocation": "Community", - "rankingMode": "$3", - "withCommunity": true -}""".replace(" ", "").replace("\n", "") - - communityMediaVars* = """{ - "communityId": "$1", $2 - "count": 20, - "withCommunity": true -}""".replace(" ", "").replace("\n", "") - - communityMembersVars* = """{ - "communityId": "$1", - "cursor": $2 -}""".replace(" ", "").replace("\n", "") - - communityHashtagsVars* = """{ - "communityId": "$1", $2 - "count": 20, - "hashtags": ["$3"], - "withCommunity": true -}""".replace(" ", "").replace("\n", "") - - userFieldToggles = """{"withPayments":false,"withAuxiliaryUserLabels":true}""" - userTweetsFieldToggles* = """{"withArticleRichContentState":true,"withArticlePlainText":false}""" - tweetDetailFieldToggles* = """{"withArticleRichContentState":true,"withArticlePlainText":false,"withGrokAnalyze":false,"withDisallowedReplyControls":false}""" + searchParams* = { + "query_source": "typed_query", + "pc": "1", + "spelling_corrections": "1" + }.toSeq + ## top: nothing + ## latest: "tweet_search_mode: live" + ## user: "result_filter: user" + ## photos: "result_filter: photos" + ## videos: "result_filter: videos" diff --git a/src/experimental/parser.nim b/src/experimental/parser.nim index e22a51f..98ce7df 100644 --- a/src/experimental/parser.nim +++ b/src/experimental/parser.nim @@ -1,2 +1,2 @@ -import parser/[user, graphql, article] -export user, graphql, article +import parser/[user, graphql, timeline] +export user, graphql, timeline diff --git a/src/experimental/parser/article.nim b/src/experimental/parser/article.nim deleted file mode 100644 index ae5de7c..0000000 --- a/src/experimental/parser/article.nim +++ /dev/null @@ -1,88 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import std/[strutils, tables, times, options] -import jsony -import utils, graphql, ../types/article -from ../../types import Article, ArticleParagraph, ArticleEntity, ArticleMedia, - User, TweetStats - -proc parseGraphArticle*(json: string): Article = - if json.len == 0 or json[0] != '{': - return - - var raw: GraphArticle - try: - raw = json.fromJson(GraphArticle) - except CatchableError: - return - - let - tweet = raw.data.tweetResult.result - article = tweet.article.articleResults.result - - if article.title.len == 0: - return - - let publishedAt = article.metadata.firstPublishedAtSecs - var articleTime: DateTime - if publishedAt > 0: - articleTime = publishedAt.int64.fromUnix.utc - elif tweet.legacy.createdAt.len > 0: - articleTime = parseTwitterDate(tweet.legacy.createdAt) - - result = Article( - title: article.title, - coverImage: getImageUrl(article.coverMedia.mediaInfo.originalImgUrl), - time: articleTime, - user: parseUserResult(tweet.core.userResults.result), - ) - - result.stats = TweetStats( - replies: tweet.legacy.replyCount, - retweets: tweet.legacy.retweetCount, - likes: tweet.legacy.favoriteCount, - ) - if tweet.views.count.len > 0: - try: result.stats.views = parseInt(tweet.views.count) - except ValueError: discard - - for blk in article.contentState.blocks: - result.paragraphs.add ArticleParagraph( - text: blk.text, - kind: blk.blockKind, - inlineStyles: blk.inlineStyleRanges, - entityRanges: blk.entityRanges, - ) - - for entry in article.contentState.entityMap: - let key = try: parseInt(entry.key) except ValueError: continue - var entity = ArticleEntity(kind: entry.value.entityKind) - case entity.kind - of "LINK": entity.url = entry.value.data.url - of "MEDIA": - for mi in entry.value.data.mediaItems: - entity.mediaIds.add mi.mediaId - entity.caption = entry.value.data.caption - of "TWEET": entity.tweetId = entry.value.data.tweetId - of "MARKDOWN": entity.markdown = entry.value.data.markdown - else: discard - result.entities[key] = entity - - for me in article.mediaEntities: - let typeName = me.mediaInfo.typeName - var media = ArticleMedia(kind: typeName) - if me.mediaInfo.videoInfo.isSome: - let variants = me.mediaInfo.videoInfo.get.variants - case typeName - of "ApiGif": - if variants.len > 0: - media.url = variants[0].url - of "ApiVideo": - var bestBitrate = -1 - for v in variants: - if v.bitrate > bestBitrate: - bestBitrate = v.bitrate - media.url = v.url - else: discard - elif typeName == "ApiImage": - media.url = getImageUrl(me.mediaInfo.originalImgUrl) - result.media[me.mediaId] = media diff --git a/src/experimental/parser/graphql.nim b/src/experimental/parser/graphql.nim index 85be202..b00ab24 100644 --- a/src/experimental/parser/graphql.nim +++ b/src/experimental/parser/graphql.nim @@ -1,73 +1,11 @@ -import options, strutils import jsony -import user, utils, ../types/[graphuser, graphlistmembers, graphfollowers] -from ../../types import User, VerifiedType, Result, Query, QueryKind - -proc parseUserResult*(userResult: UserResult): User = - result = userResult.legacy - - if result.verifiedType == none and userResult.isBlueVerified: - result.verifiedType = blue - - if result.username.len == 0 and userResult.core.screenName.len > 0: - # Modern UserByRestId/UserByScreenName shape: `legacy` is empty and the data - # lives in typed sub-objects (core/relationship_counts/tweet_counts/etc.). - result.id = userResult.restId - result.username = userResult.core.screenName - result.fullname = userResult.core.name - result.userPic = userResult.avatar.imageUrl.replace("_normal", "") - - if userResult.banner.imageUrl.len > 0: - result.banner = userResult.banner.imageUrl & "/1500x500" - - if userResult.privacy.isSome: - result.protected = userResult.privacy.get.protected - - if userResult.location.isSome: - result.location = userResult.location.get.location - - if userResult.core.createdAt.len > 0: - result.joinDate = parseTwitterDate(userResult.core.createdAt) - - if userResult.verification.isSome: - let v = userResult.verification.get - if v.verifiedType != VerifiedType.none: - result.verifiedType = v.verifiedType - - if userResult.relationshipCounts.isSome: - let rc = userResult.relationshipCounts.get - result.followers = rc.followers - result.following = rc.following - - if userResult.tweetCounts.isSome: - let tc = userResult.tweetCounts.get - result.tweets = tc.tweets - result.media = tc.mediaTweets - - if userResult.actionCounts.isSome: - result.likes = userResult.actionCounts.get.favoritesCount - - if userResult.profileBio.isSome: - let bio = userResult.profileBio.get - result.bio = bio.description - result.expandUserEntities(bio.entities) +import user, ../types/[graphuser, graphlistmembers] +from ../../types import User, Result, Query, QueryKind proc parseGraphUser*(json: string): User = - if json.len == 0 or json[0] != '{': - return - - let - raw = json.fromJson(GraphUser) - userResult = - if raw.data.userResult.isSome: raw.data.userResult.get.result - elif raw.data.user.isSome: raw.data.user.get.result - else: UserResult() - - if userResult.unavailableReason.get("") == "Suspended" or - userResult.reason.get("") == "Suspended": - return User(suspended: true) - - result = parseUserResult(userResult) + let raw = json.fromJson(GraphUser) + result = toUser raw.data.user.result.legacy + result.id = raw.data.user.result.restId proc parseGraphListMembers*(json, cursor: string): Result[User] = result = Result[User]( @@ -83,29 +21,7 @@ proc parseGraphListMembers*(json, cursor: string): Result[User] = of TimelineTimelineItem: let userResult = entry.content.itemContent.userResults.result if userResult.restId.len > 0: - result.content.add parseUserResult(userResult) - of TimelineTimelineCursor: - if entry.content.cursorType == "Bottom": - result.bottom = entry.content.value - -proc parseGraphFollowers*(json, cursor: string; kind: QueryKind): Result[User] = - result = Result[User]( - beginning: cursor.len == 0, - query: Query(kind: kind) - ) - - if json.len == 0 or json[0] != '{': - return - - let raw = json.fromJson(GraphFollowers) - for instruction in raw.data.user.result.timeline.timeline.instructions: - if instruction.kind == "TimelineAddEntries": - for entry in instruction.entries: - case entry.content.entryType - of TimelineTimelineItem: - let userResult = entry.content.itemContent.userResults.result - if userResult.restId.len > 0: - result.content.add parseUserResult(userResult) + result.content.add toUser userResult.legacy of TimelineTimelineCursor: if entry.content.cursorType == "Bottom": result.bottom = entry.content.value diff --git a/src/experimental/parser/media.nim b/src/experimental/parser/media.nim new file mode 100644 index 0000000..8af5c8b --- /dev/null +++ b/src/experimental/parser/media.nim @@ -0,0 +1,44 @@ +import std/[json, strutils, times, math] +import utils +import ".."/types/[media, tweet] +from ../../types import Poll, Gif, Video, VideoVariant, VideoType + +proc parseVideo*(entity: Entity): Video = + result = Video( + thumb: entity.mediaUrlHttps.getImageUrl, + views: entity.ext.mediaStats{"r", "ok", "viewCount"}.getStr, + available: entity.extMediaAvailability.status == "available", + title: entity.extAltText, + durationMs: entity.videoInfo.durationMillis, + description: entity.additionalMediaInfo.description, + variants: entity.videoInfo.variants + # playbackType: mp4 + ) + + if entity.additionalMediaInfo.title.len > 0: + result.title = entity.additionalMediaInfo.title + +proc parseGif*(entity: Entity): Gif = + result = Gif( + url: entity.videoInfo.variants[0].url.getImageUrl, + thumb: entity.getImageUrl + ) + +proc parsePoll*(card: Card): Poll = + let vals = card.bindingValues + + # name format is pollNchoice_* + for i in '1' .. card.name[4]: + let choice = "choice" & i + result.values.add parseInt(vals{choice & "_count", "string_value"}.getStr("0")) + result.options.add vals{choice & "_label", "string_value"}.getStr + + let time = vals{"end_datetime_utc", "string_value"}.getStr.parseIsoDate + if time > now(): + let timeLeft = $(time - now()) + result.status = timeLeft[0 ..< timeLeft.find(",")] + else: + result.status = "Final results" + + result.leader = result.values.find(max(result.values)) + result.votes = result.values.sum diff --git a/src/experimental/parser/session.nim b/src/experimental/parser/session.nim deleted file mode 100644 index 2e5a171..0000000 --- a/src/experimental/parser/session.nim +++ /dev/null @@ -1,30 +0,0 @@ -import std/strutils -import jsony -import ../types/session -from ../../types import Session, SessionKind - -proc parseSession*(raw: string): Session = - let session = raw.fromJson(RawSession) - let kind = if session.kind == "": "oauth" else: session.kind - - case kind - of "oauth": - let id = session.oauthToken[0 ..< session.oauthToken.find('-')] - result = Session( - kind: SessionKind.oauth, - id: parseBiggestInt(id), - username: session.username, - oauthToken: session.oauthToken, - oauthSecret: session.oauthTokenSecret - ) - of "cookie": - let id = if session.id.len > 0: parseBiggestInt(session.id) else: 0 - result = Session( - kind: SessionKind.cookie, - id: id, - username: session.username, - authToken: session.authToken, - ct0: session.ct0 - ) - else: - raise newException(ValueError, "Unknown session kind: " & kind) diff --git a/src/experimental/parser/slices.nim b/src/experimental/parser/slices.nim index db2c98d..887b8cc 100644 --- a/src/experimental/parser/slices.nim +++ b/src/experimental/parser/slices.nim @@ -1,15 +1,14 @@ import std/[macros, htmlgen, unicode] -import ../types/common import ".."/../[formatters, utils] type - ReplaceSliceKind = enum + ReplaceSliceKind* = enum rkRemove, rkUrl, rkHashtag, rkMention ReplaceSlice* = object - slice: Slice[int] - kind: ReplaceSliceKind - url, display: string + slice*: Slice[int] + kind*: ReplaceSliceKind + url*, display*: string proc cmp*(x, y: ReplaceSlice): int = cmp(x.slice.a, y.slice.b) @@ -27,11 +26,14 @@ proc dedupSlices*(s: var seq[ReplaceSlice]) = inc j inc i -proc extractUrls*(result: var seq[ReplaceSlice]; url: Url; - textLen: int; hideTwitter = false) = +proc extractHashtags*(result: var seq[ReplaceSlice]; slice: Slice[int]) = + result.add ReplaceSlice(kind: rkHashtag, slice: slice) + +proc extractUrls*[T](result: var seq[ReplaceSlice]; entity: T; + textLen: int; hideTwitter = false) = let - link = url.expandedUrl - slice = url.indices[0] ..< url.indices[1] + link = entity.expandedUrl + slice = entity.indices if hideTwitter and slice.b.succ >= textLen and link.isTwitterUrl: if slice.a < textLen: @@ -54,7 +56,7 @@ proc replacedWith*(runes: seq[Rune]; repls: openArray[ReplaceSlice]; let name = $runes[rep.slice.a.succ .. rep.slice.b] symbol = $runes[rep.slice.a] - result.add a(symbol & name, href = "/search?f=tweets&q=%23" & name) + result.add a(symbol & name, href = "/search?q=%23" & name) of rkMention: result.add a($runes[rep.slice], href = rep.url, title = rep.display) of rkUrl: diff --git a/src/experimental/parser/tid.nim b/src/experimental/parser/tid.nim deleted file mode 100644 index 28fccea..0000000 --- a/src/experimental/parser/tid.nim +++ /dev/null @@ -1,8 +0,0 @@ -import jsony -import ../types/tid -export TidPair - -proc parseTidPairs*(raw: string): seq[TidPair] = - result = raw.fromJson(seq[TidPair]) - if result.len == 0: - raise newException(ValueError, "Parsing pairs failed: " & raw) diff --git a/src/experimental/parser/timeline.nim b/src/experimental/parser/timeline.nim new file mode 100644 index 0000000..8f7a8b6 --- /dev/null +++ b/src/experimental/parser/timeline.nim @@ -0,0 +1,84 @@ +import std/[strutils, tables, options] +import jsony +import user, tweet, utils, ../types/timeline +from ../../types import Result, User, Tweet + +proc parseHook(s: string; i: var int; v: var Slice[int]) = + var slice: array[2, int] + parseHook(s, i, slice) + v = slice[0] ..< slice[1] + +proc getId(id: string): string {.inline.} = + let start = id.rfind("-") + if start < 0: return id + id[start + 1 ..< id.len] + +proc processTweet(id: string; objects: GlobalObjects; + userCache: var Table[string, User]): Tweet = + let raw = objects.tweets[id] + result = toTweet raw + + let uid = result.user.id + if uid.len > 0 and uid in objects.users: + if uid notin userCache: + userCache[uid] = toUser objects.users[uid] + result.user = userCache[uid] + + let rtId = raw.retweetedStatusIdStr + if rtId.len > 0: + if rtId in objects.tweets: + result.retweet = some processTweet(rtId, objects, userCache) + else: + result.retweet = some Tweet(id: rtId.toId) + + let qId = raw.quotedStatusIdStr + if qId.len > 0: + if qId in objects.tweets: + result.quote = some processTweet(qId, objects, userCache) + else: + result.quote = some Tweet(id: qId.toId) + +proc parseCursor[T](e: Entry; result: var Result[T]) = + let cursor = e.content.operation.cursor + if cursor.cursorType == "Top": + result.top = cursor.value + elif cursor.cursorType == "Bottom": + result.bottom = cursor.value + +proc parseUsers*(json: string; after=""): Result[User] = + result = Result[User](beginning: after.len == 0) + + let raw = json.fromJson(Search) + if raw.timeline.instructions.len == 0: + return + + for e in raw.timeline.instructions[0].addEntries.entries: + let + eId = e.entryId + id = eId.getId + + if eId.startsWith("user") or eId.startsWith("sq-U"): + if id in raw.globalObjects.users: + result.content.add toUser raw.globalObjects.users[id] + elif eId.startsWith("cursor") or eId.startsWith("sq-C"): + parseCursor(e, result) + +proc parseTweets*(json: string; after=""): Result[Tweet] = + result = Result[Tweet](beginning: after.len == 0) + + let raw = json.fromJson(Search) + if raw.timeline.instructions.len == 0: + return + + var userCache: Table[string, User] + + for e in raw.timeline.instructions[0].addEntries.entries: + let + eId = e.entryId + id = eId.getId + + if eId.startsWith("tweet") or eId.startsWith("sq-I-t"): + if id in raw.globalObjects.tweets: + result.content.add processTweet(id, raw.globalObjects, userCache) + elif eId.startsWith("cursor") or eId.startsWith("sq-C"): + parseCursor(e, result) diff --git a/src/experimental/parser/tweet.nim b/src/experimental/parser/tweet.nim new file mode 100644 index 0000000..f7e31bb --- /dev/null +++ b/src/experimental/parser/tweet.nim @@ -0,0 +1,97 @@ +import std/[strutils, options, algorithm, json] +import std/unicode except strip +import utils, slices, media, user +import ../types/tweet +from ../types/media as mediaTypes import MediaType +from ../../types import Tweet, User, TweetStats + +proc expandTweetEntities(tweet: var Tweet; raw: RawTweet) = + let + orig = raw.fullText.toRunes + textRange = raw.displayTextRange + textSlice = textRange[0] .. textRange[1] + hasCard = raw.card.isSome + + var replyTo = "" + if tweet.replyId > 0: + tweet.reply.add raw.inReplyToScreenName + replyTo = raw.inReplyToScreenName + + var replacements = newSeq[ReplaceSlice]() + + for u in raw.entities.urls: + if u.url.len == 0 or u.url notin raw.fullText: + continue + + replacements.extractUrls(u, textSlice.b, hideTwitter=raw.isQuoteStatus) + # if hasCard and u.url == get(tweet.card).url: + # get(tweet.card).url = u.expandedUrl + + for m in raw.entities.media: + replacements.extractUrls(m, textSlice.b, hideTwitter=true) + + for hashtag in raw.entities.hashtags: + replacements.extractHashtags(hashtag.indices) + + for symbol in raw.entities.symbols: + replacements.extractHashtags(symbol.indices) + + for mention in raw.entities.userMentions: + let + name = mention.screenName + idx = tweet.reply.find(name) + + if mention.indices.a >= textSlice.a: + replacements.add ReplaceSlice(kind: rkMention, slice: mention.indices, + url: "/" & name, display: mention.name) + if idx > -1 and name != replyTo: + tweet.reply.delete idx + elif idx == -1 and tweet.replyId != 0: + tweet.reply.add name + + replacements.dedupSlices + replacements.sort(cmp) + + tweet.text = orig.replacedWith(replacements, textSlice) + .strip(leading=false) + +proc toTweet*(raw: RawTweet): Tweet = + result = Tweet( + id: raw.idStr.toId, + threadId: raw.conversationIdStr.toId, + replyId: raw.inReplyToStatusIdStr.toId, + time: parseTwitterDate(raw.createdAt), + hasThread: raw.selfThread.idStr.len > 0, + available: true, + user: User(id: raw.userIdStr), + stats: TweetStats( + replies: raw.replyCount, + retweets: raw.retweetCount, + likes: raw.favoriteCount, + quotes: raw.quoteCount + ) + ) + + result.expandTweetEntities(raw) + + if raw.card.isSome: + let card = raw.card.get + if "poll" in card.name: + result.poll = some parsePoll(card) + if "image" in card.name: + result.photos.add card.bindingValues{"image_large", "image_value", "url"} + .getStr.getImageUrl + # elif card.name == "amplify": + # discard + # # result.video = some(parsePromoVideo(jsCard{"binding_values"})) + # else: + # result.card = some parseCard(card, raw.entities.urls) + + for m in raw.extendedEntities.media: + case m.kind + of photo: result.photos.add m.getImageUrl + of video: + result.video = some parseVideo(m) + if m.additionalMediaInfo.sourceUser.isSome: + result.attribution = some toUser get(m.additionalMediaInfo.sourceUser) + of animatedGif: result.gif = some parseGif(m) diff --git a/src/experimental/parser/unifiedcard.nim b/src/experimental/parser/unifiedcard.nim index de4df18..1cffb8a 100644 --- a/src/experimental/parser/unifiedcard.nim +++ b/src/experimental/parser/unifiedcard.nim @@ -1,12 +1,8 @@ -import std/[options, tables, strutils, strformat, sugar] +import std/[options, tables, strformat] import jsony -import user, ../types/unifiedcard -import ../../formatters +import utils +import ".."/types/[unifiedcard, media] from ../../types import Card, CardKind, Video -from ../../utils import twimg, https - -proc getImageUrl(entity: MediaEntity): string = - entity.mediaUrlHttps.dup(removePrefix(twimg), removePrefix(https)) proc parseDestination(id: string; card: UnifiedCard; result: var Card) = let destination = card.destinationObjects[id].data @@ -28,14 +24,6 @@ proc parseMediaDetails(data: ComponentData; card: UnifiedCard; result: var Card) result.text = data.topicDetail.title result.dest = "Topic" -proc parseJobDetails(data: ComponentData; card: UnifiedCard; result: var Card) = - data.destination.parseDestination(card, result) - - result.kind = CardKind.jobDetails - result.title = data.title - result.text = data.shortDescriptionText - result.dest = &"@{data.profileUser.username} · {data.location}" - proc parseAppDetails(data: ComponentData; card: UnifiedCard; result: var Card) = let app = card.appStoreData[data.appId][0] @@ -75,20 +63,7 @@ proc parseMedia(component: Component; card: UnifiedCard; result: var Card) = durationMs: videoInfo.durationMillis, variants: videoInfo.variants ) - of model3d: - result.title = "Unsupported 3D model ad" - -proc parseGrokShare(data: ComponentData; card: UnifiedCard; result: var Card) = - result.kind = summaryLarge - - data.destination.parseDestination(card, result) - result.dest = "Answer by Grok" - - for msg in data.conversationPreview: - if msg.sender == "USER": - result.title = msg.message.shorten(70) - elif msg.sender == "AGENT": - result.text = msg.message.shorten(500) + of animatedGif: discard proc parseUnifiedCard*(json: string): Card = let card = json.fromJson(UnifiedCard) @@ -101,18 +76,10 @@ proc parseUnifiedCard*(json: string): Card = component.data.parseAppDetails(card, result) of mediaWithDetailsHorizontal: component.data.parseMediaDetails(card, result) - of media, swipeableMedia: + of ComponentType.media, swipeableMedia: component.parseMedia(card, result) of buttonGroup: discard - of grokShare: - component.data.parseGrokShare(card, result) - of ComponentType.jobDetails: - component.data.parseJobDetails(card, result) - of ComponentType.hidden: - result.kind = CardKind.hidden - of ComponentType.unknown: - echo "ERROR: Unknown component type: ", json case component.kind of twitterListDetails: diff --git a/src/experimental/parser/user.nim b/src/experimental/parser/user.nim index 866973c..dc760f0 100644 --- a/src/experimental/parser/user.nim +++ b/src/experimental/parser/user.nim @@ -1,18 +1,20 @@ -import std/[algorithm, unicode, re, strutils, strformat, options, nre] +import std/[algorithm, unicode, re, strutils, strformat, options] import jsony import utils, slices import ../types/user as userType -from ../../types import Result, User, Error +from ../../types import User, Error let - unRegex = re.re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})" + unRegex = re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})" unReplace = "$1@$2" - htRegex = nre.re"""(*U)(^|[^\w-_.?])([##$])([\w_]*+)(?!|">|#)""" - htReplace = "$1$2$3" + htRegex = re"(^|[^\w-_./?])([##$])([\w_]+)" + htReplace = "$1$2$3" -proc expandUserEntities*(user: var User; ent: Entities) = - let orig = user.bio.toRunes +proc expandUserEntities(user: var User; raw: RawUser) = + let + orig = user.bio.toRunes + ent = raw.entities if ent.url.urls.len > 0: user.website = ent.url.urls[0].expandedUrl @@ -27,7 +29,7 @@ proc expandUserEntities*(user: var User; ent: Entities) = user.bio = orig.replacedWith(replacements, 0 .. orig.len) .replacef(unRegex, unReplace) - .replace(htRegex, htReplace) + .replacef(htRegex, htReplace) proc getBanner(user: RawUser): string = if user.profileBannerUrl.len > 0: @@ -54,21 +56,23 @@ proc toUser*(raw: RawUser): User = tweets: raw.statusesCount, likes: raw.favouritesCount, media: raw.mediaCount, - verifiedType: raw.verifiedType, + verified: raw.verified, protected: raw.protected, + joinDate: parseTwitterDate(raw.createdAt), banner: getBanner(raw), userPic: getImageUrl(raw.profileImageUrlHttps).replace("_normal", "") ) - if raw.createdAt.len > 0: - result.joinDate = parseTwitterDate(raw.createdAt) - if raw.pinnedTweetIdsStr.len > 0: result.pinnedTweet = parseBiggestInt(raw.pinnedTweetIdsStr[0]) - result.expandUserEntities(raw.entities) + result.expandUserEntities(raw) -proc parseHook*(s: string; i: var int; v: var User) = - var u: RawUser - parseHook(s, i, u) - v = toUser u +proc parseUser*(json: string; username=""): User = + handleErrors: + case error.code + of suspended: return User(username: username, suspended: true) + of userNotFound: return + else: echo "[error - parseUser]: ", error + + result = toUser json.fromJson(RawUser) diff --git a/src/experimental/parser/utils.nim b/src/experimental/parser/utils.nim index 999683d..17afd83 100644 --- a/src/experimental/parser/utils.nim +++ b/src/experimental/parser/utils.nim @@ -1,12 +1,16 @@ # SPDX-License-Identifier: AGPL-3.0-only import std/[sugar, strutils, times] -import ../types/common +import ".."/types/[common, media, tweet] import ../../utils as uutils template parseTime(time: string; f: static string; flen: int): DateTime = if time.len != flen: return parse(time, f, utc()) +proc toId*(id: string): int64 = + if id.len == 0: 0'i64 + else: parseBiggestInt(id) + proc parseIsoDate*(date: string): DateTime = date.parseTime("yyyy-MM-dd\'T\'HH:mm:ss\'Z\'", 20) @@ -16,6 +20,9 @@ proc parseTwitterDate*(date: string): DateTime = proc getImageUrl*(url: string): string = url.dup(removePrefix(twimg), removePrefix(https)) +proc getImageUrl*(entity: MediaEntity | Entity): string = + entity.mediaUrlHttps.getImageUrl + template handleErrors*(body) = if json.startsWith("{\"errors"): for error {.inject.} in json.fromJson(Errors).errors: diff --git a/src/experimental/types/article.nim b/src/experimental/types/article.nim deleted file mode 100644 index 946f8c3..0000000 --- a/src/experimental/types/article.nim +++ /dev/null @@ -1,79 +0,0 @@ -import std/options -import graphuser -from ../../types import ArticleStyle, ArticleEntityRange - -type - GraphArticle* = object - data*: tuple[tweetResult: tuple[result: TweetResultNode]] - - TweetResultNode* = object - article*: tuple[articleResults: tuple[result: ArticleResultNode]] - legacy*: TweetLegacy - core*: tuple[userResults: UserData] - views*: tuple[count: string] - - TweetLegacy* = object - createdAt*: string - replyCount*: int - retweetCount*: int - favoriteCount*: int - - ArticleResultNode* = object - title*: string - coverMedia*: tuple[mediaInfo: MediaInfoNode] - contentState*: ContentState - metadata*: tuple[firstPublishedAtSecs: int] - mediaEntities*: seq[RawMediaEntity] - - ContentState* = object - blocks*: seq[ContentBlock] - entityMap*: seq[EntityMapEntry] - - ContentBlock* = object - text*: string - blockKind*: string - inlineStyleRanges*: seq[ArticleStyle] - entityRanges*: seq[ArticleEntityRange] - - EntityMapEntry* = object - key*: string - value*: EntityMapValue - - EntityMapValue* = object - entityKind*: string - data*: EntityDataNode - - EntityDataNode* = object - url*: string - mediaItems*: seq[tuple[mediaId: string]] - tweetId*: string - markdown*: string - caption*: string - - RawMediaEntity* = object - mediaId*: string - mediaInfo*: MediaInfoNode - - MediaInfoNode* = object - typeName*: string - originalImgUrl*: string - videoInfo*: Option[VideoInfoNode] - - VideoInfoNode* = object - variants*: seq[VideoVariant] - - VideoVariant* = object - url*: string - bitrate*: int - -proc renameHook*(v: var ContentBlock; fieldName: var string) = - if fieldName == "type": - fieldName = "blockKind" - -proc renameHook*(v: var EntityMapValue; fieldName: var string) = - if fieldName == "type": - fieldName = "entityKind" - -proc renameHook*(v: var MediaInfoNode; fieldName: var string) = - if fieldName == "__typename": - fieldName = "typeName" diff --git a/src/experimental/types/common.nim b/src/experimental/types/common.nim index e979015..512a095 100644 --- a/src/experimental/types/common.nim +++ b/src/experimental/types/common.nim @@ -1,3 +1,4 @@ +import jsony from ../../types import Error type @@ -5,7 +6,7 @@ type url*: string expandedUrl*: string displayUrl*: string - indices*: array[2, int] + indices*: Slice[int] ErrorObj* = object code*: Error @@ -18,3 +19,8 @@ proc contains*(codes: set[Error]; errors: Errors): bool = for e in errors.errors: if e.code in codes: return true + +proc parseHook*(s: string; i: var int; v: var Slice[int]) = + var slice: array[2, int] + parseHook(s, i, slice) + v = slice[0] ..< slice[1] diff --git a/src/experimental/types/graphfollowers.nim b/src/experimental/types/graphfollowers.nim deleted file mode 100644 index ba9210b..0000000 --- a/src/experimental/types/graphfollowers.nim +++ /dev/null @@ -1,17 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import graphlistmembers - -type - GraphFollowers* = object - data*: tuple[user: UserWrapper] - - UserWrapper = object - result*: UserResultWrapper - - UserResultWrapper = object - timeline*: tuple[timeline: graphlistmembers.Timeline] - -# Hook to normalize snake_case field from API to camelCase used by shared types -proc renameHook*(v: var Content; fieldName: var string) = - if fieldName == "user_results": - fieldName = "userResults" diff --git a/src/experimental/types/graphlistmembers.nim b/src/experimental/types/graphlistmembers.nim index 9e520d8..4cb3757 100644 --- a/src/experimental/types/graphlistmembers.nim +++ b/src/experimental/types/graphlistmembers.nim @@ -7,10 +7,10 @@ type List = object membersTimeline*: tuple[timeline: Timeline] - Timeline* = object + Timeline = object instructions*: seq[Instruction] - Instruction* = object + Instruction = object kind*: string entries*: seq[tuple[content: Content]] @@ -18,7 +18,7 @@ type TimelineTimelineItem TimelineTimelineCursor - Content* = object + Content = object case entryType*: ContentEntryType of TimelineTimelineItem: itemContent*: tuple[userResults: UserData] diff --git a/src/experimental/types/graphuser.nim b/src/experimental/types/graphuser.nim index ec41f89..dded4eb 100644 --- a/src/experimental/types/graphuser.nim +++ b/src/experimental/types/graphuser.nim @@ -1,67 +1,12 @@ -import options, strutils -from ../../types import User, VerifiedType -import user as userType # Entities, for modern profile_bio parsing +import user type GraphUser* = object - data*: tuple[userResult: Option[UserData], user: Option[UserData]] + data*: tuple[user: UserData] UserData* = object result*: UserResult - UserCore* = object - name*: string - screenName*: string - createdAt*: string - - UserBio* = object - description*: string - entities*: Entities - - UserAvatar* = object - imageUrl*: string - - Verification* = object - verifiedType*: VerifiedType - - Location* = object - location*: string - - Privacy* = object - protected*: bool - - # Modern UserByRestId/UserByScreenName shape moves the counts out of `legacy` - # into these typed sub-objects. - RelationshipCounts* = object - followers*: int - following*: int - - TweetCounts* = object - tweets*: int - mediaTweets*: int - - ActionCounts* = object - favoritesCount*: int - - UserResult* = object - legacy*: User + UserResult = object + legacy*: RawUser restId*: string - isBlueVerified*: bool - core*: UserCore - avatar*: UserAvatar - banner*: UserAvatar - unavailableReason*: Option[string] - reason*: Option[string] - privacy*: Option[Privacy] - profileBio*: Option[UserBio] - verification*: Option[Verification] - location*: Option[Location] - relationshipCounts*: Option[RelationshipCounts] - tweetCounts*: Option[TweetCounts] - actionCounts*: Option[ActionCounts] - -proc enumHook*(s: string; v: var VerifiedType) = - v = try: - parseEnum[VerifiedType](s) - except: - VerifiedType.none diff --git a/src/experimental/types/media.nim b/src/experimental/types/media.nim new file mode 100644 index 0000000..924a25e --- /dev/null +++ b/src/experimental/types/media.nim @@ -0,0 +1,15 @@ +import options +from ../../types import VideoType, VideoVariant + +type + MediaType* = enum + photo, video, animatedGif + + MediaEntity* = object + kind*: MediaType + mediaUrlHttps*: string + videoInfo*: Option[VideoInfo] + + VideoInfo* = object + durationMillis*: int + variants*: seq[VideoVariant] diff --git a/src/experimental/types/session.nim b/src/experimental/types/session.nim deleted file mode 100644 index dfec428..0000000 --- a/src/experimental/types/session.nim +++ /dev/null @@ -1,9 +0,0 @@ -type - RawSession* = object - kind*: string - id*: string - username*: string - oauthToken*: string - oauthTokenSecret*: string - authToken*: string - ct0*: string diff --git a/src/experimental/types/tid.nim b/src/experimental/types/tid.nim deleted file mode 100644 index ad036d9..0000000 --- a/src/experimental/types/tid.nim +++ /dev/null @@ -1,4 +0,0 @@ -type - TidPair* = object - animationKey*: string - verification*: string diff --git a/src/experimental/types/timeline.nim b/src/experimental/types/timeline.nim new file mode 100644 index 0000000..c87e511 --- /dev/null +++ b/src/experimental/types/timeline.nim @@ -0,0 +1,28 @@ +import std/tables +import user, tweet + +type + Search* = object + globalObjects*: GlobalObjects + timeline*: Timeline + + GlobalObjects* = object + users*: Table[string, RawUser] + tweets*: Table[string, RawTweet] + + Timeline = object + instructions*: seq[Instructions] + + Instructions = object + addEntries*: tuple[entries: seq[Entry]] + + Entry* = object + entryId*: string + content*: tuple[operation: Operation] + + Operation = object + cursor*: tuple[value, cursorType: string] + +proc renameHook*(v: var Entity; fieldName: var string) = + if fieldName == "type": + fieldName = "kind" diff --git a/src/experimental/types/tweet.nim b/src/experimental/types/tweet.nim new file mode 100644 index 0000000..3004167 --- /dev/null +++ b/src/experimental/types/tweet.nim @@ -0,0 +1,85 @@ +import options +import jsony +from json import JsonNode +import user, media, common + +type + RawTweet* = object + createdAt*: string + idStr*: string + fullText*: string + displayTextRange*: array[2, int] + entities*: Entities + extendedEntities*: ExtendedEntities + inReplyToStatusIdStr*: string + inReplyToScreenName*: string + userIdStr*: string + isQuoteStatus*: bool + replyCount*: int + retweetCount*: int + favoriteCount*: int + quoteCount*: int + conversationIdStr*: string + favorited*: bool + retweeted*: bool + selfThread*: tuple[idStr: string] + card*: Option[Card] + quotedStatusIdStr*: string + retweetedStatusIdStr*: string + + Card* = object + name*: string + url*: string + bindingValues*: JsonNode + + Entities* = object + hashtags*: seq[Hashtag] + symbols*: seq[Hashtag] + userMentions*: seq[UserMention] + urls*: seq[Url] + media*: seq[Entity] + + Hashtag* = object + indices*: Slice[int] + + UserMention* = object + screenName*: string + name*: string + indices*: Slice[int] + + ExtendedEntities* = object + media*: seq[Entity] + + Entity* = object + kind*: MediaType + indices*: Slice[int] + mediaUrlHttps*: string + url*: string + expandedUrl*: string + videoInfo*: VideoInfo + ext*: Ext + extMediaAvailability*: tuple[status: string] + extAltText*: string + additionalMediaInfo*: AdditionalMediaInfo + sourceStatusIdStr*: string + sourceUserIdStr*: string + + AdditionalMediaInfo* = object + sourceUser*: Option[RawUser] + title*: string + description*: string + + Ext* = object + mediaStats*: JsonNode + + MediaStats* = object + ok*: tuple[viewCount: string] + +proc renameHook*(v: var Entity; fieldName: var string) = + if fieldName == "type": + fieldName = "kind" + +proc parseHook*(s: string; i: var int; v: var Slice[int]) = + var slice: array[2, int] + parseHook(s, i, slice) + v = slice[0] ..< slice[1] diff --git a/src/experimental/types/unifiedcard.nim b/src/experimental/types/unifiedcard.nim index cef6f44..eba0385 100644 --- a/src/experimental/types/unifiedcard.nim +++ b/src/experimental/types/unifiedcard.nim @@ -1,10 +1,7 @@ -import std/[options, tables, times] -import jsony -from ../../types import VideoType, VideoVariant, User +import options, tables +import media as mediaTypes type - Text* = distinct string - UnifiedCard* = object componentObjects*: Table[string, Component] destinationObjects*: Table[string, Destination] @@ -16,14 +13,10 @@ type media swipeableMedia buttonGroup - jobDetails appStoreDetails twitterListDetails communityDetails mediaWithDetailsHorizontal - hidden - grokShare - unknown Component* = object kind*: ComponentType @@ -34,40 +27,24 @@ type appId*: string mediaId*: string destination*: string - location*: string title*: Text subtitle*: Text name*: Text memberCount*: int mediaList*: seq[MediaItem] topicDetail*: tuple[title: Text] - profileUser*: User - shortDescriptionText*: string - conversationPreview*: seq[GrokConversation] MediaItem* = object id*: string destination*: string - Destination* = object - kind*: string - data*: tuple[urlData: UrlData] - UrlData* = object url*: string vanity*: string - MediaType* = enum - photo, video, model3d - - MediaEntity* = object - kind*: MediaType - mediaUrlHttps*: string - videoInfo*: Option[VideoInfo] - - VideoInfo* = object - durationMillis*: int - variants*: seq[VideoVariant] + Destination* = object + kind*: string + data*: tuple[urlData: UrlData] AppType* = enum androidApp, iPhoneApp, iPadApp @@ -78,58 +55,13 @@ type title*: Text category*: Text - GrokConversation* = object - message*: string - sender*: string + Text = object + content: string - TypeField = Component | Destination | MediaEntity | AppStoreData + HasTypeField = Component | Destination | MediaEntity | AppStoreData -converter fromText*(text: Text): string = string(text) +converter fromText*(text: Text): string = text.content -proc renameHook*(v: var TypeField; fieldName: var string) = +proc renameHook*(v: var HasTypeField; fieldName: var string) = if fieldName == "type": fieldName = "kind" - -proc enumHook*(s: string; v: var ComponentType) = - v = case s - of "details": details - of "media": media - of "swipeable_media": swipeableMedia - of "button_group": buttonGroup - of "job_details": jobDetails - of "app_store_details": appStoreDetails - of "twitter_list_details": twitterListDetails - of "community_details": communityDetails - of "media_with_details_horizontal": mediaWithDetailsHorizontal - of "commerce_drop_details": hidden - of "grok_share": grokShare - else: echo "ERROR: Unknown enum value (ComponentType): ", s; unknown - -proc enumHook*(s: string; v: var AppType) = - v = case s - of "android_app": androidApp - of "iphone_app": iPhoneApp - of "ipad_app": iPadApp - else: echo "ERROR: Unknown enum value (AppType): ", s; androidApp - -proc enumHook*(s: string; v: var MediaType) = - v = case s - of "video": video - of "photo": photo - of "model3d": model3d - else: echo "ERROR: Unknown enum value (MediaType): ", s; photo - -proc parseHook*(s: string; i: var int; v: var DateTime) = - var str: string - parseHook(s, i, str) - v = parse(str, "yyyy-MM-dd hh:mm:ss") - -proc parseHook*(s: string; i: var int; v: var Text) = - if s[i] == '"': - var str: string - parseHook(s, i, str) - v = Text(str) - else: - var t: tuple[content: string] - parseHook(s, i, t) - v = Text(t.content) diff --git a/src/experimental/types/user.nim b/src/experimental/types/user.nim index 7dc0194..a7fd727 100644 --- a/src/experimental/types/user.nim +++ b/src/experimental/types/user.nim @@ -1,6 +1,6 @@ import options +import jsony import common -from ../../types import VerifiedType type RawUser* = object @@ -16,7 +16,7 @@ type favouritesCount*: int statusesCount*: int mediaCount*: int - verifiedType*: VerifiedType + verified*: bool protected*: bool profileLinkColor*: string profileBannerUrl*: string @@ -42,3 +42,8 @@ type Color* = object red*, green*, blue*: int + +proc parseHook*(s: string; i: var int; v: var Slice[int]) = + var slice: array[2, int] + parseHook(s, i, slice) + v = slice[0] ..< slice[1] diff --git a/src/formatters.nim b/src/formatters.nim index 958e518..bb8698c 100644 --- a/src/formatters.nim +++ b/src/formatters.nim @@ -1,20 +1,19 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, times, uri, tables, xmltree, htmlparser, htmlgen, math +import strutils, strformat, times, uri, tables, xmltree, htmlparser, htmlgen import std/[enumerate, re] import types, utils, query const cards = "cards.twitter.com/cards" tco = "https://t.co" - twitter = parseUri("https://x.com") + twitter = parseUri("https://twitter.com") let twRegex = re"(?<=(?twitter\.com(\S+)""" - xRegex = re"(?<=(?x\.com(\S+)""" - ytRegex = re(r"([A-z.]+\.)?youtu(be\.com|\.be)", {reStudy, reIgnoreCase}) + ytRegex = re"([A-z.]+\.)?youtu(be\.com|\.be)" + igRegex = re"(www\.)?instagram\.com" rdRegex = re"(? length: result = result[0 ..< length] & "…" - -proc shortLink*(text: string; length=28): string = - result = text.replace(wwwRegex, "").shorten(length) proc stripHtml*(text: string; shorten=false): string = var html = parseHtml(text) @@ -59,63 +55,43 @@ proc replaceUrls*(body: string; prefs: Prefs; absolute=""): string = result = body if prefs.replaceYouTube.len > 0 and "youtu" in result: - let youtubeHost = strip(prefs.replaceYouTube, chars={'/'}) - result = result.replace(ytRegex, youtubeHost) + result = result.replace(ytRegex, prefs.replaceYouTube) + if prefs.replaceYouTube in result: + result = result.replace("/c/", "/") - if prefs.replaceTwitter.len > 0: - let twitterHost = strip(prefs.replaceTwitter, chars={'/'}) - if tco in result: - result = result.replace(tco, https & twitterHost & "/t.co") - if "x.com" in result: - result = result.replace(xRegex, twitterHost) - result = result.replacef(xLinkRegex, a( - twitterHost & "$2", href = https & twitterHost & "$1")) - if "twitter.com" in result: - result = result.replace(cards, twitterHost & "/cards") - result = result.replace(twRegex, twitterHost) - result = result.replacef(twLinkRegex, a( - twitterHost & "$2", href = https & twitterHost & "$1")) + if prefs.replaceTwitter.len > 0 and ("twitter.com" in body or tco in body): + result = result.replace(tco, https & prefs.replaceTwitter & "/t.co") + result = result.replace(cards, prefs.replaceTwitter & "/cards") + result = result.replace(twRegex, prefs.replaceTwitter) + result = result.replacef(twLinkRegex, a( + prefs.replaceTwitter & "$2", href = https & prefs.replaceTwitter & "$1")) if prefs.replaceReddit.len > 0 and ("reddit.com" in result or "redd.it" in result): - let redditHost = strip(prefs.replaceReddit, chars={'/'}) - result = result.replace(rdShortRegex, redditHost & "/comments/") - result = result.replace(rdRegex, redditHost) - if redditHost in result and "/gallery/" in result: + result = result.replace(rdShortRegex, prefs.replaceReddit & "/comments/") + result = result.replace(rdRegex, prefs.replaceReddit) + if prefs.replaceReddit in result and "/gallery/" in result: result = result.replace("/gallery/", "/comments/") + if prefs.replaceInstagram.len > 0 and "instagram.com" in result: + result = result.replace(igRegex, prefs.replaceInstagram) + if absolute.len > 0 and "href" in result: - result = result.replace("href=\"/", &"href=\"{absolute}/") + result = result.replace("href=\"/", "href=\"" & absolute & "/") proc getM3u8Url*(content: string): string = var matches: array[1, string] if re.find(content, m3u8Regex, matches) != -1: result = matches[0] -proc proxifyVideo*(manifest: string; proxy: bool; manifestUrl = ""): string = - let (baseUrl, basePath) = - if manifestUrl.len > 0: - let - u = parseUri(manifestUrl) - origin = u.scheme & "://" & u.hostname - idx = manifestUrl.rfind('/') - dirPath = if idx > 8: manifestUrl[0 .. idx] else: "" - (origin, dirPath) - else: - ("https://video.twimg.com", "") +proc proxifyVideo*(manifest: string; proxy: bool): string = var replacements: seq[(string, string)] for line in manifest.splitLines: let url = if line.startsWith("#EXT-X-MAP:URI"): line[16 .. ^2] - elif line.startsWith("#EXT-X-MEDIA") and "URI=" in line: - line[line.find("URI=") + 5 .. -1 + line.find("\"", start= 5 + line.find("URI="))] else: line - let resolved = - if url.startsWith('/'): baseUrl & url - elif basePath.len > 0 and url.len > 0 and not url.startsWith('#') and - not url.startsWith("http") and ('.' in url): basePath & url - else: "" - if resolved.len > 0: - replacements.add (url, if proxy: resolved.getVidUrl else: resolved) + if url.startsWith('/'): + let path = "https://video.twimg.com" & url + replacements.add (url, if proxy: path.getVidUrl else: path) return manifest.multiReplace(replacements) proc getUserPic*(userPic: string; style=""): string = @@ -140,30 +116,25 @@ proc pageDesc*(user: User): string = "The latest tweets from " & user.fullname proc getJoinDate*(user: User): string = - if user.joinDate.year == 0: return "" user.joinDate.format("'Joined' MMMM YYYY") proc getJoinDateFull*(user: User): string = - if user.joinDate.year == 0: return "" user.joinDate.format("h:mm tt - d MMM YYYY") proc getTime*(tweet: Tweet): string = - if tweet.time.year == 0: return "" tweet.time.format("MMM d', 'YYYY' · 'h:mm tt' UTC'") proc getRfc822Time*(tweet: Tweet): string = - if tweet.time.year == 0: return "" tweet.time.format("ddd', 'dd MMM yyyy HH:mm:ss 'GMT'") -proc getShortTime*(time: DateTime): string = - if time.year == 0: return "" +proc getShortTime*(tweet: Tweet): string = let now = now() - let since = now - time + let since = now - tweet.time - if now.year != time.year: - result = time.format("d MMM yyyy") + if now.year != tweet.time.year: + result = tweet.time.format("d MMM yyyy") elif since.inDays >= 1: - result = time.format("MMM d") + result = tweet.time.format("MMM d") elif since.inHours >= 1: result = $since.inHours & "h" elif since.inMinutes >= 1: @@ -173,33 +144,13 @@ proc getShortTime*(time: DateTime): string = else: result = "now" -proc getShortTime*(tweet: Tweet): string = - getShortTime(tweet.time) - -proc getDuration*(ms: int): string = - let - sec = int(round(ms / 1000)) - min = floorDiv(sec, 60) - hour = floorDiv(min, 60) - if hour > 0: - &"{hour}:{min mod 60:02}:{sec mod 60:02}" - else: - &"{min mod 60}:{sec mod 60:02}" - -proc getDuration*(video: Video): string = - getDuration(video.durationMs) - -proc getLink*(id: int64; username="i"; focus=true): string = - var username = username - if username.len == 0: - username = "i" - result = &"/{username}/status/{id}" - if focus: result &= "#m" - proc getLink*(tweet: Tweet; focus=true): string = if tweet.id == 0: return var username = tweet.user.username - return getLink(tweet.id, username, focus) + if username.len == 0: + username = "i" + result = &"/{username}/status/{tweet.id}" + if focus: result &= "#m" proc getTwitterLink*(path: string; params: Table[string, string]): string = var @@ -227,7 +178,7 @@ proc getTwitterLink*(path: string; params: Table[string, string]): string = proc getLocation*(u: User | Tweet): (string, string) = if "://" in u.location: return (u.location, "") let loc = u.location.split(":") - let url = if loc.len > 1: "/search?f=tweets&q=place:" & loc[1] else: "" + let url = if loc.len > 1: "/search?q=place:" & loc[1] else: "" (loc[0], url) proc getSuspended*(username: string): string = diff --git a/src/http_pool.nim b/src/http_pool.nim index 2553dd9..2037520 100644 --- a/src/http_pool.nim +++ b/src/http_pool.nim @@ -27,7 +27,7 @@ proc release*(pool: HttpPool; client: AsyncHttpClient; badClient=false) = proc acquire*(pool: HttpPool; heads: HttpHeaders): AsyncHttpClient = if pool.conns.len == 0: - result = newAsyncHttpClient(userAgent="", headers=heads, proxy=proxy) + result = newAsyncHttpClient(headers=heads, proxy=proxy) else: result = pool.conns.pop() result.headers = heads @@ -39,11 +39,8 @@ template use*(pool: HttpPool; heads: HttpHeaders; body: untyped): untyped = try: body - except BadClientError, ProtocolError: - # Twitter returned 503 or closed the connection, we need a new client - pool.release(c, true) - badClient = false - c = pool.acquire(heads) + except ProtocolError: + # Twitter closed the connection, retry body finally: pool.release(c, badClient) diff --git a/src/nitter.nim b/src/nitter.nim index a76dda6..9f8fcb7 100644 --- a/src/nitter.nim +++ b/src/nitter.nim @@ -2,26 +2,21 @@ import asyncdispatch, strformat, logging from net import Port from htmlgen import a -from os import getEnv, normalizedPath +from os import getEnv import jester -import types, config, prefs, formatters, redis_cache, http_pool, auth, apiutils +import types, config, prefs, formatters, redis_cache, http_pool, tokens import views/[general, about] import routes/[ - preferences, timeline, status, media, search, rss, list, community, debug, - unsupported, embed, resolver, broadcast, space, article, router_utils] + preferences, timeline, status, media, search, rss, list, debug, + unsupported, embed, resolver, router_utils] const instancesUrl = "https://github.com/zedeus/nitter/wiki/Instances" const issuesUrl = "https://github.com/zedeus/nitter/issues" -let - configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf") - (cfg, fullCfg) = getConfig(configPath) - - sessionsPath = getEnv("NITTER_SESSIONS_FILE", "./sessions.jsonl") - -initSessionPool(cfg, sessionsPath) +let configPath = getEnv("NITTER_CONF_FILE", "./nitter.conf") +let (cfg, fullCfg) = getConfig(configPath) if not cfg.enableDebug: # Silence Jester's query warning @@ -34,62 +29,40 @@ stdout.flushFile updateDefaultPrefs(fullCfg) setCacheTimes(cfg) setHmacKey(cfg.hmacKey) -if cfg.hmacKey.len == 0 or cfg.hmacKey == "secretkey": - stderr.write "WARNING: insecure default 'hmacKey' in nitter.conf; " & - "set a unique random value to stop media URL signatures being forgeable.\n" - stderr.flushFile setProxyEncoding(cfg.base64Media) setMaxHttpConns(cfg.httpMaxConns) setHttpProxy(cfg.proxy, cfg.proxyAuth) -setApiProxy(cfg.apiProxy) -setDisableTid(cfg.disableTid) -setMaxConcurrentReqs(cfg.maxConcurrentReqs) -setMaxRetries(cfg.maxRetries) -setRetryDelayMs(cfg.retryDelayMs) initAboutPage(cfg.staticDir) waitFor initRedisPool(cfg) stdout.write &"Connected to Redis at {cfg.redisHost}:{cfg.redisPort}\n" stdout.flushFile -createArticleRouter(cfg) +asyncCheck initTokenPool(cfg) + createUnsupportedRouter(cfg) createResolverRouter(cfg) createPrefRouter(cfg) createTimelineRouter(cfg) createListRouter(cfg) -createCommunityRouter(cfg) createStatusRouter(cfg) createSearchRouter(cfg) createMediaRouter(cfg) createEmbedRouter(cfg) createRssRouter(cfg) -createBroadcastRouter(cfg) -createSpaceRouter(cfg) createDebugRouter(cfg) settings: port = Port(cfg.port) - staticDir = normalizedPath(cfg.staticDir) + staticDir = cfg.staticDir bindAddr = cfg.address - reusePort = true - maxBody = 64 * 1024 routes: - before: - # Reject malformed paths - if request.path.len == 0 or request.path[0] != '/': - halt Http400 - - # skip all file URLs (except Twitter widget compatibility) - cond "." notin request.path or request.path == "/embed/Tweet.html" - applyUrlPrefs() - get "/": - resp renderMain(renderSearch(), request, cfg, requestPrefs()) + resp renderMain(renderSearch(), request, cfg, themePrefs()) get "/about": - resp renderMain(renderAbout(), request, cfg, requestPrefs()) + resp renderMain(renderAbout(), request, cfg, themePrefs()) get "/explore": redirect("/about") @@ -100,7 +73,7 @@ routes: get "/i/redirect": let url = decodeUrl(@"url") if url.len == 0: resp Http404 - redirect(replaceUrls(url, requestPrefs())) + redirect(replaceUrls(url, cookiePrefs())) error Http404: resp Http404, showError("Page not found", cfg) @@ -111,32 +84,20 @@ routes: resp Http500, showError( &"An error occurred, please {link} with the URL you tried to visit.", cfg) - error BadClientError: - echo error.exc.name, ": ", error.exc.msg - resp Http500, showError("Network error occurred, please try again.", cfg) - error RateLimitError: + echo error.exc.name, ": ", error.exc.msg const link = a("another instance", href = instancesUrl) resp Http429, showError( &"Instance has been rate limited.
Use {link} or try again later.", cfg) - error NoSessionsError: - const link = a("another instance", href = instancesUrl) - resp Http429, showError( - &"Instance has no auth tokens, or is fully rate limited.
Use {link} or try again later.", cfg) - - extend articleRoute, "" - extend rss, "" - extend status, "" - extend search, "" - extend timeline, "" - extend media, "" - extend list, "" - extend community, "" + extend unsupported, "" extend preferences, "" extend resolver, "" + extend rss, "" + extend search, "" + extend timeline, "" + extend list, "" + extend status, "" + extend media, "" extend embed, "" - extend broadcastRoute, "" - extend spaceRoute, "" extend debug, "" - extend unsupported, "" diff --git a/src/parser.nim b/src/parser.nim index eebca2d..ae5e505 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -1,21 +1,9 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, options, times, math, tables, uri +import strutils, options, tables, times, math import packedjson, packedjson/deserialiser import types, parserutils, utils import experimental/parser/unifiedcard -proc parseGraphTweet*(js: JsonNode): Tweet - -proc parseVerifiedType(s: string; current: VerifiedType): VerifiedType = - try: parseEnum[VerifiedType](s) - except ValueError: current - -proc parseCommunityNote(js: JsonNode): string = - let subtitle = js{"subtitle"} - result = subtitle{"text"}.getStr - with entities, subtitle{"entities"}: - result = expandBirdwatchEntities(result, entities) - proc parseUser(js: JsonNode; id=""): User = if js.isNull: return result = User( @@ -31,195 +19,13 @@ proc parseUser(js: JsonNode; id=""): User = tweets: js{"statuses_count"}.getInt, likes: js{"favourites_count"}.getInt, media: js{"media_count"}.getInt, - protected: js{"protected"}.getBool(js{"privacy", "protected"}.getBool), + verified: js{"verified"}.getBool, + protected: js{"protected"}.getBool, joinDate: js{"created_at"}.getTime ) - if js{"is_blue_verified"}.getBool(false): - result.verifiedType = blue - - with verifiedType, js{"verified_type"}: - result.verifiedType = parseVerifiedType(verifiedType.getStr, result.verifiedType) - result.expandUserEntities(js) -proc parseGraphUser(js: JsonNode): User = - var user = js{"user_result", "result"} - if user.isNull: - user = js{"user_results", "result"} - - if user.isNull: - if js{"core"}.notNull: - user = js - else: - return - - result = parseUser(user{"legacy"}, user{"rest_id"}.getStr) - - if result.verifiedType == none and user{"is_blue_verified"}.getBool(false): - result.verifiedType = blue - - # fallback to support UserMedia/recent GraphQL updates - if result.username.len == 0: - result.id = user{"rest_id"}.getStr - result.username = user{"core", "screen_name"}.getStr - result.fullname = user{"core", "name"}.getStr - result.userPic = user{"avatar", "image_url"}.getImageStr.replace("_normal", "") - - if user{"is_blue_verified"}.getBool( - user{"verification", "is_blue_verified"}.getBool(false)): - result.verifiedType = blue - - with verifiedType, user{"verification", "verified_type"}: - result.verifiedType = parseVerifiedType(verifiedType.getStr, result.verifiedType) - -proc parseAboutAccount*(js: JsonNode): AccountInfo = - if js.isNull: return - - let user = ? js{"data", "user_result_by_screen_name", "result"} - - if user{"unavailable_reason"}.getStr == "Suspended": - result.suspended = true - return - - result = AccountInfo( - username: user{"core", "screen_name"}.getStr, - fullname: user{"core", "name"}.getStr, - joinDate: user{"core", "created_at"}.getTime, - userPic: user{"avatar", "image_url"}.getImageStr.replace("_normal", ""), - affiliateLabel: user{"identity_profile_labels_highlighted_label", "label", "description"}.getStr, - ) - - if user{"is_blue_verified"}.getBool(false): - result.verifiedType = blue - with verifiedType, user{"verification", "verified_type"}: - result.verifiedType = parseVerifiedType(verifiedType.getStr, result.verifiedType) - - with about, user{"about_profile"}: - result.basedIn = about{"account_based_in"}.getStr - result.source = about{"source"}.getStr - result.affiliateUsername = about{"affiliate_username"}.getStr - - try: - result.usernameChanges = about{"username_changes", "count"}.getStr("0").parseInt - except ValueError: - discard - - with lastChange, about{"username_changes", "last_changed_at_msec"}: - result.lastUsernameChange = lastChange.getTimeFromMsStr - - with info, user{"verification_info"}: - result.isIdentityVerified = info{"is_identity_verified"}.getBool - with reason, info{"reason"}: - result.overrideVerifiedYear = reason{"override_verified_year"}.getInt - with since, reason{"verified_since_msec"}: - result.verifiedSince = since.getTimeFromMsStr - -proc parseBroadcastInfo*(js: JsonNode): Broadcast = - let bc = ? js{"data", "broadcast"} - result = Broadcast( - id: bc{"broadcast_id"}.getStr, - title: bc{"status"}.getStr, - state: bc{"state"}.getStr.toUpperAscii, - thumb: bc{"image_url"}.getStr, - mediaKey: bc{"media_key"}.getStr, - totalWatched: bc{"total_watched"}.getInt, - startTime: bc{"start_time"}.getTimeFromMs, - endTime: bc{"end_time"}.getTimeFromMs, - replayStart: bc{"edited_replay", "start_time"}.getInt, - availableForReplay: bc{"available_for_replay"}.getBool, - user: parseGraphUser(bc) - ) - -proc parseSpaceParticipant(js: JsonNode): SpaceParticipant = - result = SpaceParticipant( - userId: js{"user_results", "rest_id"}.getStr, - username: js{"twitter_screen_name"}.getStr, - displayName: js{"display_name"}.getStr, - avatarUrl: js{"avatar_url"}.getStr, - isVerified: js{"is_verified"}.getBool or - js{"user_results", "result", "is_blue_verified"}.getBool - ) - -proc parseAudioSpace*(js: JsonNode): AudioSpace = - let space = ? js{"data", "audioSpace"} - let meta = space{"metadata"} - - result = AudioSpace( - id: meta{"rest_id"}.getStr, - title: meta{"title"}.getStr, - state: meta{"state"}.getStr.toUpperAscii, - mediaKey: meta{"media_key"}.getStr, - totalLiveListeners: meta{"total_live_listeners"}.getInt, - totalReplayWatched: meta{"total_replay_watched"}.getInt, - availableForReplay: meta{"is_space_available_for_replay"}.getBool - ) - - let startedAt = meta{"started_at"}.getInt(0) - if startedAt > 0: - result.startTime = fromUnix(startedAt div 1000).utc() - - let endedAtStr = meta{"ended_at"}.getStr - if endedAtStr.len > 0: - try: - let endedAt = parseBiggestInt(endedAtStr) - if endedAt > 0: - result.endTime = fromUnix(endedAt div 1000).utc() - except ValueError: - discard - - result.creator = parseGraphUser(meta{"creator_results", "result"}) - - for admin in space{"participants", "admins"}: - result.admins.add parseSpaceParticipant(admin) - - for speaker in space{"participants", "speakers"}: - result.speakers.add parseSpaceParticipant(speaker) - -proc parseGraphCommunity*(js: JsonNode): Community = - if js.isNull: return - let c = ? js{"data", "communityResults", "result"} - - result = Community( - id: c{"rest_id"}.getStr(c{"id_str"}.getStr), - name: c{"name"}.getStr, - description: c{"description"}.getStr, - memberCount: c{"member_count"}.getInt, - joinPolicy: c{"join_policy"}.getStr, - category: c{"primary_community_topic", "topic_name"}.getStr, - banner: c{"custom_banner_media", "media_info", "original_img_url"}.getImageStr, - creator: parseGraphUser(c{"creator_results", "result"}), - ) - - let createdMs = c{"created_at"}.getInt(0) - if createdMs > 0: - result.createdAt = fromUnix(createdMs div 1000).utc() - - for rule in c{"rules"}: - result.rules.add CommunityRule( - name: rule{"name"}.getStr, - description: rule{"description"}.getStr - ) - - for item in c{"trending_hashtags_slice", "items"}: - let tag = item{"hashtag"}.getStr - if tag.len > 0: - result.hashtags.add tag - -proc parseListObject(js: JsonNode; owner: User): List = - List( - id: js{"id_str"}.getStr, - name: js{"name"}.getStr, - username: owner.username, - userId: owner.id, - description: js{"description"}.getStr, - members: js{"member_count"}.getInt, - banner: select( - js{"custom_banner_media", "media_info", "original_img_url"}, - js{"default_banner_media", "media_info", "original_img_url"} - ).getImageStr - ) - proc parseGraphList*(js: JsonNode): List = if js.isNull: return @@ -229,17 +35,16 @@ proc parseGraphList*(js: JsonNode): List = if list.isNull: return - result = parseListObject(list, parseGraphUser(list)) - -proc parseGraphSearchList(js: JsonNode): ListSearchResult = - let owner = parseGraphUser(js) - result = ListSearchResult( - list: parseListObject(js, owner), - owner: owner, - followersContext: js{"followers_context"}.getStr + result = List( + id: list{"id_str"}.getStr, + name: list{"name"}.getStr, + username: list{"user", "legacy", "screen_name"}.getStr, + userId: list{"user", "rest_id"}.getStr, + description: list{"description"}.getStr, + members: list{"member_count"}.getInt, + banner: list{"custom_banner_media", "media_info", "url"}.getImageStr ) - for url in js{"facepile_urls"}: - result.facepiles.add url.getStr + proc parsePoll(js: JsonNode): Poll = let vals = js{"binding_values"} @@ -259,121 +64,34 @@ proc parsePoll(js: JsonNode): Poll = result.leader = result.values.find(max(result.values)) result.votes = result.values.sum -proc parseVideoVariants(variants: JsonNode): seq[VideoVariant] = - result = @[] - for v in variants: - let - url = v{"url"}.getStr - contentType = parseEnum[VideoType](v{"content_type"}.getStr("video/mp4")) - bitrate = v{"bit_rate"}.getInt(v{"bitrate"}.getInt(0)) - - result.add VideoVariant( - contentType: contentType, - bitrate: bitrate, - url: url, - resolution: if contentType == mp4: getMp4Resolution(url) else: 0 - ) +proc parseGif(js: JsonNode): Gif = + result = Gif( + url: js{"video_info", "variants"}[0]{"url"}.getImageStr, + thumb: js{"media_url_https"}.getImageStr + ) proc parseVideo(js: JsonNode): Video = result = Video( thumb: js{"media_url_https"}.getImageStr, - available: true, + views: js{"ext", "mediaStats", "r", "ok", "viewCount"}.getStr, + available: js{"ext_media_availability", "status"}.getStr == "available", title: js{"ext_alt_text"}.getStr, durationMs: js{"video_info", "duration_millis"}.getInt # playbackType: mp4 ) - with status, js{"ext_media_availability", "status"}: - if status.getStr.len > 0 and status.getStr.toLowerAscii != "available": - result.available = false - with title, js{"additional_media_info", "title"}: result.title = title.getStr with description, js{"additional_media_info", "description"}: result.description = description.getStr - result.variants = parseVideoVariants(js{"video_info", "variants"}) - -proc addMedia(media: var MediaEntities; photo: Photo) = - media.add Media(kind: photoMedia, photo: photo) - -proc addMedia(media: var MediaEntities; video: Video) = - media.add Media(kind: videoMedia, video: video) - -proc addMedia(media: var MediaEntities; gif: Gif) = - media.add Media(kind: gifMedia, gif: gif) - -proc parseLegacyMediaEntities(js: JsonNode; result: var Tweet) = - with jsMedia, js{"extended_entities", "media"}: - for m in jsMedia: - case m.getTypeName: - of "photo": - result.media.addMedia(Photo( - url: m{"media_url_https"}.getImageStr, - altText: m{"ext_alt_text"}.getStr - )) - of "video": - result.media.addMedia(parseVideo(m)) - with user, m{"additional_media_info", "source_user"}: - if user{"id"}.getInt > 0: - result.attribution = some(parseUser(user)) - else: - result.attribution = some(parseGraphUser(user)) - # Set attribution link from expanded_url (strip /video/N suffix) - let expanded = m{"expanded_url"}.getStr - if expanded.len > 0: - result.attributionLink = expanded.parseUri.path.replace("/video/1", "") - of "animated_gif": - result.media.addMedia(Gif( - url: m{"video_info", "variants"}[0]{"url"}.getImageStr, - thumb: m{"media_url_https"}.getImageStr, - altText: m{"ext_alt_text"}.getStr - )) - else: discard - -proc parseMediaEntities(js: JsonNode; result: var Tweet) = - with mediaEntities, js{"media_entities"}: - var parsedMedia: MediaEntities - for mediaEntity in mediaEntities: - with mediaInfo, mediaEntity{"media_results", "result", "media_info"}: - case mediaInfo.getTypeName - of "ApiImage": - parsedMedia.addMedia(Photo( - url: mediaInfo{"original_img_url"}.getImageStr, - altText: mediaInfo{"alt_text"}.getStr - )) - of "ApiVideo": - let status = mediaEntity{"media_results", "result", "media_availability_v2", "status"} - parsedMedia.addMedia(Video( - available: status.getStr == "Available", - thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr, - title: mediaInfo{"alt_text"}.getStr, - durationMs: mediaInfo{"duration_millis"}.getInt, - variants: parseVideoVariants(mediaInfo{"variants"}) - )) - - # Parse source user for video attribution - with sourceUser, mediaEntity{"source_user_results", "result"}: - if result.attribution.isNone: - let expanded = mediaEntity{"expanded_url"}.getStr - if expanded.len > 0: - result.attributionLink = expanded.parseUri.path.replace("/video/1", "") - result.attribution = some(User( - id: sourceUser{"rest_id"}.getStr, - fullname: sourceUser{"core", "name"}.getStr, - userPic: sourceUser{"avatar", "image_url"}.getImageStr.replace("_normal", "") - )) - of "ApiGif": - parsedMedia.addMedia(Gif( - url: mediaInfo{"variants"}[0]{"url"}.getImageStr, - thumb: mediaInfo{"preview_image", "original_img_url"}.getImageStr, - altText: mediaInfo{"alt_text"}.getStr - )) - else: discard - - if mediaEntities.len > 0 and parsedMedia.len == mediaEntities.len: - result.media = parsedMedia + for v in js{"video_info", "variants"}: + result.variants.add VideoVariant( + contentType: parseEnum[VideoType](v{"content_type"}.getStr("summary")), + bitrate: v{"bitrate"}.getInt, + url: v{"url"}.getStr + ) proc parsePromoVideo(js: JsonNode): Video = result = Video( @@ -396,23 +114,14 @@ proc parsePromoVideo(js: JsonNode): Video = result.variants.add variant proc parseBroadcast(js: JsonNode): Card = - let - image = js{"broadcast_thumbnail_large"}.getImageVal - broadcastUrl = js{"broadcast_url"}.getStrVal - broadcastId = broadcastUrl.rsplit('/', maxsplit=1)[^1] - streamUrl = "/i/broadcasts/" & broadcastId & "/stream" + let image = js{"broadcast_thumbnail_large"}.getImageVal result = Card( kind: broadcast, - url: "/i/broadcasts/" & broadcastId, + url: js{"broadcast_url"}.getStrVal, title: js{"broadcaster_display_name"}.getStrVal, text: js{"broadcast_title"}.getStrVal, image: image, - video: some Video( - thumb: image, - available: true, - playbackType: m3u8, - variants: @[VideoVariant(contentType: m3u8, url: streamUrl)] - ) + video: some Video(thumb: image) ) proc parseCard(js: JsonNode; urls: JsonNode): Card = @@ -451,13 +160,7 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card = result.url = vals{"player_url"}.getStrVal if "youtube.com" in result.url: result.url = result.url.replace("/embed/", "/watch?v=") - of audiospace: - let spaceId = vals{"id"}.getStrVal - if spaceId.len > 0: - result.url = "/i/spaces/" & spaceId - result.title = "Twitter Space" - result.text = "Click to view Space" - of unknown: + of audiospace, unknown: result.title = "This card type is not supported." else: discard @@ -468,7 +171,7 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card = for u in ? urls: if u{"url"}.getStr == result.url: - result.url = u.getExpandedUrl(result.url) + result.url = u{"expanded_url"}.getStr break if kind in {videoDirectMessage, imageDirectMessage}: @@ -478,20 +181,14 @@ proc parseCard(js: JsonNode; urls: JsonNode): Card = result.url.len == 0 or result.url.startsWith("card://"): result.url = getPicUrl(result.image) -proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull(); - replyId: int64 = 0; hasArticle = false): Tweet = - if js.isNull: return Tweet() - - let time = - if js{"created_at"}.notNull: js{"created_at"}.getTime - else: js{"created_at_ms"}.getTimeFromMs - +proc parseTweet(js: JsonNode): Tweet = + if js.isNull: return result = Tweet( id: js{"id_str"}.getId, threadId: js{"conversation_id_str"}.getId, replyId: js{"in_reply_to_status_id_str"}.getId, text: js{"full_text"}.getStr, - time: time, + time: js{"created_at"}.getTime, hasThread: js{"self_thread"}.notNull, available: true, user: User(id: js{"user_id_str"}.getStr), @@ -499,56 +196,43 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull(); replies: js{"reply_count"}.getInt, retweets: js{"retweet_count"}.getInt, likes: js{"favorite_count"}.getInt, - views: js{"views_count"}.getInt + quotes: js{"quote_count"}.getInt ) ) - if result.replyId == 0: - result.replyId = replyId + result.expandTweetEntities(js) - # fix for pinned threads - if result.hasThread and result.threadId == 0: - result.threadId = js{"self_thread", "id_str"}.getId - - if "retweeted_status" in js: - result.retweet = some Tweet() - elif js{"is_quote_status"}.getBool: + if js{"is_quote_status"}.getBool: result.quote = some Tweet(id: js{"quoted_status_id_str"}.getId) - # legacy with rt, js{"retweeted_status_id_str"}: result.retweet = some Tweet(id: rt.getId) return - # graphql - with rt, js{"retweeted_status_result", "result"}: - # needed due to weird edgecase where the actual tweet data isn't included - if "legacy" in rt or "rest_id" in rt: - result.retweet = some parseGraphTweet(rt) - return - - with reposts, js{"repostedStatusResults"}: - with rt, reposts{"result"}: - if "legacy" in rt or "rest_id" in rt: - result.retweet = some parseGraphTweet(rt) - return - - if jsCard.kind != JNull: + with jsCard, js{"card"}: let name = jsCard{"name"}.getStr if "poll" in name: if "image" in name: - result.media.addMedia(Photo( - url: jsCard{"binding_values", "image_large"}.getImageVal - )) + result.photos.add jsCard{"binding_values", "image_large"}.getImageVal result.poll = some parsePoll(jsCard) elif name == "amplify": - result.media.addMedia(parsePromoVideo(jsCard{"binding_values"})) - elif name.len > 0 and jsCard{"binding_values"}.notNull: + result.video = some(parsePromoVideo(jsCard{"binding_values"})) + else: result.card = some parseCard(jsCard, js{"entities", "urls"}) - result.expandTweetEntities(js, hasArticle) - parseLegacyMediaEntities(js, result) + with jsMedia, js{"extended_entities", "media"}: + for m in jsMedia: + case m{"type"}.getStr + of "photo": + result.photos.add m{"media_url_https"}.getImageStr + of "video": + result.video = some(parseVideo(m)) + with user, m{"additional_media_info", "source_user"}: + result.attribution = some(parseUser(user)) + of "animated_gif": + result.gif = some(parseGif(m)) + else: discard with jsWithheld, js{"withheld_in_countries"}: let withheldInCountries: seq[string] = @@ -564,472 +248,159 @@ proc parseTweet(js: JsonNode; jsCard: JsonNode = newJNull(); result.text.removeSuffix(" Learn more.") result.available = false -proc parseGraphTweet*(js: JsonNode): Tweet = - if js.kind == JNull: - return Tweet() +proc finalizeTweet(global: GlobalObjects; id: string): Tweet = + let intId = if id.len > 0: parseBiggestInt(id) else: 0 + result = global.tweets.getOrDefault(id, Tweet(id: intId)) - case js.getTypeName: - of "TweetUnavailable": - return Tweet() - of "TweetTombstone": - with text, select(js{"tombstone", "richText"}, js{"tombstone", "text"}): - return Tweet(text: text.getTombstone) - return Tweet() - of "TweetPreviewDisplay": - return Tweet(text: "You're unable to view this Tweet because it's only available to the Subscribers of the account owner.") - of "TweetWithVisibilityResults": - return parseGraphTweet(js{"tweet"}) - else: - discard - - if "legacy" notin js and "rest_id" notin js: - return Tweet() - - var jsCard = select(js{"card"}, js{"tweet_card"}, js{"legacy", "tweet_card"}) - if jsCard.kind != JNull: - let legacyCard = jsCard{"legacy"} - if legacyCard.kind != JNull: - let bindingArray = legacyCard{"binding_values"} - if bindingArray.kind == JArray: - var bindingObj: seq[(string, JsonNode)] - for item in bindingArray: - bindingObj.add((item{"key"}.getStr, item{"value"})) - # Create a new card object with flattened structure - jsCard = %*{ - "name": legacyCard{"name"}, - "url": legacyCard{"url"}, - "binding_values": %bindingObj - } - - var replyId: int64 = 0 - with restId, js{"reply_to_results", "rest_id"}: - replyId = restId.getId - - let hasArticle = js{"article", "article_results", "result", "title"}.getStr.len > 0 - - if "details" in js: - result = Tweet( - id: js{"rest_id"}.getId, - available: true, - text: js{"details", "full_text"}.getStr, - time: js{"details", "created_at_ms"}.getTimeFromMs, - replyId: js{"reply_to_results", "rest_id"}.getId, - isAd: js{"content_disclosure", "advertising_disclosure", "is_paid_promotion"}.getBool, - isAI: js{"content_disclosure", "ai_generated_disclosure", "has_ai_generated_media"}.getBool, - stats: TweetStats( - replies: js{"counts", "reply_count"}.getInt, - retweets: js{"counts", "retweet_count"}.getInt, - likes: js{"counts", "favorite_count"}.getInt, - ) - ) - - if jsCard.kind != JNull: - let name = jsCard{"name"}.getStr - if "poll" in name: - if "image" in name: - result.media.addMedia(Photo( - url: jsCard{"binding_values", "image_large"}.getImageVal - )) - - result.poll = some parsePoll(jsCard) - elif name == "amplify": - result.media.addMedia(parsePromoVideo(jsCard{"binding_values"})) - elif name.len > 0 and jsCard{"binding_values"}.notNull: - result.card = some parseCard(jsCard, js{"url_entities"}) - - parseMediaEntities(js, result) - if result.attribution.isNone: - parseLegacyMediaEntities(js{"legacy"}, result) - - result.expandTweetEntitiesV2(js, hasArticle) - - # Strip video source URL from text (for videos from other tweets) - with mediaEntities, js{"media_entities"}: - for m in mediaEntities: - if "source_status_id_str" in m: - let mediaUrl = m{"url"}.getStr - if mediaUrl.len > 0: - let idx = result.text.rfind(mediaUrl) - if idx >= 0: - result.text = result.text[0 ..< idx].strip() - break - else: - result = parseTweet(js{"legacy"}, jsCard, replyId, hasArticle) - result.id = js{"rest_id"}.getId - - with artNode, js{"article", "article_results", "result"}: - let artTitle = artNode{"title"}.getStr - if artTitle.len > 0: - result.articlePreview = some ArticlePreview( - title: artTitle, - previewText: artNode{"preview_text"}.getStr, - coverImage: select( - artNode{"cover_media_results", "result", "media_info", "original_img_url"}, - artNode{"cover_media", "media_info", "original_img_url"} - ).getImageStr, - tweetId: result.id - ) - - result.user = parseGraphUser(js{"core"}) - - if result.reply.len == 0: - with replyTo, js{"reply_to_user_results", "result", "core", "screen_name"}: - result.reply = @[replyTo.getStr] - - with count, js{"views", "count"}: - result.stats.views = count.getStr("0").parseInt - - with noteTweet, js{"note_tweet", "note_tweet_results", "result"}: - result.expandNoteTweetEntities(noteTweet) - - parseMediaEntities(js, result) - - # Hide card if it's redundant with attribution (same video shown via embed) - if result.attribution.isSome and result.card.isSome: - let cardUri = get(result.card).url.parseUri - if cardUri.isTwitterUrl: - let cardPath = cardUri.path.replace("/video/1", "") - if cardPath.len > 0 and cardPath == result.attributionLink: - get(result.card).kind = hidden - - # Handle retweets - check both legacy and top-level paths - with reposts, js{"legacy", "repostedStatusResults"}: - with rt, reposts{"result"}: - if "legacy" in rt or "rest_id" in rt: - result.retweet = some parseGraphTweet(rt) - - with quoted, js{"quoted_status_result", "result"}: - result.quote = some(parseGraphTweet(quoted)) - - with quoted, js{"quotedPostResults"}: - if "result" in quoted: - result.quote = some(parseGraphTweet(quoted{"result"})) + if result.quote.isSome: + let quote = get(result.quote).id + if $quote in global.tweets: + result.quote = some global.tweets[$quote] else: - result.quote = some Tweet(id: js{"legacy", "quoted_status_id_str"}.getId) + result.quote = some Tweet() - with ids, js{"edit_control", "edit_control_initial", "edit_tweet_ids"}: - for id in ids: - result.history.add parseBiggestInt(id.getStr) + if result.retweet.isSome: + let rt = get(result.retweet).id + if $rt in global.tweets: + result.retweet = some finalizeTweet(global, $rt) + else: + result.retweet = some Tweet() - with birdwatch, js{"birdwatch_pivot"}: - result.note = parseCommunityNote(birdwatch) +proc parsePin(js: JsonNode; global: GlobalObjects): Tweet = + let pin = js{"pinEntry", "entry", "entryId"}.getStr + if pin.len == 0: return -proc getConvSection(js: JsonNode): string = - let details = select( - js{"item", "client_event_info", "details"}, - js{"item", "clientEventInfo", "details"} - ) - select( - details{"conversation_details", "conversation_section"}, - details{"conversationDetails", "conversationSection"} - ).getStr + let id = pin.getId + if id notin global.tweets: return -proc parseGraphThread(js: JsonNode): tuple[thread: Chain; self: bool] = - var checkedSection = false - for t in ? js{"content", "items"}: - let entryId = t.getEntryId - if "tweet-" in entryId and "promoted" notin entryId: - if not checkedSection: - checkedSection = true - if getConvSection(t) == "RelatedTweet": - result.thread.related = true + global.tweets[id].pinned = true + return finalizeTweet(global, id) - let tweet = t.getTweetResult("item") - if tweet.notNull: - result.thread.content.add parseGraphTweet(tweet) +proc parseGlobalObjects(js: JsonNode): GlobalObjects = + result = GlobalObjects() + let + tweets = ? js{"globalObjects", "tweets"} + users = ? js{"globalObjects", "users"} - let tweetDisplayType = select( - t{"item", "content", "tweet_display_type"}, - t{"item", "itemContent", "tweetDisplayType"} - ) - if tweetDisplayType.getStr == "SelfThread": - result.self = true - else: - result.thread.content.add Tweet(id: entryId.getId) - elif "cursor-showmore" in entryId: - let cursor = t{"item", "content", "value"} - result.thread.cursor = cursor.getStr - result.thread.hasMore = true + for k, v in users: + result.users[k] = parseUser(v, k) -proc parseGraphTweetResult*(js: JsonNode): Tweet = - with tweet, js{"data", "tweet_result", "result"}: - result = parseGraphTweet(tweet) + for k, v in tweets: + var tweet = parseTweet(v) + if tweet.user.id in result.users: + tweet.user = result.users[tweet.user.id] + result.tweets[k] = tweet -proc parseTweetByRestId*(js: JsonNode): Tweet = - with tweet, js{"data", "tweetResult", "result"}: - result = parseGraphTweet(tweet) +proc parseThread(js: JsonNode; global: GlobalObjects): tuple[thread: Chain, self: bool] = + result.thread = Chain() -proc parseGraphTweetResults*(js: JsonNode): seq[Tweet] = - let results = js{"data", "tweetResult"} - if results.kind != JArray: return - for item in results: - let tweet = item{"result"} - if tweet.isNull: continue - let t = parseGraphTweet(tweet) - if t != nil: - result.add t + let thread = js{"content", "item", "content", "conversationThread"} + with cursor, thread{"showMoreCursor"}: + result.thread.cursor = cursor{"value"}.getStr + result.thread.hasMore = true -proc parseGraphConversation*(js: JsonNode; tweetId: string): Conversation = - result = Conversation(replies: Result[Chain](beginning: true)) + for t in thread{"conversationComponents"}: + let content = t{"conversationTweetComponent", "tweet"} - let instructions = ? select( - js{"data", "timelineResponse", "instructions"}, - js{"data", "timeline_response", "instructions"}, - js{"data", "threaded_conversation_with_injections_v2", "instructions"} - ) - if instructions.len == 0: - return + if content{"displayType"}.getStr == "SelfThread": + result.self = true - for i in instructions: - if i.getTypeName == "TimelineAddEntries": - for e in i{"entries"}: - let entryId = e.getEntryId - if entryId.startsWith("tweet-"): - let tweetResult = getTweetResult(e) - if tweetResult.notNull: - let tweet = parseGraphTweet(tweetResult) - - if not tweet.available: - tweet.id = entryId.getId - - if entryId.endsWith(tweetId): - result.tweet = tweet - else: - result.before.content.add tweet - elif not entryId.endsWith(tweetId): - result.before.content.add Tweet(id: entryId.getId) - elif entryId.startsWith("conversationthread") or - entryId.startsWith("tweetdetailrelatedtweets"): - let (thread, self) = parseGraphThread(e) - if self: - result.after = thread - elif thread.content.len > 0: - result.replies.content.add thread - elif entryId.startsWith("tombstone"): - let - content = select(e{"content", "content"}, e{"content", "itemContent"}) - tweet = Tweet( - id: entryId.getId, - available: false, - text: content{"tombstoneInfo", "richText"}.getTombstone - ) - - if $tweet.id == tweetId: - result.tweet = tweet - else: - result.before.content.add tweet - elif entryId.startsWith("cursor-bottom"): - var cursorValue = select( - e{"content", "value"}, - e{"content", "content", "value"}, - e{"content", "itemContent", "value"} - ) - result.replies.bottom = cursorValue.getStr - -proc parseGraphEditHistory*(js: JsonNode; tweetId: string): EditHistory = - let instructions = ? js{ - "data", "tweet_result_by_rest_id", "result", - "edit_history_timeline", "timeline", "instructions" - } - if instructions.len == 0: - return - - for i in instructions: - if i.getTypeName == "TimelineAddEntries": - for e in i{"entries"}: - let entryId = e.getEntryId - if entryId == "latestTweet": - with item, e{"content", "items"}[0]: - let tweetResult = item.getTweetResult("item") - if tweetResult.notNull: - result.latest = parseGraphTweet(tweetResult) - elif entryId == "staleTweets": - for item in e{"content", "items"}: - let tweetResult = item.getTweetResult("item") - if tweetResult.notNull: - result.history.add parseGraphTweet(tweetResult) - -iterator extractTweetsFromModuleItems(items: JsonNode): Tweet = - for item in items: - with tweetResult, item.getTweetResult("item"): - let tweet = parseGraphTweet(tweetResult) - if not tweet.available: - tweet.id = item.getEntryId.getId - yield tweet - -iterator extractListsFromItems(items: JsonNode): ListSearchResult = - for item in items: - with listJs, item{"item", "itemContent", "list"}: - let r = parseGraphSearchList(listJs) - if r.list.id.len > 0: - yield r - -proc extractTweetsFromEntry*(e: JsonNode): seq[Tweet] = - with tweetResult, getTweetResult(e): - let tweet = parseGraphTweet(tweetResult) + var tweet = finalizeTweet(global, content{"id"}.getStr) if not tweet.available: - tweet.id = e.getEntryId.getId - result.add tweet - return + tweet.tombstone = getTombstone(content{"tombstone"}) + result.thread.content.add tweet - for tweet in extractTweetsFromModuleItems(e{"content", "items"}): - result.add tweet +proc parseConversation*(js: JsonNode; tweetId: string): Conversation = + result = Conversation(replies: Result[Chain](beginning: true)) + let global = parseGlobalObjects(? js) -proc parseGraphTimeline*(js: JsonNode; after=""): Profile = - result = Profile(tweets: Timeline(beginning: after.len == 0)) - - let instructions = ? select( - js{"data", "list", "timeline_response", "timeline", "instructions"}, - js{"data", "user", "result", "timeline", "timeline", "instructions"}, - js{"data", "user_result", "result", "timeline_response", "timeline", "instructions"} - ) + let instructions = ? js{"timeline", "instructions"} if instructions.len == 0: return - for i in instructions: - if i{"moduleItems"}.notNull: - for tweet in extractTweetsFromModuleItems(i{"moduleItems"}): - result.tweets.content.add tweet - continue + for e in instructions[0]{"addEntries", "entries"}: + let entry = e{"entryId"}.getStr + if "tweet" in entry or "tombstone" in entry: + let tweet = finalizeTweet(global, e.getEntryId) + if $tweet.id != tweetId: + result.before.content.add tweet + else: + result.tweet = tweet + elif "conversationThread" in entry: + let (thread, self) = parseThread(e, global) + if thread.content.len > 0: + if self: + result.after = thread + else: + result.replies.content.add thread + elif "cursor-showMore" in entry: + result.replies.bottom = e.getCursor + elif "cursor-bottom" in entry: + result.replies.bottom = e.getCursor - if i{"entries"}.notNull: - for e in i{"entries"}: - let entryId = e.getEntryId - if entryId.startsWith("tweet") or entryId.startsWith("profile-grid"): - for tweet in extractTweetsFromEntry(e): - result.tweets.content.add tweet - elif "-conversation-" in entryId or entryId.startsWith("homeConversation"): - let (thread, self) = parseGraphThread(e) - result.tweets.content.add thread.content - elif entryId.startsWith("cursor-bottom"): - result.tweets.bottom = e{"content", "value"}.getStr +proc parseStatus*(js: JsonNode): Tweet = + with e, js{"errors"}: + if e.getError == tweetNotFound: + return - if after.len == 0: - if i.getTypeName == "TimelinePinEntry": - let tweets = extractTweetsFromEntry(i{"entry"}) - if tweets.len > 0: - var tweet = tweets[0] - tweet.pinned = true - result.pinned = some tweet + result = parseTweet(js) + if not result.isNil: + result.user = parseUser(js{"user"}) -proc parseGraphPhotoRail*(js: JsonNode): PhotoRail = - result = @[] + with quote, js{"quoted_status"}: + result.quote = some parseStatus(js{"quoted_status"}) - let instructions = select( - js{"data", "user", "result", "timeline", "timeline", "instructions"}, - js{"data", "user_result", "result", "timeline_response", "timeline", "instructions"} - ) - if instructions.len == 0: +proc parseInstructions[T](res: var Result[T]; global: GlobalObjects; js: JsonNode) = + if js.kind != JArray or js.len == 0: return - for i in instructions: - if i{"moduleItems"}.notNull: - for t in extractTweetsFromModuleItems(i{"moduleItems"}): - let photo = extractGalleryPhoto(t) - if photo.url.len > 0: - result.add photo + for i in js: + when T is Tweet: + if res.beginning and i{"pinEntry"}.notNull: + with pin, parsePin(i, global): + res.content.add pin - if result.len == 16: - return - continue + with r, i{"replaceEntry", "entry"}: + if "top" in r{"entryId"}.getStr: + res.top = r.getCursor + elif "bottom" in r{"entryId"}.getStr: + res.bottom = r.getCursor - if i.getTypeName != "TimelineAddEntries": - continue - - for e in i{"entries"}: - let entryId = e.getEntryId - if entryId.startsWith("tweet") or entryId.startsWith("profile-grid"): - for t in extractTweetsFromEntry(e): - let photo = extractGalleryPhoto(t) - if photo.url.len > 0: - result.add photo - - if result.len == 16: - return - -proc parseGraphSearch*[T: User | Tweets | ListSearchResult](js: JsonNode; after=""): Result[T] = - result = Result[T](beginning: after.len == 0) - - let instructions = select( - js{"data", "search", "timeline_response", "timeline", "instructions"}, - js{"data", "search_by_raw_query", "search_timeline", "timeline", "instructions"} - ) - if instructions.len == 0: - return - - for instruction in instructions: - let typ = getTypeName(instruction) - if typ == "TimelineAddEntries": - for e in instruction{"entries"}: - let entryId = e.getEntryId - when T is Tweets: - if entryId.startsWith("tweet") or entryId.startsWith("search-grid"): - for tweet in extractTweetsFromEntry(e): - result.content.add tweet - elif T is User: - if entryId.startsWith("user"): - with userRes, e{"content", "itemContent"}: - result.content.add parseGraphUser(userRes) - elif T is ListSearchResult: - if entryId.startsWith("list-search"): - for list in extractListsFromItems(e{"content", "items"}): - result.content.add list - - if entryId.startsWith("cursor-bottom"): - result.bottom = e{"content", "value"}.getStr - elif typ == "TimelineAddToModule": - when T is Tweets: - for tweet in extractTweetsFromModuleItems(instruction{"moduleItems"}): - result.content.add tweet - elif T is ListSearchResult: - for list in extractListsFromItems(instruction{"moduleItems"}): - result.content.add list - elif typ == "TimelineReplaceEntry": - if instruction{"entry_id_to_replace"}.getStr.startsWith("cursor-bottom"): - result.bottom = instruction{"entry", "content", "value"}.getStr - -proc parseGraphCommunityTimeline*(js: JsonNode; after=""): Timeline = +proc parseTimeline*(js: JsonNode; after=""): Timeline = result = Timeline(beginning: after.len == 0) + let global = parseGlobalObjects(? js) - let communityResult = js{"data", "communityResults", "result"} - let instructions = ? select( - communityResult{"ranked_community_timeline", "timeline", "instructions"}, - communityResult{"community_media_timeline", "timeline", "instructions"}, - communityResult{"community_filtered_timeline", "timeline", "instructions"} - ) - if instructions.len == 0: - return + let instructions = ? js{"timeline", "instructions"} + if instructions.len == 0: return - for i in instructions: - if i{"entries"}.notNull: - for e in i{"entries"}: - let entryId = e.getEntryId - if entryId.startsWith("tweet") or entryId.startsWith("profile-grid") or - entryId.startsWith("communities-grid"): - for tweet in extractTweetsFromEntry(e): - result.content.add tweet - elif entryId.startsWith("cursor-bottom"): - result.bottom = e{"content", "value"}.getStr + result.parseInstructions(global, instructions) - if after.len == 0 and i.getTypeName == "TimelinePinEntry": - var tweets = extractTweetsFromEntry(i{"entry"}) - for tweet in tweets.mitems: - tweet.pinned = true - if tweets.len > 0: - result.content.insert(tweets, 0) + for e in instructions[0]{"addEntries", "entries"}: + let entry = e{"entryId"}.getStr + if "tweet" in entry or entry.startsWith("sq-I-t") or "tombstone" in entry: + let tweet = finalizeTweet(global, e.getEntryId) + if not tweet.available: continue + result.content.add tweet + elif "cursor-top" in entry: + result.top = e.getCursor + elif "cursor-bottom" in entry: + result.bottom = e.getCursor + elif entry.startsWith("sq-C"): + with cursor, e{"content", "operation", "cursor"}: + if cursor{"cursorType"}.getStr == "Bottom": + result.bottom = cursor{"value"}.getStr + else: + result.top = cursor{"value"}.getStr -proc parseGraphCommunityMembers*(js: JsonNode; after=""): Result[User] = - result = Result[User](beginning: after.len == 0) - - let r = js{"data", "communityResults", "result"} - let slice = if not r{"members_slice"}.isNull: r{"members_slice"} - else: r{"moderators_slice"} - for item in slice{"items_results"}: - let user = parseGraphUser(item{"result"}) - if user.username.len > 0: - result.content.add user - - let cursor = slice{"slice_info", "next_cursor"}.getStr - if cursor.len > 0: - result.bottom = cursor +proc parsePhotoRail*(js: JsonNode): PhotoRail = + for tweet in js: + let + t = parseTweet(tweet) + url = if t.photos.len > 0: t.photos[0] + elif t.video.isSome: get(t.video).thumb + elif t.gif.isSome: get(t.gif).thumb + elif t.card.isSome: get(t.card).image + else: "" + if url.len == 0: continue + result.add GalleryPhoto(url: url, tweetId: $t.id) diff --git a/src/parserutils.nim b/src/parserutils.nim index 4b8e0b4..a605ea4 100644 --- a/src/parserutils.nim +++ b/src/parserutils.nim @@ -1,23 +1,15 @@ # SPDX-License-Identifier: AGPL-3.0-only -import std/[times, macros, htmlgen, options, algorithm, re] -import std/strutils except escape +import std/[strutils, times, macros, htmlgen, options, algorithm, re] import std/unicode except strip -from xmltree import escape import packedjson import types, utils, formatters -const - unicodeOpen = "\uFFFA" - unicodeClose = "\uFFFB" - xmlOpen = escape("<") - xmlClose = escape(">") - let unRegex = re"(^|[^A-z0-9-_./?])@([A-z0-9_]{1,15})" unReplace = "$1@$2" htRegex = re"(^|[^\w-_./?])([#$]|#)([\w_]+)" - htReplace = "$1$2$3" + htReplace = "$1$2$3" type ReplaceSliceKind = enum @@ -36,19 +28,13 @@ template `?`*(js: JsonNode): untyped = if j.isNull: return j -template select*(a, b: JsonNode): untyped = - if a.notNull: a else: b - -template select*(a, b, c: JsonNode): untyped = - if a.notNull: a elif b.notNull: b else: c - -template with*(ident, value, body): untyped = - if true: +template `with`*(ident, value, body): untyped = + block: let ident {.inject.} = value if ident != nil: body -template with*(ident; value: JsonNode; body): untyped = - if true: +template `with`*(ident; value: JsonNode; body): untyped = + block: let ident {.inject.} = value if value.notNull: body @@ -59,19 +45,6 @@ template getError*(js: JsonNode): Error = if js.kind != JArray or js.len == 0: null else: Error(js[0]{"code"}.getInt) -proc getTweetResult*(js: JsonNode; root="content"): JsonNode = - select( - js{root, "content", "tweet_results", "result"}, - js{root, "itemContent", "tweet_results", "result"}, - js{root, "content", "tweetResult", "result"} - ) - -template getTypeName*(js: JsonNode): string = - js{"__typename"}.getStr(js{"type"}.getStr) - -template getEntryId*(e: JsonNode): string = - e{"entryId"}.getStr(e{"entry_id"}.getStr) - template parseTime(time: string; f: static string; flen: int): DateTime = if time.len != flen: return parse(time, f, utc()) @@ -82,32 +55,29 @@ proc getDateTime*(js: JsonNode): DateTime = proc getTime*(js: JsonNode): DateTime = parseTime(js.getStr, "ddd MMM dd hh:mm:ss \'+0000\' yyyy", 30) -proc getTimeFromMs*(js: JsonNode): DateTime = - let ms = js.getInt(0) - if ms == 0: return - let seconds = ms div 1000 - return fromUnix(seconds).utc() - -proc getTimeFromMsStr*(js: JsonNode): DateTime = - var ms: int64 - try: ms = parseBiggestInt(js.getStr("0")) - except ValueError: return - if ms == 0: return - let seconds = ms div 1000 - return fromUnix(seconds).utc() - -proc getId*(id: string): int64 {.inline.} = +proc getId*(id: string): string {.inline.} = let start = id.rfind("-") - try: - parseBiggestInt(if start < 0: id else: id[start + 1 ..< id.len]) - except ValueError: 0'i64 + if start < 0: return id + id[start + 1 ..< id.len] proc getId*(js: JsonNode): int64 {.inline.} = case js.kind - of JString: return js.getStr("0").getId + of JString: return parseBiggestInt(js.getStr("0")) of JInt: return js.getBiggestInt() else: return 0 +proc getEntryId*(js: JsonNode): string {.inline.} = + let entry = js{"entryId"}.getStr + if entry.len == 0: return + + if "tweet" in entry or "sq-I-t" in entry: + return entry.getId + elif "tombstone" in entry: + return js{"content", "item", "content", "tombstone", "tweet", "id"}.getStr + else: + echo "unknown entry: ", entry + return + template getStrVal*(js: JsonNode; default=""): string = js{"string_value"}.getStr(default) @@ -119,9 +89,6 @@ proc getImageStr*(js: JsonNode): string = template getImageVal*(js: JsonNode): string = js{"image_value", "url"}.getImageStr -template getExpandedUrl*(js: JsonNode; fallback=""): string = - js{"expanded_url"}.getStr(js{"url"}.getStr(fallback)) - proc getCardUrl*(js: JsonNode; kind: CardKind): string = result = js{"website_url"}.getStrVal if kind == promoVideoConvo: @@ -163,38 +130,19 @@ proc getBanner*(js: JsonNode): string = return proc getTombstone*(js: JsonNode): string = - result = js{"text"}.getStr + result = js{"tombstoneInfo", "richText", "text"}.getStr result.removeSuffix(" Learn more") -proc getMp4Resolution*(url: string): int = - # parses the height out of a URL like this one: - # https://video.twimg.com/ext_tw_video//pu/vid/720x1280/.mp4 - const vidSep = "/vid/" - let - vidIdx = url.find(vidSep) + vidSep.len - resIdx = url.find('x', vidIdx) + 1 - res = url[resIdx ..< url.find("/", resIdx)] - - try: - return parseInt(res) - except ValueError: - # cannot determine resolution (e.g. m3u8/non-mp4 video) - return 0 - proc extractSlice(js: JsonNode): Slice[int] = result = js["indices"][0].getInt ..< js["indices"][1].getInt proc extractUrls(result: var seq[ReplaceSlice]; js: JsonNode; - textLen: int; hideTwitter = false; - hideArticle = false) = + textLen: int; hideTwitter = false) = let - url = js.getExpandedUrl + url = js["expanded_url"].getStr slice = js.extractSlice - if hideArticle and url.isTwitterUrl and "/article/" in url: - if slice.a < textLen: - result.add ReplaceSlice(kind: rkRemove, slice: slice) - elif hideTwitter and slice.b.succ >= textLen and url.isTwitterUrl: + if hideTwitter and slice.b.succ >= textLen and url.isTwitterUrl: if slice.a < textLen: result.add ReplaceSlice(kind: rkRemove, slice: slice) else: @@ -206,41 +154,28 @@ proc extractHashtags(result: var seq[ReplaceSlice]; js: JsonNode) = proc replacedWith(runes: seq[Rune]; repls: openArray[ReplaceSlice]; textSlice: Slice[int]): string = - let - runeLen = runes.len - safeStart = max(0, textSlice.a) - safeEnd = min(runeLen, textSlice.b) - - var validRepls: seq[ReplaceSlice] - for rep in repls: - if rep.slice.a >= 0 and rep.slice.b >= 0 and rep.slice.b < runeLen and rep.slice.a <= rep.slice.b: - validRepls.add rep - template extractLowerBound(i: int; idx): int = - if i > 0: min(validRepls[idx].slice.b.succ, runeLen) else: safeStart + if i > 0: repls[idx].slice.b.succ else: textSlice.a result = newStringOfCap(runes.len) - for i, rep in validRepls: - let lower = extractLowerBound(i, i - 1) - if lower < rep.slice.a: - result.add $runes[lower ..< rep.slice.a] + for i, rep in repls: + result.add $runes[extractLowerBound(i, i - 1) ..< rep.slice.a] case rep.kind of rkHashtag: - if rep.slice.a.succ <= rep.slice.b: - let - name = $runes[rep.slice.a.succ .. rep.slice.b] - symbol = $runes[rep.slice.a] - result.add a(symbol & name, href = "/search?f=tweets&q=%23" & name) + let + name = $runes[rep.slice.a.succ .. rep.slice.b] + symbol = $runes[rep.slice.a] + result.add a(symbol & name, href = "/search?q=%23" & name) of rkMention: - result.add a($runes[rep.slice], href = rep.url, title = escape(rep.display)) + result.add a($runes[rep.slice], href = rep.url, title = rep.display) of rkUrl: - result.add a(escape(rep.display), href = rep.url) + result.add a(rep.display, href = rep.url) of rkRemove: discard - let rest = extractLowerBound(validRepls.len, ^1) ..< safeEnd - if rest.a >= 0 and rest.a <= rest.b and rest.b <= runeLen: + let rest = extractLowerBound(repls.len, ^1) ..< textSlice.b + if rest.a <= rest.b: result.add $runes[rest] proc deduplicate(s: var seq[ReplaceSlice]) = @@ -265,7 +200,7 @@ proc expandUserEntities*(user: var User; js: JsonNode) = ent = ? js{"entities"} with urls, ent{"url", "urls"}: - user.website = urls[0].getExpandedUrl + user.website = urls[0]{"expanded_url"}.getStr var replacements = newSeq[ReplaceSlice]() @@ -280,38 +215,47 @@ proc expandUserEntities*(user: var User; js: JsonNode) = user.bio = user.bio.replacef(unRegex, unReplace) .replacef(htRegex, htReplace) -proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlice: Slice[int]; - replyTo=""; hasRedundantLink=false; hasArticle=false) = - let hasCard = tweet.card.isSome +proc expandTweetEntities*(tweet: Tweet; js: JsonNode) = + let + orig = tweet.text.toRunes + textRange = js{"display_text_range"} + textSlice = textRange{0}.getInt .. textRange{1}.getInt + hasQuote = js{"is_quote_status"}.getBool + hasCard = tweet.card.isSome + + var replyTo = "" + if tweet.replyId != 0: + with reply, js{"in_reply_to_screen_name"}: + tweet.reply.add reply.getStr + replyTo = reply.getStr + + let ent = ? js{"entities"} var replacements = newSeq[ReplaceSlice]() - with urls, entities{"urls"}: + with urls, ent{"urls"}: for u in urls: let urlStr = u["url"].getStr - if urlStr.len == 0 or urlStr notin text: + if urlStr.len == 0 or urlStr notin tweet.text: continue - - replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink, - hideArticle = hasArticle) - + replacements.extractUrls(u, textSlice.b, hideTwitter = hasQuote) if hasCard and u{"url"}.getStr == get(tweet.card).url: - get(tweet.card).url = u.getExpandedUrl + get(tweet.card).url = u{"expanded_url"}.getStr - with media, entities{"media"}: + with media, ent{"media"}: for m in media: replacements.extractUrls(m, textSlice.b, hideTwitter = true) - if "hashtags" in entities: - for hashtag in entities["hashtags"]: + if "hashtags" in ent: + for hashtag in ent["hashtags"]: replacements.extractHashtags(hashtag) - if "symbols" in entities: - for symbol in entities["symbols"]: + if "symbols" in ent: + for symbol in ent["symbols"]: replacements.extractHashtags(symbol) - if "user_mentions" in entities: - for mention in entities["user_mentions"]: + if "user_mentions" in ent: + for mention in ent["user_mentions"]: let name = mention{"screen_name"}.getStr slice = mention.extractSlice @@ -328,116 +272,5 @@ proc expandTextEntities(tweet: Tweet; entities: JsonNode; text: string; textSlic replacements.deduplicate replacements.sort(cmp) - tweet.text = text.toRunes.replacedWith(replacements, textSlice).strip(leading=false) - -proc expandTweetEntities*(tweet: Tweet; js: JsonNode; hasArticle=false) = - let - entities = ? js{"entities"} - textRange = js{"display_text_range"} - textSlice = textRange{0}.getInt .. textRange{1}.getInt - hasQuote = js{"is_quote_status"}.getBool - hasJobCard = tweet.card.isSome and get(tweet.card).kind == jobDetails - - var replyTo = "" - if tweet.replyId != 0: - with reply, js{"in_reply_to_screen_name"}: - replyTo = reply.getStr - tweet.reply.add replyTo - - tweet.expandTextEntities(entities, tweet.text, textSlice, replyTo, - hasQuote or hasJobCard, hasArticle) - -proc expandTextEntitiesV2(tweet: Tweet; js: JsonNode; text: string; textSlice: Slice[int]; - hasRedundantLink=false; hasArticle=false) = - let hasCard = tweet.card.isSome - - var replacements = newSeq[ReplaceSlice]() - - with urls, js{"url_entities"}: - for u in urls: - let urlStr = u["url"].getStr - if urlStr.len == 0 or urlStr notin text: - continue - - replacements.extractUrls(u, textSlice.b, hideTwitter = hasRedundantLink, - hideArticle = hasArticle) - - if hasCard and u{"url"}.getStr == get(tweet.card).url: - get(tweet.card).url = u.getExpandedUrl - - with hashtags, js{"details", "hashtag_entities"}: - for hashtag in hashtags: - replacements.extractHashtags(hashtag) - - with cashtags, js{"details", "cashtag_entities"}: - for cashtag in cashtags: - replacements.extractHashtags(cashtag) - - with mentions, js{"mention_entities"}: - for mention in mentions: - let - name = mention{"screen_name"}.getStr - slice = mention.extractSlice - idx = tweet.reply.find(name) - - if slice.a >= textSlice.a: - replacements.add ReplaceSlice(kind: rkMention, slice: slice, - url: "/" & name, display: mention["name"].getStr) - elif idx == -1 and tweet.replyId != 0: - tweet.reply.add name - - replacements.deduplicate - replacements.sort(cmp) - - tweet.text = text.toRunes.replacedWith(replacements, textSlice).strip(leading=false) - -proc expandTweetEntitiesV2*(tweet: Tweet; js: JsonNode; hasArticle=false) = - let - textRange = js{"details", "display_text_range"} - textSlice = textRange{0}.getInt .. textRange{1}.getInt - hasQuote = "quoted_tweet_results" in js - hasJobCard = tweet.card.isSome and get(tweet.card).kind == jobDetails - hasAttribution = tweet.attribution.isSome - - tweet.expandTextEntitiesV2(js, tweet.text, textSlice, - hasQuote or hasJobCard or hasAttribution, - hasArticle) - -proc expandNoteTweetEntities*(tweet: Tweet; js: JsonNode) = - let - entities = ? js{"entity_set"} - text = js{"text"}.getStr.multiReplace(("<", unicodeOpen), (">", unicodeClose)) - textSlice = 0..text.runeLen - hasAttribution = tweet.attribution.isSome - - tweet.expandTextEntities(entities, text, textSlice, hasRedundantLink=hasAttribution) - - tweet.text = tweet.text.multiReplace((unicodeOpen, xmlOpen), (unicodeClose, xmlClose)) - -proc expandBirdwatchEntities*(text: string; entities: JsonNode): string = - let runes = text.toRunes - var replacements: seq[ReplaceSlice] - - for entity in entities: - let - fromIdx = entity{"from_index"}.getInt - toIdx = entity{"to_index"}.getInt - url = entity{"ref", "url"}.getStr - if url.len > 0: - replacements.add ReplaceSlice( - kind: rkUrl, - slice: fromIdx ..< toIdx, - url: url, - display: $runes[fromIdx ..< min(toIdx, runes.len)] - ) - - replacements.sort(cmp) - result = runes.replacedWith(replacements, 0 ..< runes.len) - -proc extractGalleryPhoto*(t: Tweet): GalleryPhoto = - let url = - if t.media.len > 0: t.media[0].getThumb - elif t.card.isSome: get(t.card).image - else: "" - - result = GalleryPhoto(url: url, tweetId: $t.id) + tweet.text = orig.replacedWith(replacements, textSlice) + .strip(leading=false) diff --git a/src/prefs.nim b/src/prefs.nim index 1a75f75..fa40a6d 100644 --- a/src/prefs.nim +++ b/src/prefs.nim @@ -1,22 +1,22 @@ # SPDX-License-Identifier: AGPL-3.0-only -import tables, strutils +import tables import types, prefs_impl from config import get from parsecfg import nil -export genUpdatePrefs, genResetPrefs, genApplyPrefs +export genUpdatePrefs, genResetPrefs var defaultPrefs*: Prefs proc updateDefaultPrefs*(cfg: parsecfg.Config) = genDefaultPrefs() -proc getPrefs*(cookies, params: Table[string, string]): Prefs = +proc getPrefs*(cookies: Table[string, string]): Prefs = result = defaultPrefs - genParsePrefs(cookies) - genParsePrefs(params) + genCookiePrefs(cookies) -proc encodePrefs*(prefs: Prefs): string = - var encPairs: seq[string] - genEncodePrefs(prefs) - encPairs.join(",") +template getPref*(cookies: Table[string, string], pref): untyped = + bind genCookiePref + var res = defaultPrefs.`pref` + genCookiePref(cookies, pref, res) + res diff --git a/src/prefs_impl.nim b/src/prefs_impl.nim index 8519bd5..0223c82 100644 --- a/src/prefs_impl.nim +++ b/src/prefs_impl.nim @@ -60,9 +60,6 @@ genPrefs: stickyProfile(checkbox, true): "Make profile sidebar stick to top" - stickyNav(checkbox, true): - "Keep navbar fixed to top" - bidiSupport(checkbox, false): "Support bidirectional text (makes clicking on tweets harder)" @@ -78,12 +75,6 @@ genPrefs: hideReplies(checkbox, false): "Hide tweet replies" - hideRelated(checkbox, true): - "Hide related tweets under replies" - - hideCommunityNotes(checkbox, false): - "Hide community notes" - squareAvatars(checkbox, false): "Square profile pictures" @@ -92,7 +83,7 @@ genPrefs: "Enable mp4 video playback (only for gifs)" hlsPlayback(checkbox, false): - "Enable HLS video streaming (requires JavaScript)" + "Enable hls video streaming (requires JavaScript)" proxyVideos(checkbox, true): "Proxy video streaming through the server (might be slow)" @@ -103,17 +94,6 @@ genPrefs: autoplayGifs(checkbox, true): "Autoplay gifs" - compactGallery(checkbox, false): - "Compact media gallery (no profile info or text)" - - gallerySize(select, "Medium"): - "Gallery column size" - options: @["Small", "Medium", "Large"] - - mediaView(select, "Timeline"): - "Default media view" - options: @["Timeline", "Grid", "Gallery"] - "Link replacements (blank to disable)": replaceTwitter(input, ""): "Twitter -> Nitter" @@ -127,6 +107,10 @@ genPrefs: "Reddit -> Teddit/Libreddit" placeholder: "Teddit hostname" + replaceInstagram(input, ""): + "Instagram -> Bibliogram" + placeholder: "Bibliogram hostname" + iterator allPrefs*(): Pref = for k, v in prefList: for pref in v: @@ -147,7 +131,7 @@ macro genDefaultPrefs*(): untyped = result.add quote do: defaultPrefs.`ident` = cfg.get("Preferences", `name`, `default`) -macro genParsePrefs*(prefs): untyped = +macro genCookiePrefs*(cookies): untyped = result = nnkStmtList.newTree() for pref in allPrefs(): let @@ -157,17 +141,37 @@ macro genParsePrefs*(prefs): untyped = options = pref.options result.add quote do: - if `name` in `prefs`: + if `name` in `cookies`: when `kind` == input or `name` == "theme": - result.`ident` = `prefs`[`name`] + result.`ident` = `cookies`[`name`] elif `kind` == checkbox: - result.`ident` = `prefs`[`name`] == "on" or - `prefs`[`name`] == "true" or - `prefs`[`name`] == "1" + result.`ident` = `cookies`[`name`] == "on" else: - let value = `prefs`[`name`] + let value = `cookies`[`name`] if value in `options`: result.`ident` = value +macro genCookiePref*(cookies, prefName, res): untyped = + result = nnkStmtList.newTree() + for pref in allPrefs(): + let ident = ident(pref.name) + if ident != prefName: + continue + + let + name = pref.name + kind = newLit(pref.kind) + options = pref.options + + result.add quote do: + if `name` in `cookies`: + when `kind` == input or `name` == "theme": + `res` = `cookies`[`name`] + elif `kind` == checkbox: + `res` = `cookies`[`name`] == "on" + else: + let value = `cookies`[`name`] + if value in `options`: `res` = value + macro genUpdatePrefs*(): untyped = result = nnkStmtList.newTree() let req = ident("request") @@ -202,36 +206,6 @@ macro genResetPrefs*(): untyped = result.add quote do: savePref(`name`, "", `req`, expire=true) -macro genEncodePrefs*(prefs): untyped = - result = nnkStmtList.newTree() - for pref in allPrefs(): - let - name = newLit(pref.name) - ident = ident(pref.name) - kind = newLit(pref.kind) - defaultIdent = nnkDotExpr.newTree(ident("defaultPrefs"), ident(pref.name)) - - result.add quote do: - when `kind` == checkbox: - if `prefs`.`ident` != `defaultIdent`: - if `prefs`.`ident`: - encPairs.add `name` & "=on" - else: - encPairs.add `name` & "=" - else: - if `prefs`.`ident` != `defaultIdent`: - encPairs.add `name` & "=" & `prefs`.`ident` - -macro genApplyPrefs*(params, req): untyped = - result = nnkStmtList.newTree() - for pref in allPrefs(): - let name = newLit(pref.name) - result.add quote do: - if `name` in `params`: - savePref(`name`, `params`[`name`], `req`) - else: - savePref(`name`, "", `req`, expire=true) - macro genPrefsType*(): untyped = let name = nnkPostfix.newTree(ident("*"), ident("Prefs")) result = quote do: diff --git a/src/query.nim b/src/query.nim index ecb428d..cf9b0e6 100644 --- a/src/query.nim +++ b/src/query.nim @@ -1,14 +1,15 @@ # SPDX-License-Identifier: AGPL-3.0-only import strutils, strformat, sequtils, tables, uri -import types, utils +import types const validFilters* = @[ "media", "images", "twimg", "videos", - "native_video", "consumer_video", "spaces", + "native_video", "consumer_video", "pro_video", "links", "news", "quote", "mentions", - "replies", "retweets", "nativeretweets" + "replies", "retweets", "nativeretweets", + "verified", "safe" ] emptyQuery* = "include:nativeretweets" @@ -20,43 +21,32 @@ template `@`(param: string): untyped = proc initQuery*(pms: Table[string, string]; name=""): Query = result = Query( kind: parseEnum[QueryKind](@"f", tweets), - view: @"view", text: @"q", filters: validFilters.filterIt("f-" & it in pms), excludes: validFilters.filterIt("e-" & it in pms), since: @"since", until: @"until", - minLikes: validateNumber(@"min_faves") + near: @"near" ) - # articles is an internal tab kind, not a valid search filter - if result.kind == QueryKind.articles: - result.kind = tweets - if name.len > 0: result.fromUser = name.split(",") proc getMediaQuery*(name: string): Query = Query( - kind: QueryKind.media, + kind: media, filters: @["twimg", "native_video"], fromUser: @[name], sep: "OR" ) -proc getArticlesQuery*(name: string): Query = - Query( - kind: QueryKind.articles, - fromUser: @[name] - ) - proc getReplyQuery*(name: string): Query = Query( kind: replies, fromUser: @[name] ) -proc genQueryParam*(query: Query; maxId=""): string = +proc genQueryParam*(query: Query): string = var filters: seq[string] param: string @@ -65,20 +55,15 @@ proc genQueryParam*(query: Query; maxId=""): string = return query.text for i, user in query.fromUser: - if i == 0: - param = "(" - - param &= &"from:{user}" + param &= &"from:{user} " if i < query.fromUser.high: - param &= " OR " - else: - param &= ")" + param &= "OR " - if query.fromUser.len > 0 and query.kind in {posts, QueryKind.media}: - param &= " (filter:self_threads OR -filter:replies)" + if query.fromUser.len > 0 and query.kind in {posts, media}: + param &= "filter:self_threads OR-filter:replies " if "nativeretweets" notin query.excludes: - param &= " include:nativeretweets" + param &= "include:nativeretweets " for f in query.filters: filters.add "filter:" & f @@ -88,51 +73,38 @@ proc genQueryParam*(query: Query; maxId=""): string = for i in query.includes: filters.add "include:" & i - if filters.len > 0: - result = strip(param & " (" & filters.join(&" {query.sep} ") & ")") - else: - result = strip(param) - + result = strip(param & filters.join(&" {query.sep} ")) if query.since.len > 0: result &= " since:" & query.since - if query.until.len > 0 and maxId.len == 0: + if query.until.len > 0: result &= " until:" & query.until - if query.minLikes.len > 0: - result &= " min_faves:" & query.minLikes + if query.near.len > 0: + result &= &" near:\"{query.near}\" within:15mi" if query.text.len > 0: if result.len > 0: result &= " " & query.text else: result = query.text - if result.len > 0 and maxId.len > 0: - result &= " max_id:" & maxId - proc genQueryUrl*(query: Query): string = - var params: seq[string] + if query.kind notin {tweets, users}: return - if query.view.len > 0: - params.add "view=" & encodeUrl(query.view) + var params = @[&"f={query.kind}"] + if query.text.len > 0: + params.add "q=" & encodeUrl(query.text) + for f in query.filters: + params.add "f-" & f & "=on" + for e in query.excludes: + params.add "e-" & e & "=on" + for i in query.includes.filterIt(it != "nativeretweets"): + params.add "i-" & i & "=on" - # media doubles as the profile media tab, where f isn't part of the URL scheme - if query.kind in {tweets, users, lists, top} or - (query.kind == QueryKind.media and query.fromUser.len == 0): - params.add &"f={query.kind}" - if query.text.len > 0: - params.add "q=" & encodeUrl(query.text) - for f in query.filters: - params.add &"f-{f}=on" - for e in query.excludes: - params.add &"e-{e}=on" - for i in query.includes.filterIt(it != "nativeretweets"): - params.add &"i-{i}=on" - - if query.since.len > 0: - params.add "since=" & query.since - if query.until.len > 0: - params.add "until=" & query.until - if query.minLikes.len > 0: - params.add "min_faves=" & query.minLikes + if query.since.len > 0: + params.add "since=" & query.since + if query.until.len > 0: + params.add "until=" & query.until + if query.near.len > 0: + params.add "near=" & query.near if params.len > 0: result &= params.join("&") diff --git a/src/redis_cache.nim b/src/redis_cache.nim index b9ddbcc..469157a 100644 --- a/src/redis_cache.nim +++ b/src/redis_cache.nim @@ -52,7 +52,6 @@ proc initRedisPool*(cfg: Config) {.async.} = await migrate("profileDates", "p:*") await migrate("profileStats", "p:*") await migrate("userType", "p:*") - await migrate("verifiedType", "p:*") pool.withAcquire(r): # optimize memory usage for user ID buckets @@ -86,7 +85,7 @@ proc cache*(data: List) {.async.} = await setEx(data.listKey, listCacheTime, compress(toFlatty(data))) proc cache*(data: PhotoRail; name: string) {.async.} = - await setEx("pr2:" & toLower(name), baseCacheTime * 2, compress(toFlatty(data))) + await setEx("pr:" & toLower(name), baseCacheTime, compress(toFlatty(data))) proc cache*(data: User) {.async.} = if data.username.len == 0: return @@ -119,11 +118,11 @@ proc getUserId*(username: string): Future[string] {.async.} = pool.withAcquire(r): result = await r.hGet(name.uidKey, name) if result == redisNil: - let user = await getGraphUser(username) + let user = await getUser(username) if user.suspended: return "suspended" else: - await all(cacheUserId(name, user.id), cache(user)) + await cacheUserId(name, user.id) return user.id proc getCachedUser*(username: string; fetch=true): Future[User] {.async.} = @@ -131,7 +130,8 @@ proc getCachedUser*(username: string; fetch=true): Future[User] {.async.} = if prof != redisNil: prof.deserialize(User) elif fetch: - result = await getGraphUser(username) + let userId = await getUserId(username) + result = await getGraphUser(userId) await cache(result) proc getCachedUsername*(userId: string): Future[string] {.async.} = @@ -142,96 +142,28 @@ proc getCachedUsername*(userId: string): Future[string] {.async.} = if username != redisNil: result = username else: - let user = await getGraphUserById(userId) + let user = await getUserById(userId) result = user.username - if result.len > 0: - await setEx(key, baseCacheTime, result) - if user.id.len > 0: - await all(cacheUserId(result, user.id), cache(user)) + await setEx(key, baseCacheTime, result) -# proc getCachedTweet*(id: int64): Future[Tweet] {.async.} = -# if id == 0: return -# let tweet = await get(id.tweetKey) -# if tweet != redisNil: -# tweet.deserialize(Tweet) -# else: -# result = await getGraphTweetResult($id) -# if not result.isNil: -# await cache(result) - -proc cache*(data: Broadcast) {.async.} = - if data.id.len == 0: return - await setEx("bc:" & data.id, baseCacheTime, compress(toFlatty(data))) - -proc getCachedBroadcast*(id: string): Future[Broadcast] {.async.} = - if id.len == 0: return - let cached = await get("bc:" & id) - if cached != redisNil: - cached.deserialize(Broadcast) +proc getCachedTweet*(id: int64): Future[Tweet] {.async.} = + if id == 0: return + let tweet = await get(id.tweetKey) + if tweet != redisNil: + tweet.deserialize(Tweet) else: - result = await getBroadcastInfo(id) - await cache(result) - result.m3u8Url = await fetchBroadcastStream(result.mediaKey) + result = await getStatus($id) + if result.isNil: + await cache(result) -proc cache*(data: AudioSpace) {.async.} = - if data.id.len == 0: return - let ttl = if data.state == "RUNNING": baseCacheTime div 6 else: baseCacheTime - await setEx("sp:" & data.id, ttl, compress(toFlatty(data))) - -proc getCachedAudioSpace*(id: string): Future[AudioSpace] {.async.} = - if id.len == 0: return - let cached = await get("sp:" & id) - if cached != redisNil: - cached.deserialize(AudioSpace) - else: - result = await getAudioSpace(id) - await cache(result) - result.m3u8Url = await fetchBroadcastStream(result.mediaKey) - -proc cache*(data: AccountInfo; name: string) {.async.} = - await setEx("ai:" & toLower(name), baseCacheTime * 24, compress(toFlatty(data))) - -proc getCachedAccountInfo*(username: string; fetch=true): Future[AccountInfo] {.async.} = - if username.len == 0: return - let name = toLower(username) - let cached = await get("ai:" & name) - if cached != redisNil: - cached.deserialize(AccountInfo) - elif fetch: - result = await getAboutAccount(username) - await cache(result, name) - -proc getCachedPhotoRail*(id: string): Future[PhotoRail] {.async.} = - if id.len == 0: return - let rail = await get("pr2:" & toLower(id)) +proc getCachedPhotoRail*(name: string): Future[PhotoRail] {.async.} = + if name.len == 0: return + let rail = await get("pr:" & toLower(name)) if rail != redisNil: rail.deserialize(PhotoRail) else: - result = await getPhotoRail(id) - await cache(result, id) - -proc cache*(data: Community) {.async.} = - if data.id.len == 0: return - await setEx("cm:" & data.id, listCacheTime, compress(toFlatty(data))) - -proc getCachedCommunity*(id: string): Future[Community] {.async.} = - if id.len == 0: return - let cached = await get("cm:" & id) - if cached != redisNil: - cached.deserialize(Community) - else: - result = await getGraphCommunity(id) - await cache(result) - -proc getCachedCommunityModerators*(id: string): Future[seq[User]] {.async.} = - if id.len == 0: return - let cached = await get("cmm:" & id) - if cached != redisNil: - cached.deserialize(seq[User]) - else: - let mods = await getGraphCommunityModerators(id) - result = mods.content - await setEx("cmm:" & id, listCacheTime, compress(toFlatty(result))) + result = await getPhotoRail(name) + await cache(result, name) proc getCachedList*(username=""; slug=""; id=""): Future[List] {.async.} = let list = if id.len == 0: redisNil diff --git a/src/routes/article.nim b/src/routes/article.nim deleted file mode 100644 index 0a7d1dc..0000000 --- a/src/routes/article.nim +++ /dev/null @@ -1,48 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, tables, strutils -import jester, karax/vdom -import ".."/[types, api] -import ../views/[article, general] -import router_utils - -export api, article, vdom, general, router_utils - -proc createArticleRouter*(cfg: Config) = - router articleRoute: - get "/i/article/@id": - cond @"id".allCharsInSet(Digits) - - let article = await getGraphArticle(@"id") - if article == nil: - resp Http404, showError("Article not found", cfg) - - var tweetIds: seq[string] - for e in article.entities.values: - if e.kind == "TWEET": - tweetIds.add e.tweetId - - var tweets = initTable[int64, Tweet]() - if tweetIds.len > 0: - try: - for t in await getGraphTweetResults(tweetIds): - tweets[t.id] = t - except CatchableError: - discard - - let - prefs = requestPrefs() - path = getPath() - html = renderArticle(article, tweets, path, prefs, @"id") - twitterUrl = "https://x.com/" & article.user.username & "/article/" & @"id" - resp renderMain(html, request, cfg, prefs, titleText=article.title, - twitterLink=twitterUrl) - - get "/@name/article/@id/?": - cond '.' notin @"name" - cond @"id".allCharsInSet(Digits) - redirect("/i/article/" & @"id") - - get "/@name/status/@id/article": - cond '.' notin @"name" - cond @"id".allCharsInSet(Digits) - redirect("/i/article/" & @"id") diff --git a/src/routes/broadcast.nim b/src/routes/broadcast.nim deleted file mode 100644 index d3bb95a..0000000 --- a/src/routes/broadcast.nim +++ /dev/null @@ -1,44 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, strutils -import jester - -import router_utils -import ".."/[types, formatters, redis_cache] -import ../views/[general, broadcast] -import media - -export broadcast - -proc createBroadcastRouter*(cfg: Config) = - router broadcastRoute: - get "/i/broadcasts/@id": - cond @"id".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9'}) - var bc: Broadcast - try: - bc = await getCachedBroadcast(@"id") - except: - discard - - if bc.id.len == 0: - resp Http404, showError("Broadcast not found", cfg) - - let prefs = requestPrefs() - resp renderMain(renderBroadcast(bc, prefs, request.path), request, cfg, prefs, - bc.title, ogTitle=bc.title) - - get "/i/broadcasts/@id/stream": - cond @"id".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9'}) - var bc: Broadcast - try: - bc = await getCachedBroadcast(@"id") - except: - discard - - if bc.m3u8Url.len == 0: - resp Http404 - - let manifest = await safeFetch(bc.m3u8Url) - if manifest.len == 0: - resp Http502 - - resp proxifyVideo(manifest, requestPrefs().proxyVideos, bc.m3u8Url), m3u8Mime diff --git a/src/routes/community.nim b/src/routes/community.nim deleted file mode 100644 index b850b6b..0000000 --- a/src/routes/community.nim +++ /dev/null @@ -1,89 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import strformat - -import jester - -import router_utils -import ".."/[types, redis_cache, api] -import ../views/[general, timeline, community] - -export community - -template respCommunity*(cmty: Community; title: string; nav, vnode: typed) = - if cmty.id.len == 0 or cmty.name.len == 0: - resp Http404, showError(&"""Community "{@"id"}" not found""", cfg) - - let html = renderCommunity(vnode, nav, cmty) - resp renderMain(html, request, cfg, prefs, titleText=title, banner=cmty.banner) - -proc createCommunityRouter*(cfg: Config) = - router community: - get "/i/communities/@id/?": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - tl = await getGraphCommunityTweets(cmty.id, "Relevance", getCursor()) - respCommunity(cmty, cmty.name, - renderCommunityTabs(QueryKind.posts, cmty), - renderTimelineTweets(tl, prefs, request.path)) - - get "/i/communities/@id/latest": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - tl = await getGraphCommunityTweets(cmty.id, "Recency", getCursor()) - respCommunity(cmty, cmty.name & " - Latest", - renderCommunityTabs(QueryKind.replies, cmty), - renderTimelineTweets(tl, prefs, request.path)) - - get "/i/communities/@id/media": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - tl = await getGraphCommunityMedia(cmty.id, getCursor()) - respCommunity(cmty, cmty.name & " - Media", - renderCommunityTabs(QueryKind.media, cmty), - renderTimelineTweets(tl, prefs, request.path)) - - get "/i/communities/@id/about": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - mods = await getCachedCommunityModerators(cmty.id) - respCommunity(cmty, cmty.name & " - About", - renderCommunityTabs(QueryKind.userList, cmty), - renderCommunityAbout(cmty, mods)) - - get "/i/communities/@id/members": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - members = await getGraphCommunityMembers(cmty.id, getCursor()) - respCommunity(cmty, cmty.name & " - Members", - renderMemberTabs(cmty, false), - renderTimelineUsers(members, prefs, request.path)) - - get "/i/communities/@id/moderators": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - mods = await getCachedCommunityModerators(cmty.id) - respCommunity(cmty, cmty.name & " - Moderators", - renderMemberTabs(cmty, true), - renderTimelineUsers(Result[User](content: mods), prefs, request.path)) - - get "/i/communities/@id/hashtag/@tag": - cond '.' notin @"id" - let - prefs = requestPrefs() - cmty = await getCachedCommunity(@"id") - tl = await getGraphCommunityHashtags(cmty.id, @"tag", getCursor()) - respCommunity(cmty, cmty.name & " - #" & @"tag", - renderHashtagHeader(cmty, @"tag"), - renderTimelineTweets(tl, prefs, request.path)) diff --git a/src/routes/debug.nim b/src/routes/debug.nim index 97c5bef..192786e 100644 --- a/src/routes/debug.nim +++ b/src/routes/debug.nim @@ -1,13 +1,10 @@ # SPDX-License-Identifier: AGPL-3.0-only import jester import router_utils -import ".."/[auth, types] +import ".."/[tokens, types] proc createDebugRouter*(cfg: Config) = router debug: - get "/.health": - respJson getSessionPoolHealth() - - get "/.sessions": + get "/.tokens": cond cfg.enableDebug - respJson getSessionPoolDebug() + respJson getPoolJson() diff --git a/src/routes/embed.nim b/src/routes/embed.nim index 24bba2d..1a93d40 100644 --- a/src/routes/embed.nim +++ b/src/routes/embed.nim @@ -1,140 +1,36 @@ # SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, strutils, strformat, json +import asyncdispatch, strutils, options import jester, karax/vdom -import ".."/[types, api, formatters] +import ".."/[types, api] import ../views/[embed, tweet, general] -include "../views/oembed.nimf" import router_utils export api, embed, vdom, tweet, general, router_utils -proc parseTweetPath(path: string): tuple[username, id: string] = - let parts = path.split('/') - if parts.len >= 3 and parts[1] in ["status", "statuses"]: - let tweetId = parts[2].split('?')[0].split('#')[0] - if tweetId.len > 0 and tweetId.allCharsInSet(Digits): - return (parts[0], tweetId) - return ("", "") - -proc parseTweetUrl*(url: string; cfg: Config): tuple[username, id: string] = - var path = url - if path.startsWith("https://"): - path = path[8..^1] - elif path.startsWith("http://"): - path = path[7..^1] - - const twitterPrefixes = ["twitter.com/", "x.com/", "mobile.twitter.com/", - "www.twitter.com/", "www.x.com/"] - - for prefix in twitterPrefixes: - if path.startsWith(prefix): - return parseTweetPath(path[prefix.len..^1]) - - let nitterPrefix = cfg.hostname & "/" - if path.startsWith(nitterPrefix): - return parseTweetPath(path[nitterPrefix.len..^1]) - - # Fall back: strip any hostname and try to parse as a tweet path. - # Handles requests where the URL's host differs from cfg.hostname - # (e.g. localhost in dev/CI, or a reverse proxy with a different domain). - let slashPos = path.find('/') - if slashPos > 0: - let afterHost = path[slashPos + 1..^1] - let parsed = parseTweetPath(afterHost) - if parsed.username.len > 0: - return parsed - - return ("", "") - proc createEmbedRouter*(cfg: Config) = router embed: get "/i/videos/tweet/@id": - let - id = @"id" - tweet = await getTweetByRestId(id) - prefs = requestPrefs() + let convo = await getTweet(@"id") + if convo == nil or convo.tweet == nil or convo.tweet.video.isNone: + resp Http404 - if tweet == nil: - resp renderErrorEmbed("Tweet not found", prefs, cfg, request, tweetId=id) - - if not tweet.hasVideos: - resp renderErrorEmbed("No video in tweet", prefs, cfg, request, - tweetId=id, username=tweet.user.username) - - resp renderVideoEmbed(tweet, cfg, request) + resp renderVideoEmbed(convo.tweet, cfg, request) get "/@user/status/@id/embed": let - id = @"id" - user = @"user" - tweet = await getTweetByRestId(id) - prefs = requestPrefs() + convo = await getTweet(@"id") + prefs = cookiePrefs() path = getPath() - if tweet == nil: - resp renderErrorEmbed("Tweet not found", prefs, cfg, request, - tweetId=id, username=user) + if convo == nil or convo.tweet == nil: + resp Http404 - resp renderTweetEmbed(tweet, path, prefs, cfg, request) + resp $renderTweetEmbed(convo.tweet, path, prefs, cfg, request) get "/embed/Tweet.html": let id = @"id" if id.len > 0: - redirect(&"/i/status/{id}/embed") + redirect("/i/status/" & id & "/embed") else: resp Http404 - - get "/api/oembed": - responseHeaders().get.add(("Access-Control-Allow-Origin", "*")) - - let - url = @"url" - format = @"format" - - if format.len > 0 and format != "json": - resp Http501, "Only JSON format is supported" - - if url.len == 0: - resp Http400, "Missing url parameter" - - let (username, tweetId) = parseTweetUrl(url, cfg) - if username.len == 0 or tweetId.len == 0: - resp Http400, "Invalid tweet URL" - - let tweet = await getTweetByRestId(tweetId) - if tweet == nil: - resp Http404 - - let - maxwidthParam = @"maxwidth" - maxwidth = if maxwidthParam.len > 0: - try: clamp(parseInt(maxwidthParam), 220, 550) - except ValueError: 550 - else: 550 - embedUrl = getUrlPrefix(cfg) & "/" & tweet.user.username & "/status/" & tweetId & "/embed" - authorUrl = getUrlPrefix(cfg) & "/" & tweet.user.username - title = stripHtml(tweet.text) - - var response = %*{ - "version": "1.0", - "type": "rich", - "provider_name": cfg.title, - "provider_url": getUrlPrefix(cfg), - "title": title, - "author_name": tweet.user.fullname, - "author_url": authorUrl, - "url": embedUrl, - "width": maxwidth, - "height": newJNull(), - "cache_age": "3153600000", - "html": renderOembedIframe(embedUrl, maxwidth) - } - - if tweet.media.len > 0: - let thumbUrl = getUrlPrefix(cfg) & getPicUrl(tweet.media[0].getThumb) - response["thumbnail_url"] = %thumbUrl - response["thumbnail_width"] = %maxwidth - response["thumbnail_height"] = %maxwidth - - respJson response diff --git a/src/routes/list.nim b/src/routes/list.nim index b4ab091..d466080 100644 --- a/src/routes/list.nim +++ b/src/routes/list.nim @@ -1,25 +1,23 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, uri +import strutils, uri import jester import router_utils import ".."/[types, redis_cache, api] import ../views/[general, timeline, list] +export getListTimeline, getGraphList template respList*(list, timeline, title, vnode: typed) = if list.id.len == 0 or list.name.len == 0: - resp Http404, showError(&"""List "{@"id"}" not found""", cfg) + resp Http404, showError("List " & @"id" & " not found", cfg) let html = renderList(vnode, timeline.query, list) - rss = if cfg.enableRSSList: &"""/i/lists/{@"id"}/rss""" else: "" + rss = "/i/lists/$1/rss" % [@"id"] resp renderMain(html, request, cfg, prefs, titleText=title, rss=rss, banner=list.banner) -proc title*(list: List): string = - &"@{list.username}/{list.name}" - proc createListRouter*(cfg: Config) = router list: get "/@name/lists/@slug/?": @@ -30,22 +28,24 @@ proc createListRouter*(cfg: Config) = slug = decodeUrl(@"slug") list = await getCachedList(@"name", slug) if list.id.len == 0: - resp Http404, showError(&"""List "{@"slug"}" not found""", cfg) - redirect(&"/i/lists/{list.id}") + resp Http404, showError("List \"" & @"slug" & "\" not found", cfg) + redirect("/i/lists/" & list.id) get "/i/lists/@id/?": cond '.' notin @"id" let - prefs = requestPrefs() + prefs = cookiePrefs() list = await getCachedList(id=(@"id")) - timeline = await getGraphListTweets(list.id, getCursor()) + title = "@" & list.username & "/" & list.name + timeline = await getListTimeline(list.id, getCursor()) vnode = renderTimelineTweets(timeline, prefs, request.path) - respList(list, timeline, list.title, vnode) + respList(list, timeline, title, vnode) get "/i/lists/@id/members": cond '.' notin @"id" let - prefs = requestPrefs() + prefs = cookiePrefs() list = await getCachedList(id=(@"id")) + title = "@" & list.username & "/" & list.name members = await getGraphListMembers(list, getCursor()) - respList(list, members, list.title, renderTimelineUsers(members, prefs, request.path)) + respList(list, members, title, renderTimelineUsers(members, prefs, request.path)) diff --git a/src/routes/media.nim b/src/routes/media.nim index 3442916..c953a93 100644 --- a/src/routes/media.nim +++ b/src/routes/media.nim @@ -1,7 +1,6 @@ # SPDX-License-Identifier: AGPL-3.0-only import uri, strutils, httpclient, os, hashes, base64, re import asynchttpserver, asyncstreams, asyncfile, asyncnet -import asyncdispatch import jester @@ -16,9 +15,7 @@ const maxAge* = "max-age=604800" proc safeFetch*(url: string): Future[string] {.async.} = - # maxRedirects=0: the caller already validated the host, so never follow a - # redirect off the allowlisted host (would re-open the #1411 SSRF). - let client = newAsyncHttpClient(maxRedirects = 0) + let client = newAsyncHttpClient() try: result = await client.getContent(url) except: discard finally: client.close() @@ -33,79 +30,44 @@ template respond*(req: asynchttpserver.Request; headers) = proc proxyMedia*(req: jester.Request; url: string): Future[HttpCode] {.async.} = result = Http200 - let request = req.getNativeReq() - var fetchUrl = url + let + request = req.getNativeReq() + client = newAsyncHttpClient() - for attempt in 0 .. 2: - let client = newAsyncHttpClient(maxRedirects = 0) - var shouldRetry = false - try: - let resFut = client.get(fetchUrl) - let completed = await withTimeout(resFut, 5000) - if not completed: - if attempt < 2: - echo "[media] Retry $1/2, timeout after 5s, url: $2" % [$(attempt + 1), fetchUrl] - shouldRetry = true - else: - echo "[media] Proxying timeout after 5s, url: $1" % [fetchUrl] - return Http504 + try: + let res = await client.get(url) + if res.status != "200 OK": + return Http404 + + let hashed = $hash(url) + if request.headers.getOrDefault("If-None-Match") == hashed: + return Http304 + + let contentLength = + if res.headers.hasKey("content-length"): + res.headers["content-length", 0] else: - let res = resFut.read() - if res.status != "200 OK": - if res.status == "404 Not Found": - return Http404 - if res.status.startsWith("30") and res.headers.hasKey("location"): - let location = res.headers["location", 0] - if isTwitterUrl(location): - fetchUrl = location - shouldRetry = true - continue - else: - return Http403 - if attempt < 2: - echo "[media] Retry $1/2, status: $2, url: $3" % [$(attempt + 1), res.status, fetchUrl] - shouldRetry = true - else: - echo "[media] Proxying failed, status: $1, url: $2" % [res.status, fetchUrl] - return Http404 - else: - let hashed = $hash(url) - if request.headers.getOrDefault("If-None-Match") == hashed: - return Http304 + "" - let contentLength = - if res.headers.hasKey("content-length"): - res.headers["content-length", 0] - else: - "" + let headers = newHttpHeaders({ + "Content-Type": res.headers["content-type", 0], + "Content-Length": contentLength, + "Cache-Control": maxAge, + "ETag": hashed + }) - let headers = newHttpHeaders({ - "content-type": res.headers["content-type", 0], - "content-length": contentLength, - "cache-control": maxAge, - "etag": hashed - }) + respond(request, headers) - respond(request, headers) - - var (hasValue, data) = (true, "") - while hasValue: - (hasValue, data) = await res.bodyStream.read() - if hasValue: - await request.client.send(data) - data.setLen 0 - return Http200 - except CatchableError: - if attempt < 2: - echo "[media] Retry $1/2, error: $2, url: $3" % [$(attempt + 1), getCurrentExceptionMsg(), fetchUrl] - shouldRetry = true - else: - echo "[media] Proxying exception, error: $1, url: $2" % [getCurrentExceptionMsg(), fetchUrl] - result = Http404 - finally: - client.close() - if not shouldRetry: - break + var (hasValue, data) = (true, "") + while hasValue: + (hasValue, data) = await res.bodyStream.read() + if hasValue: + await request.client.send(data) + data.setLen 0 + except HttpRequestError, ProtocolError, OSError: + result = Http404 + finally: + client.close() template check*(code): untyped = if code != Http200: @@ -121,33 +83,17 @@ proc decoded*(req: jester.Request; index: int): string = if based: decode(encoded) else: decodeUrl(encoded) -proc normalizeImgUrl*(url: var string) = - if not url.startsWith("http"): - if "twimg.com" notin url: - url.insert(twimg) - url.insert(https) - proc createMediaRouter*(cfg: Config) = router media: get "/pic/?": resp Http404 - get re"^\/pic\/orig\/(enc)?\/?(.+)": - var url = decoded(request, 1) - cond "/amplify_video/" notin url - normalizeImgUrl(url) - url.add("?name=orig") - - let uri = parseUri(url) - cond isTwitterUrl(uri) == true - - let code = await proxyMedia(request, url) - check code - get re"^\/pic\/(enc)?\/?(.+)": var url = decoded(request, 1) - cond "/amplify_video/" notin url - normalizeImgUrl(url) + if "twimg.com" notin url: + url.insert(twimg) + if not url.startsWith(https): + url.insert(https) let uri = parseUri(url) cond isTwitterUrl(uri) == true @@ -157,12 +103,12 @@ proc createMediaRouter*(cfg: Config) = get re"^\/video\/(enc)?\/?(.+)\/(.+)$": let url = decoded(request, 2) - cond isTwitterUrl(url) + cond "http" in url if getHmac(url) != request.matches[1]: - resp Http403, showError("Failed to verify signature", cfg) + resp showError("Failed to verify signature", cfg) - if ".mp4" in url or ".ts" in url or ".m4s" in url or ".aac" in url: + if ".mp4" in url or ".ts" in url or ".m4s" in url: let code = await proxyMedia(request, url) check code @@ -176,6 +122,6 @@ proc createMediaRouter*(cfg: Config) = if ".m3u8" in url: let vid = await safeFetch(url) - content = proxifyVideo(vid, requestPrefs().proxyVideos, url) + content = proxifyVideo(vid, cookiePref(proxyVideos)) resp content, m3u8Mime diff --git a/src/routes/preferences.nim b/src/routes/preferences.nim index 7f04de2..b8af03d 100644 --- a/src/routes/preferences.nim +++ b/src/routes/preferences.nim @@ -19,10 +19,8 @@ proc createPrefRouter*(cfg: Config) = router preferences: get "/settings": let - prefs = requestPrefs() - prefsCode = encodePrefs(prefs) - prefsUrl = getUrlPrefix(cfg) & "/?prefs=" & prefsCode - html = renderPreferences(prefs, refPath(), findThemes(cfg.staticDir), prefsUrl) + prefs = cookiePrefs() + html = renderPreferences(prefs, refPath(), findThemes(cfg.staticDir)) resp renderMain(html, request, cfg, prefs, "Preferences") get "/settings/@i?": @@ -40,6 +38,3 @@ proc createPrefRouter*(cfg: Config) = savePref("hlsPlayback", "on", request) redirect(refPath()) - post "/enablemp4": - savePref("mp4Playback", "on", request) - redirect(refPath()) diff --git a/src/routes/resolver.nim b/src/routes/resolver.nim index 5f074a5..1baf873 100644 --- a/src/routes/resolver.nim +++ b/src/routes/resolver.nim @@ -18,8 +18,8 @@ proc createResolverRouter*(cfg: Config) = router resolver: get "/cards/@card/@id": let url = "https://cards.twitter.com/cards/$1/$2" % [@"card", @"id"] - respResolved(await resolve(url, requestPrefs()), "card") + respResolved(await resolve(url, cookiePrefs()), "card") get "/t.co/@url": let url = "https://t.co/" & @"url" - respResolved(await resolve(url, requestPrefs()), "t.co") + respResolved(await resolve(url, cookiePrefs()), "t.co") diff --git a/src/routes/router_utils.nim b/src/routes/router_utils.nim index 612a96b..a071a0d 100644 --- a/src/routes/router_utils.nim +++ b/src/routes/router_utils.nim @@ -4,19 +4,26 @@ from jester import Request, cookies import ../views/general import ".."/[utils, prefs, types] -export utils, prefs, types, uri, json +export utils, prefs, types, uri template savePref*(pref, value: string; req: Request; expire=false) = if not expire or pref in cookies(req): - let sameSite = if cfg.useHttps: None else: Lax setCookie(pref, value, daysForward(when expire: -10 else: 360), - httpOnly=true, secure=cfg.useHttps, sameSite=sameSite, path="/") + httpOnly=true, secure=cfg.useHttps, sameSite=None) -template requestPrefs*(): untyped {.dirty.} = - getPrefs(cookies(request), params(request)) +template cookiePrefs*(): untyped {.dirty.} = + getPrefs(cookies(request)) + +template cookiePref*(pref): untyped {.dirty.} = + getPref(cookies(request), pref) + +template themePrefs*(): Prefs = + var res = defaultPrefs + res.theme = cookiePref(theme) + res template showError*(error: string; cfg: Config): string = - renderMain(renderError(error), request, cfg, requestPrefs(), "Error") + renderMain(renderError(error), request, cfg, themePrefs(), "Error") template getPath*(): untyped {.dirty.} = $(parseUri(request.path) ? filterParams(request.params)) @@ -36,28 +43,5 @@ template getCursor*(req: Request): string = proc getNames*(name: string): seq[string] = name.strip(chars={'/'}).split(",").filterIt(it.len > 0) -template applyUrlPrefs*() {.dirty.} = - if @"prefs".len > 0: - var prefParams = initTable[string, string]() - for pair in @"prefs".split(','): - let kv = pair.split('=', maxsplit=1) - if kv.len == 2: - prefParams[kv[0]] = kv[1] - elif kv.len == 1 and kv[0].len > 0: - prefParams[kv[0]] = "" - genApplyPrefs(prefParams, request) - - # Rebuild URL without prefs param - var params: seq[(string, string)] - for k, v in request.params: - if k != "prefs": - params.add (k, v) - - if params.len > 0: - let cleanUrl = request.getNativeReq.url ? params - redirect($cleanUrl) - else: - redirect(request.path) - template respJson*(node: JsonNode) = resp $node, "application/json" diff --git a/src/routes/rss.nim b/src/routes/rss.nim index 038096c..40aa6a7 100644 --- a/src/routes/rss.nim +++ b/src/routes/rss.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, tables, times, hashes, uri +import asyncdispatch, strutils, tables, times, hashes, uri import jester @@ -10,12 +10,7 @@ include "../views/rss.nimf" export times, hashes -proc redisKey*(page, name, cursor: string): string = - result = page & ":" & name - if cursor.len > 0: - result &= ":" & cursor - -proc timelineRss*(req: Request; cfg: Config; query: Query; prefs: Prefs): Future[Rss] {.async.} = +proc timelineRss*(req: Request; cfg: Config; query: Query): Future[Rss] {.async.} = var profile: Profile let name = req.params.getOrDefault("name") @@ -23,30 +18,32 @@ proc timelineRss*(req: Request; cfg: Config; query: Query; prefs: Prefs): Future names = getNames(name) if names.len == 1: - profile = await fetchProfile(after, query, skipRail=true) + profile = await fetchProfile(after, query, skipRail=true, skipPinned=true) else: var q = query q.fromUser = names - profile.tweets = await getGraphTweetSearch(q, after) - # this is kinda dumb - profile.user = User( - username: name, - fullname: names.join(" | "), - userpic: "https://abs.twimg.com/sticky/default_profile_images/default_profile.png" + profile = Profile( + tweets: await getSearch[Tweet](q, after), + # this is kinda dumb + user: User( + username: name, + fullname: names.join(" | "), + userpic: "https://abs.twimg.com/sticky/default_profile_images/default_profile.png" + ) ) if profile.user.suspended: return Rss(feed: profile.user.username, cursor: "suspended") if profile.user.fullname.len > 0: - let rss = renderTimelineRss(profile, cfg, prefs, multi=(names.len > 1)) + let rss = renderTimelineRss(profile, cfg, multi=(names.len > 1)) return Rss(feed: rss, cursor: profile.tweets.bottom) template respRss*(rss, page) = if rss.cursor.len == 0: let info = case page - of "User": " \"" & @"name" & "\" " - of "List": " \"" & @"id" & "\" " + of "User": " \"$1\" " % @"name" + of "List": " $1 " % @"id" else: " " resp Http404, showError(page & info & "not found", cfg) @@ -60,81 +57,75 @@ template respRss*(rss, page) = proc createRssRouter*(cfg: Config) = router rss: get "/search/rss": - if not cfg.enableRSSSearch: - resp Http403, showError("RSS feed is disabled", cfg) + cond cfg.enableRss if @"q".len > 200: resp Http400, showError("Search input too long.", cfg) - let - prefs = requestPrefs() - query = initQuery(params(request)) - if query.kind notin {QueryKind.tweets, QueryKind.top, QueryKind.media}: + let query = initQuery(params(request)) + if query.kind != tweets: resp Http400, showError("Only Tweet searches are allowed for RSS feeds.", cfg) let cursor = getCursor() - key = redisKey("search", $hash(genQueryUrl(query)), cursor) + key = "search:" & $hash(genQueryUrl(query)) & ":" & cursor var rss = await getCachedRss(key) if rss.cursor.len > 0: respRss(rss, "Search") - let tweets = await getGraphTweetSearch(query, cursor) + let tweets = await getSearch[Tweet](query, cursor) rss.cursor = tweets.bottom - rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg, prefs) + rss.feed = renderSearchRss(tweets.content, query.text, genQueryUrl(query), cfg) await cacheRss(key, rss) respRss(rss, "Search") get "/@name/rss": + cond cfg.enableRss cond '.' notin @"name" - if not cfg.enableRSSUserTweets: - resp Http403, showError("RSS feed is disabled", cfg) let - prefs = requestPrefs() + cursor = getCursor() name = @"name" - key = redisKey("twitter", name, getCursor()) + key = "twitter:" & name & ":" & cursor var rss = await getCachedRss(key) if rss.cursor.len > 0: respRss(rss, "User") - rss = await timelineRss(request, cfg, Query(fromUser: @[name]), prefs) + rss = await timelineRss(request, cfg, Query(fromUser: @[name])) await cacheRss(key, rss) respRss(rss, "User") get "/@name/@tab/rss": + cond cfg.enableRss cond '.' notin @"name" - cond @"tab" in ["with_replies", "media", "search", "articles"] - # articles can't be approximated by search, so multi-user is unsupported - cond not (@"tab" == "articles" and ',' in @"name") - if not cfg.tabRssEnabled(@"tab"): - resp Http403, showError("RSS feed is disabled", cfg) - let - prefs = requestPrefs() - name = @"name" - tab = @"tab" - query = request.getQuery(tab, name, prefs) + cond @"tab" in ["with_replies", "media", "search"] + let name = @"name" + let query = + case @"tab" + of "with_replies": getReplyQuery(name) + of "media": getMediaQuery(name) + of "search": initQuery(params(request), name=name) + else: Query(fromUser: @[name]) - let searchKey = if tab != "search": "" - else: ":" & $hash(genQueryUrl(query)) - - let key = redisKey(tab, name & searchKey, getCursor()) + var key = @"tab" & ":" & @"name" & ":" + if @"tab" == "search": + key &= $hash(genQueryUrl(query)) & ":" + key &= getCursor() var rss = await getCachedRss(key) if rss.cursor.len > 0: respRss(rss, "User") - rss = await timelineRss(request, cfg, query, prefs) + rss = await timelineRss(request, cfg, query) await cacheRss(key, rss) respRss(rss, "User") get "/@name/lists/@slug/rss": + cond cfg.enableRss cond @"name" != "i" - if not cfg.enableRSSList: - resp Http403, showError("RSS feed is disabled", cfg) let slug = decodeUrl(@"slug") list = await getCachedList(@"name", slug) @@ -150,23 +141,22 @@ proc createRssRouter*(cfg: Config) = redirect(url) get "/i/lists/@id/rss": - if not cfg.enableRSSList: - resp Http403, showError("RSS feed is disabled", cfg) + cond cfg.enableRss let - prefs = requestPrefs() - id = @"id" cursor = getCursor() - key = redisKey("lists", id, cursor) + key = + if cursor.len == 0: "lists:" & @"id" + else: "lists:" & @"id" & ":" & cursor var rss = await getCachedRss(key) if rss.cursor.len > 0: respRss(rss, "List") let - list = await getCachedList(id=id) - timeline = await getGraphListTweets(list.id, cursor) + list = await getCachedList(id=(@"id")) + timeline = await getListTimeline(list.id, cursor) rss.cursor = timeline.bottom - rss.feed = renderListRss(timeline.content, list, cfg, prefs) + rss.feed = renderListRss(timeline.content, list, cfg) await cacheRss(key, rss) respRss(rss, "List") diff --git a/src/routes/search.nim b/src/routes/search.nim index 7c7fd14..3fc44a9 100644 --- a/src/routes/search.nim +++ b/src/routes/search.nim @@ -14,55 +14,32 @@ export search proc createSearchRouter*(cfg: Config) = router search: get "/search/?": - let q = @"q" - if q.len > 500: + if @"q".len > 500: resp Http400, showError("Search input too long.", cfg) let - prefs = requestPrefs() - title = "Search" & (if q.len > 0: " (" & q & ")" else: "") - - var query = initQuery(params(request)) - # x.com URL compat: f=user and f=list map to our kind names - # (f=live already falls back to tweets/Latest; f=media matches natively) - if @"f" == "user": - query.kind = users - elif @"f" == "list": - query.kind = lists - - # media searches support view modes, defaulting like /user/media - if query.kind == QueryKind.media and - query.view notin ["timeline", "grid", "gallery"]: - query.view = prefs.mediaView.toLowerAscii + prefs = cookiePrefs() + query = initQuery(params(request)) case query.kind of users: - if "," in q: - redirect("/" & q) - var users: Result[User] - try: - users = await getGraphUserSearch(query, getCursor()) - except InternalError: - users = Result[User](beginning: true, query: query) - resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs, title) - of tweets, top, QueryKind.media: + if "," in @"q": + redirect("/" & @"q") + let users = await getSearch[User](query, getCursor()) + resp renderMain(renderUserSearch(users, prefs), request, cfg, prefs) + of tweets: let - tweets = await getGraphTweetSearch(query, getCursor()) - rss = if cfg.enableRSSSearch: "/search/rss?" & genQueryUrl(query) else: "" + tweets = await getSearch[Tweet](query, getCursor()) + rss = "/search/rss?" & genQueryUrl(query) resp renderMain(renderTweetSearch(tweets, prefs, getPath()), - request, cfg, prefs, title, rss=rss) - of lists: - let listResults = await getGraphListSearch(query, getCursor()) - resp renderMain(renderListSearch(listResults, prefs, getPath()), - request, cfg, prefs, title) + request, cfg, prefs, rss=rss) else: resp Http404, showError("Invalid search", cfg) get "/hashtag/@hash": - redirect("/search?f=tweets&q=" & encodeUrl("#" & @"hash")) + redirect("/search?q=" & encodeUrl("#" & @"hash")) get "/opensearch": - let - url = getUrlPrefix(cfg) & "/search?f=tweets&q=" - headers = {"Content-Type": "application/opensearchdescription+xml"} - resp Http200, headers, generateOpenSearchXML(cfg.title, cfg.hostname, url) + let url = getUrlPrefix(cfg) & "/search?q=" + resp Http200, {"Content-Type": "application/opensearchdescription+xml"}, + generateOpenSearchXML(cfg.title, cfg.hostname, url) diff --git a/src/routes/space.nim b/src/routes/space.nim deleted file mode 100644 index bd956ea..0000000 --- a/src/routes/space.nim +++ /dev/null @@ -1,36 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import asyncdispatch, strutils -import jester - -import router_utils -import ".."/[types, formatters, redis_cache] -import ../views/[general, space] -import media - -export space - -proc createSpaceRouter*(cfg: Config) = - router spaceRoute: - get "/i/spaces/@id": - cond @"id".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9'}) - let sp = await getCachedAudioSpace(@"id") - - if sp.id.len == 0: - resp Http404, showError("Space not found", cfg) - - let prefs = requestPrefs() - resp renderMain(renderSpace(sp, prefs, request.path), request, cfg, prefs, - sp.title, ogTitle=sp.title) - - get "/i/spaces/@id/stream": - cond @"id".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9'}) - let sp = await getCachedAudioSpace(@"id") - - if sp.m3u8Url.len == 0: - resp Http404 - - let manifest = await safeFetch(sp.m3u8Url) - if manifest.len == 0: - resp Http502 - - resp proxifyVideo(manifest, requestPrefs().proxyVideos, sp.m3u8Url), m3u8Mime diff --git a/src/routes/status.nim b/src/routes/status.nim index 32a4447..0303152 100644 --- a/src/routes/status.nim +++ b/src/routes/status.nim @@ -16,23 +16,19 @@ proc createStatusRouter*(cfg: Config) = router status: get "/@name/status/@id/?": cond '.' notin @"name" - let id = @"id" - - if id.len > 19 or id.any(c => not c.isDigit): - resp Http404, showError("Invalid tweet ID", cfg) - - let - prefs = requestPrefs() - sort = parseEnum[RankingMode](@"sort".toLowerAscii.capitalizeAscii, Relevance) + cond not @"id".any(c => not c.isDigit) + let prefs = cookiePrefs() # used for the infinite scroll feature if @"scroll".len > 0: - let replies = await getReplies(id, getCursor(), sort) + let replies = await getReplies(@"id", getCursor()) if replies.content.len == 0: - resp Http204 - resp $renderReplies(replies, prefs, getPath(), sort=sort) + resp Http404, "" + resp $renderReplies(replies, prefs, getPath()) - let conv = await getTweet(id, getCursor(), sort) + let conv = await getTweet(@"id", getCursor()) + if conv == nil: + echo "nil conv" if conv == nil or conv.tweet == nil or conv.tweet.id == 0: var error = "Tweet not found" @@ -46,19 +42,15 @@ proc createStatusRouter*(cfg: Config) = desc = conv.tweet.text var - images = conv.tweet.getPhotos.mapIt(it.url) + images = conv.tweet.photos video = "" - let - firstMediaKind = if conv.tweet.media.len > 0: conv.tweet.media[0].kind - else: photoMedia - - if firstMediaKind == videoMedia: - images = @[conv.tweet.media[0].getThumb] + if conv.tweet.video.isSome(): + images = @[get(conv.tweet.video).thumb] video = getVideoEmbed(cfg, conv.tweet.id) - elif firstMediaKind == gifMedia: - images = @[conv.tweet.media[0].getThumb] - video = getPicUrl(conv.tweet.media[0].gif.url) + elif conv.tweet.gif.isSome(): + images = @[get(conv.tweet.gif).thumb] + video = getPicUrl(get(conv.tweet.gif).url) elif conv.tweet.card.isSome(): let card = conv.tweet.card.get() if card.image.len > 0: @@ -66,33 +58,9 @@ proc createStatusRouter*(cfg: Config) = elif card.video.isSome(): images = @[card.video.get().thumb] - let - tweetUrl = getUrlPrefix(cfg) & "/" & conv.tweet.user.username & "/status/" & $conv.tweet.id - oembedUrl = getUrlPrefix(cfg) & "/api/oembed?url=" & encodeUrl(tweetUrl) - - let html = renderConversation(conv, prefs, getPath() & "#m", sort) + let html = renderConversation(conv, prefs, getPath() & "#m") resp renderMain(html, request, cfg, prefs, title, desc, ogTitle, - images=images, video=video, oembed=oembedUrl) - - get "/@name/status/@id/history/?": - cond '.' notin @"name" - let id = @"id" - - if id.len > 19 or id.any(c => not c.isDigit): - resp Http404, showError("Invalid tweet ID", cfg) - - let edits = await getGraphEditHistory(id) - if edits.latest == nil or edits.latest.id == 0: - resp Http404, showError("Tweet history not found", cfg) - - let - prefs = requestPrefs() - title = "History for " & pageTitle(edits.latest) - ogTitle = "Edit History for " & pageTitle(edits.latest.user) - desc = edits.latest.text - - let html = renderEditHistory(edits, prefs, getPath()) - resp renderMain(html, request, cfg, prefs, title, desc, ogTitle) + images=images, video=video) get "/@name/@s/@id/@m/?@i?": cond @"s" in ["status", "statuses"] @@ -104,6 +72,3 @@ proc createStatusRouter*(cfg: Config) = get "/i/web/status/@id": redirect("/i/status/" & @"id") - - get "/@name/thread/@id/?": - redirect("/$1/status/$2" % [@"name", @"id"]) diff --git a/src/routes/timeline.nim b/src/routes/timeline.nim index c7949a2..a0a6e21 100644 --- a/src/routes/timeline.nim +++ b/src/routes/timeline.nim @@ -4,39 +4,20 @@ import jester, karax/vdom import router_utils import ".."/[types, redis_cache, formatters, query, api] -import ../views/[general, profile, timeline, status, search, about_account] +import ../views/[general, profile, timeline, status, search] export vdom export uri, sequtils export router_utils export redis_cache, formatters, query, api -export profile, timeline, status, about_account +export profile, timeline, status -proc tabRssEnabled*(cfg: Config; tab: string): bool = +proc getQuery*(request: Request; tab, name: string): Query = case tab - of "": cfg.enableRSSUserTweets - of "with_replies": cfg.enableRSSUserReplies - of "media": cfg.enableRSSUserMedia - of "articles": cfg.enableRSSUserArticles - of "search": cfg.enableRSSSearch - else: false - -proc getQuery*(request: Request; tab, name: string; prefs: Prefs): Query = - let view = request.params.getOrDefault("view") - case tab - of "with_replies": - result = getReplyQuery(name) - of "articles": - result = getArticlesQuery(name) - of "media": - result = getMediaQuery(name) - result.view = - if view in ["timeline", "grid", "gallery"]: view - else: prefs.mediaView.toLowerAscii - of "search": - result = initQuery(params(request), name=name) - else: - result = Query(fromUser: @[name]) + of "with_replies": getReplyQuery(name) + of "media": getMediaQuery(name) + of "search": initQuery(params(request), name=name) + else: Query(fromUser: @[name]) template skipIf[T](cond: bool; default; body: Future[T]): Future[T] = if cond: @@ -46,7 +27,8 @@ template skipIf[T](cond: bool; default; body: Future[T]): Future[T] = else: body -proc fetchProfile*(after: string; query: Query; skipRail=false): Future[Profile] {.async.} = +proc fetchProfile*(after: string; query: Query; skipRail=false; + skipPinned=false): Future[Profile] {.async.} = let name = query.fromUser[0] userId = await getUserId(name) @@ -63,24 +45,36 @@ proc fetchProfile*(after: string; query: Query; skipRail=false): Future[Profile] after.setLen 0 let + timeline = + case query.kind + of posts: getTimeline(userId, after) + of replies: getTimeline(userId, after, replies=true) + of media: getMediaTimeline(userId, after) + else: getSearch[Tweet](query, after) + rail = - skipIf(skipRail or query.kind == QueryKind.media, @[]): - getCachedPhotoRail(userId) + skipIf(skipRail or query.kind == media, @[]): + getCachedPhotoRail(name) - user = getCachedUser(name) - info = getCachedAccountInfo(name, fetch=false) + user = await getCachedUser(name) - result = - case query.kind - of posts: await getGraphUserTweets(userId, TimelineKind.tweets, after) - of replies: await getGraphUserTweets(userId, TimelineKind.replies, after) - of media: await getGraphUserTweets(userId, TimelineKind.media, after) - of QueryKind.articles: await getGraphUserTweets(userId, TimelineKind.articles, after) - else: Profile(tweets: await getGraphTweetSearch(query, after)) + var pinned: Option[Tweet] + if not skipPinned and user.pinnedTweet > 0 and + after.len == 0 and query.kind in {posts, replies}: + let tweet = await getCachedTweet(user.pinnedTweet) + if not tweet.isNil: + tweet.pinned = true + pinned = some tweet - result.user = await user - result.photoRail = await rail - result.accountInfo = await info + result = Profile( + user: user, + pinned: pinned, + tweets: await timeline, + photoRail: await rail + ) + + if result.user.protected or result.user.suspended: + return result.tweets.query = query @@ -88,11 +82,11 @@ proc showTimeline*(request: Request; query: Query; cfg: Config; prefs: Prefs; rss, after: string): Future[string] {.async.} = if query.fromUser.len != 1: let - timeline = await getGraphTweetSearch(query, after) + timeline = await getSearch[Tweet](query, after) html = renderTweetSearch(timeline, prefs, getPath()) return renderMain(html, request, cfg, prefs, "Multi", rss=rss) - var profile = await fetchProfile(after, query) + var profile = await fetchProfile(after, query, skipPinned=prefs.hidePins) template u: untyped = profile.user if u.suspended: @@ -127,81 +121,24 @@ proc createTimelineRouter*(cfg: Config) = get "/intent/user": respUserId() - get "/intent/follow/?": - let username = request.params.getOrDefault("screen_name") - if username.len == 0: - resp Http400, showError("Missing screen_name parameter", cfg) - redirect("/" & username) - - get "/@name/about/?": - cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_'}) - let - prefs = requestPrefs() - name = @"name" - info = await getCachedAccountInfo(name) - if info.suspended: - resp showError(getSuspended(name), cfg) - if info.username.len == 0: - resp Http404, showError("User \"" & name & "\" not found", cfg) - let aboutHtml = renderAboutAccount(info) - resp renderMain(aboutHtml, request, cfg, prefs, - "About @" & info.username) - - get "/@name/@kind/?": - cond @"kind" in ["followers", "following"] - cond '.' notin @"name" - cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_'}) - let - prefs = requestPrefs() - name = @"name" - userId = await getUserId(name) - - if userId.len == 0: - resp Http404, showError("User \"" & name & "\" not found", cfg) - if userId == "suspended": - resp showError(getSuspended(name), cfg) - - let - cursor = getCursor() - isFollowers = @"kind" == "followers" - user = await getCachedUser(name) - results = if isFollowers: await getGraphFollowers(userId, cursor) - else: await getGraphFollowing(userId, cursor) - - if user.protected: - resp renderMain(renderProtected(user.username), request, cfg, prefs, - "Protected account @" & name) - - let - tab = if isFollowers: "Followers" else: "Following" - title = (if isFollowers: "People following @" else: "People followed by @") & name - html = renderUserList(user, results, prefs, request.path, tab) - - resp renderMain(html, request, cfg, prefs, title, - images = @[user.getUserPic("_400x400")]) - get "/@name/?@tab?/?": cond '.' notin @"name" - cond @"name" notin ["pic", "gif", "video", "search", "settings", "login", "intent", "i"] - cond @"name".allCharsInSet({'a'..'z', 'A'..'Z', '0'..'9', '_', ','}) - cond @"tab" in ["with_replies", "media", "search", "articles", ""] - # articles can't be approximated by search, so multi-user is unsupported - cond not (@"tab" == "articles" and ',' in @"name") + cond @"name" notin ["pic", "gif", "video"] + cond @"tab" in ["with_replies", "media", "search", ""] let - prefs = requestPrefs() + prefs = cookiePrefs() after = getCursor() names = getNames(@"name") - var query = request.getQuery(@"tab", @"name", prefs) + var query = request.getQuery(@"tab", @"name") if names.len != 1: query.fromUser = names # used for the infinite scroll feature if @"scroll".len > 0: if query.fromUser.len != 1: - var timeline = await getGraphTweetSearch(query, after) - if timeline.content.len == 0: - resp Http204 + var timeline = await getSearch[Tweet](query, after) + if timeline.content.len == 0: resp Http404 timeline.beginning = true resp $renderTweetSearch(timeline, prefs, getPath()) else: @@ -211,9 +148,7 @@ proc createTimelineRouter*(cfg: Config) = resp $renderTimelineTweets(profile.tweets, prefs, getPath()) let rss = - if not cfg.tabRssEnabled(@"tab"): - "" - elif @"tab".len == 0: + if @"tab".len == 0: "/$1/rss" % @"name" elif @"tab" == "search": "/$1/search/rss?$2" % [@"name", genQueryUrl(query)] diff --git a/src/routes/unsupported.nim b/src/routes/unsupported.nim index 345dee7..0c085d4 100644 --- a/src/routes/unsupported.nim +++ b/src/routes/unsupported.nim @@ -10,14 +10,14 @@ export feature proc createUnsupportedRouter*(cfg: Config) = router unsupported: template feature {.dirty.} = - resp renderMain(renderFeature(), request, cfg, requestPrefs()) + resp renderMain(renderFeature(), request, cfg, themePrefs()) get "/about/feature": feature() get "/login/?@i?": feature() get "/@name/lists/?": feature() get "/intent/?@i?": - cond @"i" notin ["user", "follow"] + cond @"i" notin ["user"] feature() get "/i/@i?/?@j?": diff --git a/src/sass/_article.scss b/src/sass/_article.scss deleted file mode 100644 index 9488658..0000000 --- a/src/sass/_article.scss +++ /dev/null @@ -1,278 +0,0 @@ -.article-page { - max-width: 700px; - margin: 0 auto 20px; - background-color: var(--bg_panel); - - > .top-ref { - padding-top: 20px; - } - - .article-cover { - width: 100%; - display: block; - } - - .article-body { - padding: 20px; - - > :last-child { - margin-bottom: 0; - } - - .article-title { - display: block; - font-size: 2rem; - line-height: 1.3; - margin: 0 0 10px; - color: var(--fg_color); - } - - .article-author { - margin-bottom: 12px; - padding-bottom: 10px; - border-bottom: 1px solid var(--border_grey); - font-size: 14px; - - .article-author-row { - display: flex; - align-items: center; - gap: 8px; - } - - .article-avatar { - display: flex; - } - - .avatar { - width: 40px; - height: 40px; - } - - .article-author-name { - display: flex; - align-items: center; - margin-bottom: 2px; - - .fullname { - font-size: 15px; - } - - .verified-icon { - margin-left: 2px; - } - } - - .article-author-meta { - display: flex; - align-items: center; - } - - .fullname { - font-weight: 700; - color: var(--fg_color); - max-width: unset; - text-overflow: unset; - overflow: visible; - white-space: normal; - } - - .username, - .article-date-sep, - .article-date { - color: var(--fg_dark); - } - - .username { - margin-left: 0; - } - - .article-date-sep { - margin: 0 4px; - } - - .article-date { - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - - .tweet-stats { - margin-top: 6px; - - .tweet-stat { - padding-top: 0; - } - } - } - - > h1 { - display: block; - font-size: 1.8rem; - margin: 25px 0 15px; - } - - > h2 { - font-size: 1.4rem; - font-weight: bold; - margin: 20px 0 12px; - } - - > h3 { - font-size: 1.2rem; - font-weight: bold; - margin: 18px 0 10px; - } - - > p { - font-size: 16px; - line-height: 1.7; - margin: 16px 0; - word-wrap: break-word; - } - - .blockquote-attribution { - display: block; - margin-top: 0.5em; - } - - > blockquote { - border-left: 3px solid var(--accent); - padding-left: 16px; - margin: 16px 0; - color: var(--fg_faded); - font-size: 16px; - line-height: 1.7; - } - - > pre { - background-color: var(--bg_elements); - padding: 12px 16px; - border-radius: 6px; - overflow-x: auto; - margin: 16px 0; - - code { - font-family: monospace; - font-size: 14px; - color: var(--fg_color); - } - } - - code { - background-color: var(--bg_elements); - padding: 2px 5px; - border-radius: 3px; - font-family: monospace; - font-size: 0.9em; - } - - > ul, - > ol { - margin: 16px 0; - padding-left: 2em; - - li { - font-size: 16px; - line-height: 1.7; - margin: 6px 0; - } - } - - .article-media { - text-align: center; - margin: 20px 0; - - img, - video { - max-width: 100%; - border-radius: 12px; - } - - .article-media-caption { - color: var(--fg_faded); - font-size: 0.875rem; - margin-top: 6px; - } - } - - > a, - > p a, - > h1 a, - > h2 a, - > h3 a, - > blockquote a, - > ul a, - > ol a { - color: var(--accent); - text-decoration: none; - - &:hover { - text-decoration: underline; - } - } - - .article-divider { - border: none; - border-top: 1px solid var(--border_grey); - margin: 30px 0; - } - - .timeline-item { - margin: 20px 0; - border: 1px solid var(--border_grey); - border-radius: 12px; - overflow: hidden; - } - } -} - -.conversation .article-page { - max-width: 100%; - margin-bottom: 0; -} - -.article-card { - .card-image-container { - position: relative; - } - - .card-image img { - height: auto; - } - - .article-card-badge { - position: absolute; - bottom: 8px; - left: 8px; - background: rgba(0, 0, 0, 0.75); - color: #fff; - font-size: 13px; - font-weight: 700; - padding: 2px 8px; - border-radius: 4px; - } -} - -.quote .article-card { - margin: 0; - - .card-container { - border: none; - border-radius: 0; - border-top: solid 1px var(--dark_grey); - } -} - -@media (max-width: 700px) { - .article-page { - .article-body { - padding: 12px 15px 25px; - - .article-title { - font-size: 1.6rem; - } - } - } -} diff --git a/src/sass/_broadcast.scss b/src/sass/_broadcast.scss deleted file mode 100644 index dd93606..0000000 --- a/src/sass/_broadcast.scss +++ /dev/null @@ -1,75 +0,0 @@ -.broadcast-page { - max-width: 800px; - width: 100%; - margin: 20px auto 0; -} - -.broadcast-panel { - background-color: var(--bg_panel); - border: 1px solid var(--border_grey); - border-radius: 8px; - overflow: hidden; -} - -.broadcast-player { - position: relative; - background: black; - - video, - img { - display: block; - width: 100%; - } -} - -.broadcast-info { - padding: 14px 16px; -} - -.broadcast-title { - font-size: 18px; - font-weight: bold; - margin: 0 0 12px; -} - -.broadcast-user-row { - display: flex; - align-items: center; - justify-content: space-between; -} - -.broadcast-user { - display: flex; - align-items: center; - gap: 10px; - color: var(--fg_color); - - img { - width: 40px; - height: 40px; - border-radius: 50%; - } -} - -.broadcast-username { - color: var(--fg_dark); -} - -.broadcast-meta { - color: var(--fg_faded); - font-size: 14px; - display: flex; - flex-direction: column; - align-items: flex-end; - flex-shrink: 0; - line-height: 1.5em; -} - -.broadcast-live { - background: #e0245e; - color: white; - padding: 1px 6px; - border-radius: 3px; - font-weight: bold; - font-size: 12px; -} diff --git a/src/sass/_space.scss b/src/sass/_space.scss deleted file mode 100644 index 5fe2e7e..0000000 --- a/src/sass/_space.scss +++ /dev/null @@ -1,149 +0,0 @@ -.space-page { - max-width: 800px; - width: 100%; - margin: 20px auto 0; -} - -.space-panel { - background-color: var(--bg_panel); - border: 1px solid var(--border_grey); - border-radius: 8px; - overflow: hidden; -} - -.space-player { - position: relative; - background: linear-gradient(135deg, #7b2a8c 0%, #9b3ab1 100%); - min-height: 140px; - display: flex; - align-items: center; - justify-content: center; - - audio { - width: 100%; - padding: 15px; - box-sizing: border-box; - - &:not([controls]) { - display: none; - } - } - - .video-overlay { - background-color: transparent; - } -} - -.space-live { - background: #e0245e; - color: white; - padding: 3px 8px; - border-radius: 4px; - font-weight: bold; - font-size: 12px; - text-transform: uppercase; - position: absolute; - top: 8px; - right: 8px; -} - -.space-info { - padding: 16px; -} - -.space-header { - display: flex; - justify-content: space-between; - align-items: flex-start; - gap: 12px; - margin-bottom: 16px; -} - -.space-title { - font-size: 18px; - font-weight: bold; - margin: 0; - line-height: 1.3; - flex: 1; -} - -.space-meta { - display: flex; - flex-direction: column; - align-items: flex-end; - gap: 4px; - flex-shrink: 0; - font-size: 14px; - color: var(--fg_faded); -} - -.listener-count { - color: var(--fg_color); -} - -.space-state { - color: var(--fg_dark); -} - -.space-participants { - border-top: 1px solid var(--border_grey); - padding-top: 12px; -} - -.space-participant { - margin-bottom: 10px; - - a { - display: flex; - align-items: center; - gap: 10px; - color: var(--fg_color); - padding: 6px 0; - } - - img { - width: 40px; - height: 40px; - border-radius: 50%; - flex-shrink: 0; - } -} - -.participant-info { - min-width: 0; -} - -.participant-name { - display: flex; - align-items: center; - gap: 4px; - - strong { - white-space: nowrap; - overflow: hidden; - text-overflow: ellipsis; - } - - .verified-icon { - margin-bottom: 0; - position: relative; - top: -2px; - } -} - -.host-badge { - background: var(--accent); - color: white; - padding: 2px 7px; - border-radius: 3px; - font-size: 11px; - font-weight: 600; - line-height: 1; - position: relative; - top: 1px; -} - -.participant-username { - color: var(--fg_dark); - font-size: 13px; -} diff --git a/src/sass/general.scss b/src/sass/general.scss index e6247d6..9feb3d3 100644 --- a/src/sass/general.scss +++ b/src/sass/general.scss @@ -1,40 +1,39 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; .panel-container { - margin: auto; - font-size: 130%; + margin: auto; + font-size: 130%; } .error-panel { - @include center-panel(var(--error_red)); - text-align: center; + @include center-panel(var(--error_red)); + text-align: center; } .search-bar > form { - @include center-panel(var(--darkest_grey)); + @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; - } + button { + background: var(--bg_elements); + color: var(--fg_color); + border: 0; + border-radius: 3px; + cursor: pointer; + font-weight: bold; + width: 30px; + height: 30px; + } - 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; - } + 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/src/sass/include/_mixins.css b/src/sass/include/_mixins.css index 5fde51a..94e11ee 100644 --- a/src/sass/include/_mixins.css +++ b/src/sass/include/_mixins.css @@ -66,7 +66,18 @@ } #search-panel-toggle:checked ~ .search-panel { - max-height: 380px !important; + @if $rows == 6 { + max-height: 200px !important; + } + @if $rows == 5 { + max-height: 300px !important; + } + @if $rows == 4 { + max-height: 300px !important; + } + @if $rows == 3 { + max-height: 365px !important; + } } } } diff --git a/src/sass/include/_variables.scss b/src/sass/include/_variables.scss index 127cccb..0f81235 100644 --- a/src/sass/include/_variables.scss +++ b/src/sass/include/_variables.scss @@ -1,43 +1,44 @@ // colors -$bg_color: #0f0f0f; -$fg_color: #f8f8f2; -$fg_faded: #f8f8f2cf; -$fg_dark: #ff6c60; -$fg_nav: #ff6c60; +$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; +$bg_overlays: #1F1F1F; +$bg_hover: #1A1A1A; $grey: #888889; $dark_grey: #404040; $darker_grey: #282828; $darkest_grey: #222222; -$border_grey: #3e3e35; +$border_grey: #3E3E35; -$accent: #ff6c60; -$accent_light: #ffaca0; -$accent_dark: #8a3731; -$accent_border: #ff6c6091; +$accent: #FF6C60; +$accent_light: #FFACA0; +$accent_dark: #8A3731; +$accent_border: #FF6C6091; -$play_button: #d8574d; -$play_button_hover: #ff6c60; +$play_button: #D8574D; +$play_button_hover: #FF6C60; -$more_replies_dots: #ad433b; -$error_red: #420a05; +$more_replies_dots: #AD433B; +$error_red: #420A05; -$verified_blue: #1da1f2; -$verified_business: #fac82b; -$verified_government: #c1b6a4; +$verified_blue: #1DA1F2; $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); +$shadow: rgba(0,0,0,.6); +$shadow_dark: rgba(0,0,0,.2); //fonts -$font_0: sans-serif; -$font_1: fontello; +$font_0: Helvetica Neue; +$font_1: Helvetica; +$font_2: Arial; +$font_3: sans-serif; +$font_4: fontello; diff --git a/src/sass/index.scss b/src/sass/index.scss index 404f7d5..9e2e347 100644 --- a/src/sass/index.scss +++ b/src/sass/index.scss @@ -1,220 +1,165 @@ -@import "_variables"; +@import '_variables'; -@import "tweet/_base"; -@import "profile/_base"; -@import "general"; -@import "navbar"; -@import "inputs"; -@import "timeline"; -@import "search"; -@import "broadcast"; -@import "space"; -@import "_article"; +@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}; + // 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}; + --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}; + --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}; + --accent: #{$accent}; + --accent_light: #{$accent_light}; + --accent_dark: #{$accent_dark}; + --accent_border: #{$accent_border}; - --play_button: #{$play_button}; - --play_button_hover: #{$play_button_hover}; + --play_button: #{$play_button}; + --play_button_hover: #{$play_button_hover}; - --more_replies_dots: #{$more_replies_dots}; - --error_red: #{$error_red}; + --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}; + --verified_blue: #{$verified_blue}; + --icon_text: #{$icon_text}; - --tab: #{$fg_color}; - --tab_selected: #{$accent}; + --tab: #{$fg_color}; + --tab_selected: #{$accent}; - --profile_stat: #{$fg_color}; + --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; + background-color: var(--bg_color); + color: var(--fg_color); + font-family: $font_0, $font_1, $font_2, $font_3; + font-size: 14px; + line-height: 1.3; + margin: 0; } * { - outline: unset; - margin: 0; - text-decoration: none; -} - -img { - dynamic-range-limit: standard; + outline: unset; + margin: 0; + text-decoration: none; } h1 { - display: inline; + display: inline; } -h2, -h3 { - font-weight: normal; +h2, h3 { + font-weight: normal; } p { - margin: 14px 0; + margin: 14px 0; } a { - color: var(--accent); + color: var(--accent); - &:hover { - text-decoration: underline; - } + &:hover { + text-decoration: underline; + } } fieldset { - border: 0; - padding: 0; - margin-top: -0.6em; + 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; + 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 { +.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; - } - - .bookmark-note { - margin: 0; - margin-bottom: 10px; - } } ul { - padding-left: 1.3em; + 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; + display: flex; + flex-wrap: wrap; + box-sizing: border-box; + padding-top: 50px; + margin: auto; + min-height: 100vh; } .icon-container { - display: inline; + 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; + 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; - } + ul { + margin-bottom: 14px; + } - p { - word-break: break-word; - } + p { + word-break: break-word; + } } .verified-icon { - display: inline-block; - position: relative; - width: 14px; - height: 14px; - margin-bottom: 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); - } - } + color: var(--icon_text); + background-color: var(--verified_blue); + border-radius: 50%; + flex-shrink: 0; + margin: 2px 0 3px 3px; + padding-top: 2px; + height: 12px; + width: 14px; + font-size: 8px; + display: inline-block; + text-align: center; + vertical-align: middle; } -@media (max-width: 600px) { - .preferences-container { - max-width: 95vw; - } +@media(max-width: 600px) { + .preferences-container { + max-width: 95vw; + } - .nav-item, - .nav-item .icon-container { - font-size: 16px; - } + .nav-item, .nav-item .icon-container { + font-size: 16px; + } } diff --git a/src/sass/inputs.scss b/src/sass/inputs.scss index 2b6016f..17c2a22 100644 --- a/src/sass/inputs.scss +++ b/src/sass/inputs.scss @@ -1,216 +1,185 @@ -@import "_variables"; -@import "_mixins"; +@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; + @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; + @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; +input[type="text"] { + height: 16px; } select { - height: 20px; - padding: 0 2px; - line-height: 1; + 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; + display: none; } input[type="date"]::-webkit-clear-button { - margin-left: 17px; - filter: grayscale(100%); - filter: hue-rotate(120deg); + margin-left: 17px; + filter: grayscale(100%); + filter: hue-rotate(120deg); } input::-webkit-calendar-picker-indicator { - opacity: 0; + 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; + background-color: var(--accent); + color: var(--fg_color); + outline: none; } .date-range { - .date-input { - display: inline-block; - position: relative; - } + .date-input { + display: inline-block; + position: relative; + } - .icon-container { - pointer-events: none; - position: absolute; - top: 2px; - right: 5px; - } + .icon-container { + pointer-events: none; + position: absolute; + top: 2px; + right: 5px; + } - .search-title { - margin: 0 2px; - } + .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; + color: var(--accent); + text-decoration: none; + background: none; + border: none; + float: none; + padding: unset; + padding-left: 4px; - &:hover { - color: var(--accent_light); - } + &: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; - } + 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; + display: block; + position: relative; + margin-bottom: 5px; cursor: pointer; - height: 0; - width: 0; + user-select: none; + padding-right: 22px; - &:checked ~ .checkbox:after { - display: block; + input { + position: absolute; + opacity: 0; + cursor: pointer; + height: 0; + width: 0; + + &:checked ~ .checkbox:after { + display: block; + } } - } - &:hover input ~ .checkbox { - border-color: var(--accent); - } + &:hover input ~ .checkbox { + border-color: var(--accent); + } - &:active input ~ .checkbox { - border-color: var(--accent_light); - } + &:active input ~ .checkbox { + border-color: var(--accent_light); + } - .checkbox:after { - left: 2px; - bottom: 0; - font-size: 13px; - font-family: $font_1; - content: "\e811"; - } + .checkbox:after { + left: 2px; + bottom: 0; + font-size: 13px; + font-family: $font_4; + content: '\e803'; + } } .pref-group { - display: inline; + display: inline; } .preferences { - button { - margin: 6px 0 3px 0; - } + button { + margin: 6px 0 3px 0; + } - label { - padding-right: 150px; - } + label { + padding-right: 150px; + } - select { - position: absolute; - top: 0; - right: 0; - display: block; - -moz-appearance: none; - -webkit-appearance: none; - appearance: none; - min-width: 100px; - } + 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; - } + input[type="text"] { + position: absolute; + right: 0; + max-width: 140px; + } - .pref-group { - display: block; - } + .pref-group { + display: block; + } - .pref-input { - position: relative; - margin-bottom: 6px; - } + .pref-input { + position: relative; + margin-bottom: 6px; + } - .pref-reset { - float: left; - } - - .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; - } + .pref-reset { + float: left; + } } diff --git a/src/sass/navbar.scss b/src/sass/navbar.scss index c999022..cf9c80e 100644 --- a/src/sass/navbar.scss +++ b/src/sass/navbar.scss @@ -1,90 +1,88 @@ -@import "_variables"; +@import '_variables'; nav { - display: flex; - align-items: center; - 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); - } - - body.fixed-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; + 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; + font-size: 15px; + font-weight: 600; + line-height: 1; - &:hover { - color: var(--accent_light); - text-decoration: unset; - } + &:hover { + color: var(--accent_light); + text-decoration: unset; + } } .site-logo { - display: block; - width: 35px; - height: 35px; + 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; + 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 { + text-align: right; + justify-content: flex-end; + } - &.right a:hover { - color: var(--accent_light); - text-decoration: unset; - } + &.right a { + padding-left: 4px; + + &:hover { + color: var(--accent_light); + text-decoration: unset; + } + } } .lp { - height: 14px; - display: inline-block; - position: relative; - top: 2px; - fill: var(--fg_nav); + height: 14px; + margin-top: 2px; + display: block; + fill: var(--fg_nav); - &:hover { - fill: var(--accent_light); - } + &:hover { + fill: var(--accent_light); + } } -.icon-info { - margin: 0 -3px; +.icon-info:before { + margin: 0 -3px; } .icon-cog { - font-size: 15px; - padding-left: 0 !important; + font-size: 15px; } diff --git a/src/sass/profile/_base.scss b/src/sass/profile/_base.scss index 81b3d78..b7f33e6 100644 --- a/src/sass/profile/_base.scss +++ b/src/sass/profile/_base.scss @@ -1,118 +1,83 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; -@import "card"; -@import "about-account"; -@import "photo-rail"; -@import "community"; +@import 'card'; +@import 'photo-rail'; .profile-tabs { - @include panel(auto, 900px); + @include panel(auto, 900px); - .timeline-container { - float: right; - width: 68% !important; - max-width: unset; - } + .timeline-container { + float: right; + width: 68% !important; + max-width: unset; + } } .profile-banner { - margin-bottom: 4px; - background-color: var(--bg_panel); + margin-bottom: 4px; + background-color: var(--bg_panel); - a { - display: block; - position: relative; - padding: 33.34% 0 0 0; - } + a { + display: block; + position: relative; + padding: 33.34% 0 0 0; + } - img { - max-width: 100%; - position: absolute; - top: 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: 0; - - body.fixed-nav & { + 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; + min-height: 54px; - .username { - margin: 0 !important; - } + .username { + margin: 0 !important; + } - .tweet-header { - margin-bottom: unset; - } + .tweet-header { + margin-bottom: unset; + } } -.profile-tabs.media-only { - max-width: none; - width: 100%; +@media(max-width: 700px) { + .profile-tabs { + width: 100vw; + max-width: 600px; - .timeline-container { - float: none; - width: 100% !important; - max-width: none; - padding: 0 10px; - box-sizing: border-box; - } + .timeline-container { + width: 100% !important; - .timeline-container > .tab { - max-width: 900px; - margin-left: auto; - margin-right: auto; - } -} - -@media (max-width: 700px) { - .profile-tabs { - width: 100vw; - max-width: 600px; - - .timeline-container { - width: 100% !important; - - .tab-item wide { - flex-grow: 1.4; - } + .tab-item wide { + flex-grow: 1.4; + } + } } - } - .profile-tabs.media-only { - width: 100%; - max-width: none; - - .timeline-container { - width: 100vw !important; - padding: 0; + .profile-tab { + width: 100%; + max-width: unset; + position: initial !important; + padding: 0; } - } - - .profile-tab { - width: 100%; - max-width: unset; - position: initial !important; - padding: 0; - } } @media (min-height: 900px) { - .profile-tab.sticky { - position: sticky; - } + .profile-tab.sticky { + position: sticky; + } } diff --git a/src/sass/profile/_community.scss b/src/sass/profile/_community.scss deleted file mode 100644 index 92d13c9..0000000 --- a/src/sass/profile/_community.scss +++ /dev/null @@ -1,203 +0,0 @@ -.community-header { - padding: 12px 15px; - border-bottom: 1px solid var(--border_grey); - background-color: var(--bg_panel); - - .community-name { - font-size: 22px; - margin-bottom: 6px; - - a { - color: inherit; - } - } - - .community-category { - display: inline-block; - background-color: var(--bg_elements); - border: 1px solid var(--border_grey); - border-radius: 16px; - padding: 2px 12px; - font-size: 13px; - color: var(--fg_faded); - margin-bottom: 8px; - } - - .community-description { - color: var(--fg_faded); - margin-bottom: 8px; - line-height: 1.4; - } - - .community-member-count { - font-weight: bold; - color: inherit; - } - - .community-stats { - color: var(--grey); - font-size: 14px; - } -} - -.community-about { - padding: 16px 15px 15px; - background-color: var(--bg_panel); - - h2 { - font-size: 18px; - margin: 0 0 12px; - } - - .community-info { - border-bottom: 1px solid var(--border_grey); - padding-bottom: 12px; - } - - .community-info-item { - display: flex; - gap: 10px; - padding: 8px 0; - align-items: center; - - .verified-icon { - margin-left: 2px; - } - - > .icon-container { - display: flex; - align-items: center; - justify-content: center; - font-size: 16px; - color: var(--grey); - flex-shrink: 0; - width: 26px; - height: 26px; - } - - strong { - color: var(--fg_color); - } - - a { - color: var(--accent); - } - } - - .community-rules { - border-bottom: 1px solid var(--border_grey); - padding: 16px 0 12px; - } - - .community-rules-intro { - color: var(--fg_faded); - font-size: 14px; - margin: 0 0 12px; - } - - .community-rule { - display: flex; - gap: 10px; - padding: 10px 0; - align-items: flex-start; - - .community-rule-number { - display: flex; - align-items: center; - justify-content: center; - min-width: 26px; - height: 26px; - border-radius: 50%; - background-color: var(--accent); - color: var(--fg_color); - font-weight: bold; - font-size: 13px; - flex-shrink: 0; - } - - .community-rule-content p { - margin: 4px 0 0; - color: var(--fg_faded); - font-size: 14px; - } - } - - .community-moderators { - padding-top: 16px; - - h2 { - display: flex; - align-items: center; - justify-content: space-between; - } - - .community-mods-link { - font-size: 14px; - font-weight: normal; - color: var(--accent); - } - } - - .community-moderator { - display: flex; - gap: 10px; - padding: 8px 0; - align-items: center; - - .community-mod-avatar { - width: 40px; - height: 40px; - border-radius: 50%; - } - - .community-mod-info { - display: flex; - flex-direction: column; - } - - .community-mod-name { - display: flex; - align-items: center; - font-weight: bold; - color: var(--fg_color); - - .verified-icon { - margin-left: 2px; - } - } - - .community-mod-username { - color: var(--fg_faded); - font-size: 14px; - } - } -} - -.community-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - padding: 10px 15px; - border-bottom: 1px solid var(--border_grey); - - .community-tag { - display: inline-block; - background-color: var(--bg_elements); - border: 1px solid var(--border_grey); - border-radius: 16px; - padding: 4px 12px; - font-size: 13px; - color: var(--accent); - } -} - -.community-hashtag-header { - padding: 12px 15px; - border-bottom: 1px solid var(--border_grey); - - .community-hashtag-title { - font-size: 20px; - color: var(--accent); - margin: 0; - } -} diff --git a/src/sass/profile/about-account.scss b/src/sass/profile/about-account.scss deleted file mode 100644 index aa12f49..0000000 --- a/src/sass/profile/about-account.scss +++ /dev/null @@ -1,71 +0,0 @@ -@import '_variables'; - -.about-account { - max-width: 500px; - width: 100%; - margin: 20px auto 0; - align-self: flex-start; - background: var(--bg_panel); - border-radius: 4px; - padding: 12px 20px 20px; -} - -.about-account-header { - display: flex; - flex-direction: column; - align-items: center; - margin-bottom: 16px; - padding-bottom: 14px; - border-bottom: 1px solid var(--border_grey); -} - -.about-account-avatar img { - width: 72px; - height: 72px; - border-radius: 50%; - margin-bottom: 4px; -} - -.about-account-name { - @include breakable; - font-weight: bold; -} - -.about-account-body { - display: flex; - flex-direction: column; - gap: 14px; -} - -.about-account-at { - font-size: 18px; - font-weight: bold; -} - -.about-account-row { - display: flex; - align-items: center; - gap: 10px; - - > span:first-child { - color: var(--fg_faded); - flex-shrink: 0; - } - - > div { - display: flex; - flex-direction: column; - } -} - -.about-account-label { - color: var(--fg_faded); - font-size: 13px; -} - -@media(max-width: 700px) { - .about-account { - max-width: none; - margin: 10px; - } -} diff --git a/src/sass/profile/card.scss b/src/sass/profile/card.scss index 46a9679..cc68d7d 100644 --- a/src/sass/profile/card.scss +++ b/src/sass/profile/card.scss @@ -73,9 +73,9 @@ } } - .profile-joindate, .profile-location, .profile-website { + .profile-joindate, .profile-location, profile-website { color: var(--fg_faded); - margin: 1px 0; + margin: 2px 0; width: 100%; } } @@ -115,7 +115,7 @@ } .profile-card-tabs-name { - flex-shrink: 100; + @include breakable; } .profile-card-avatar { diff --git a/src/sass/search.scss b/src/sass/search.scss index fa2a2d8..0311fb0 100644 --- a/src/sass/search.scss +++ b/src/sass/search.scss @@ -1,194 +1,120 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; .search-title { - font-weight: bold; - display: inline-block; - margin-top: 4px; + 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; - } + flex-wrap: wrap; - .pref-input { - margin: 0 4px 0 0; - flex-grow: 1; - height: 23px; - } + button { + margin: 0 2px 0 0; + height: 23px; + } - input[type="text"], - input[type="number"] { - height: calc(100% - 4px); - width: calc(100% - 8px); - } + .pref-input { + margin: 0 4px 0 0; + flex-grow: 1; + height: 23px; + } - > 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; + input[type="text"] { + height: calc(100% - 4px); + width: calc(100% - 8px); + } - @include input-colors; - } + > label { + display: inline; + background-color: var(--bg_elements); + color: var(--fg_color); + border: 1px solid var(--accent_border); + padding: 1px 6px 2px 6px; + font-size: 14px; + cursor: pointer; + margin-bottom: 2px; - @include create-toggle(search-panel, 380px); + @include input-colors; + } + + @include create-toggle(search-panel, 200px); } .search-panel { - width: 100%; - max-height: 0; - overflow: hidden; - transition: max-height 0.4s; + width: 100%; + max-height: 0; + overflow: hidden; + transition: max-height 0.4s; - flex-grow: 1; - font-weight: initial; - text-align: left; + flex-grow: 1; + font-weight: initial; + text-align: left; - .checkbox-container { - display: inline; - padding-right: unset; - margin-bottom: 5px; - margin-left: 23px; - } + > div { + line-height: 1.7em; + } - .checkbox { - right: unset; - left: -22px; - line-height: 1.6em; - } + .checkbox-container { + display: inline; + padding-right: unset; + margin-bottom: unset; + margin-left: 23px; + } - .checkbox-container .checkbox:after { - top: -4px; - } + .checkbox { + right: unset; + left: -22px; + } + + .checkbox-container .checkbox:after { + top: -4px; + } } .search-row { - display: flex; - flex-wrap: wrap; - line-height: unset; + 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; + > div { + flex-grow: 1; + flex-shrink: 1; + } input { - height: 21px; - margin-top: 1px; + 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; -} - -.list-result { - display: flex; - align-items: flex-start; - - .list-result-banner { - flex-shrink: 0; - width: 56px; - height: 56px; - margin-right: 10px; - border-radius: 8px; - overflow: hidden; - background-color: var(--darker_grey); - // stay above the tweet-link overlay's hover background - z-index: 1; - - img { - width: 100%; - height: 100%; - object-fit: cover; - } - } - - .list-result-body { - min-width: 0; - pointer-events: none; - z-index: 1; - - a { - pointer-events: all; - } - } - - .list-result-title { - align-items: baseline; - } - - .list-members { - flex-shrink: 0; - margin-left: 0.3em; - color: var(--fg_faded); - } - - .list-result-context { - display: flex; - align-items: center; - flex-wrap: wrap; - margin-top: 2px; - color: var(--fg_faded); - - a { - color: var(--fg_dark); - } - - a.fullname { - color: var(--fg_color); - } - - .list-facepile { - width: 20px; - height: 20px; - border-radius: 50%; - margin-right: 4px; - } - } - - .list-result-description { - margin-top: 2px; - overflow: hidden; - text-overflow: ellipsis; - white-space: nowrap; - pointer-events: all; - } + flex-grow: 1; + display: grid; + grid-template-columns: repeat(6, 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(820px, 5); + @include search-resize(725px, 4); + @include search-resize(600px, 6); + @include search-resize(560px, 5); + @include search-resize(480px, 4); + @include search-resize(410px, 3); } -@include search-resize(700px, 5); -@include search-resize(485px, 4); +@include search-resize(560px, 5); +@include search-resize(480px, 4); @include search-resize(410px, 3); diff --git a/src/sass/timeline.scss b/src/sass/timeline.scss index b7d4a9f..c8ce309 100644 --- a/src/sass/timeline.scss +++ b/src/sass/timeline.scss @@ -1,505 +1,162 @@ -@import "_variables"; +@import '_variables'; .timeline-container { - @include panel(100%, 600px); + @include panel(100%, 600px); } -.timeline-container.media-only { - max-width: none; - width: 100%; - padding: 0 10px; - box-sizing: border-box; +.timeline { + background-color: var(--bg_panel); - > .tab, - > .timeline-header { - max-width: 900px; - margin-left: auto; - margin-right: auto; - } -} - -@media (max-width: 700px) { - .timeline-container.media-only { - padding: 0; - } -} - -.timeline > div:not(:first-child) { - border-top: 1px solid var(--border_grey); + > 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: 4px; - box-sizing: border-box; + 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; - } + button { + float: unset; + } } .timeline-banner img { - width: 100%; + width: 100%; } .timeline-description { - font-weight: normal; + font-weight: normal; } .tab { - align-items: center; - display: flex; - flex-wrap: wrap; - list-style: none; - margin: 0 0 4px 0; - background-color: var(--bg_panel); - padding: 0; + 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; + 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; + a { + border-bottom: .1rem solid transparent; + color: var(--tab); + display: block; + padding: 8px 0; + text-decoration: none; + font-weight: bold; - &:hover { - text-decoration: none; + &:hover { + text-decoration: none; + } + + &.active { + border-bottom-color: var(--tab_selected); + color: var(--tab_selected); + } } - &.active { - border-bottom-color: var(--tab_selected); - color: var(--tab_selected); + &.active a { + 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; - } + &.wide { + flex-grow: 1.2; + flex-basis: 50px; + } } .timeline-footer { - background-color: var(--bg_panel); - padding: 6px 0; + background-color: var(--bg_panel); + padding: 6px 0; } .timeline-protected { - text-align: center; + text-align: center; - p { - margin: 8px 0; - } + p { + margin: 8px 0; + } - h2 { - color: var(--accent); - font-size: 20px; - font-weight: 600; - } + h2 { + color: var(--accent); + font-size: 20px; + font-weight: 600; + } } .timeline-none { - color: var(--accent); - font-size: 20px; - font-weight: 600; - text-align: center; + 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; + 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; + background-color: var(--bg_panel); + text-align: center; + padding: .75em 0; + display: block !important; - a { - background-color: var(--darkest_grey); - display: inline-block; - height: 2em; - padding: 0 2em; - line-height: 2em; + a { + background-color: var(--darkest_grey); + display: inline-block; + height: 2em; + padding: 0 2em; + line-height: 2em; - &:hover { - background-color: var(--darker_grey); + &:hover { + background-color: var(--darker_grey); + } } - } } .top-ref { - background-color: var(--bg_color); - border-top: none !important; + background-color: var(--bg_color); + border-top: none !important; - .icon-down { - font-size: 20px; - display: flex; - justify-content: center; - text-decoration: none; + .icon-down { + font-size: 20px; + display: flex; + justify-content: center; + text-decoration: none; - &:hover { - color: var(--accent_light); + &:hover { + color: var(--accent_light); + } + + &::before { + transform: rotate(180deg) translateY(-1px); + } } - - &::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; - background-color: var(--bg_panel); -} - -.timeline.media-grid-view, -.timeline.media-gallery-view { - > div:not(:first-child) { - border-top: none; - } - - .timeline-item::before { - display: none; - } -} - -.timeline.media-grid-view, -.timeline.media-gallery-view .gallery-masonry.compact { - .tweet-header, - .replying-to, - .retweet-header, - .pinned, - .tweet-stats, - .attribution, - .poll, - .quote, - .community-note, - .media-tag-block, - .tweet-content, - .card-content { - display: none; - } - - .card { - margin: unset; - - .card-container { - border: unset; - border-radius: unset; - - .card-image-container { - width: 100%; - min-height: 100%; - } - - .card-content-container { - display: none; - } - } - } -} - -.timeline.media-grid-view { - display: grid; - gap: 4px; - grid-template-columns: repeat(3, minmax(0, 1fr)); - - > div:not(:first-child) { - margin-top: 0; - } - - .timeline-item { - padding: 0; - } - - .tweet-link { - z-index: 1000; - - &:hover { - background-color: unset; - } - } - - > .show-more, - > .top-ref, - > .timeline-footer, - > .timeline-header { - grid-column: 1 / -1; - } - - .tweet-body { - height: 100%; - margin-left: 0; - padding: 0; + overflow-wrap: break-word; + border-left-width: 0; + min-width: 0; + padding: .75em; + display: flex; position: relative; - aspect-ratio: 1/1; - } - - .gallery-row + .gallery-row { - margin-top: 0.25em !important; - } - - .attachments { - background-color: var(--darkest_grey); - border-radius: 0; - margin: 0; - max-height: none; - } - - .attachments, - .gallery-row, - .still-image { - height: 100%; - width: 100%; - } - - .still-image img, - .attachment > video, - .attachment > img { - object-fit: cover; - height: 100%; - width: 100%; - } - - .attachment { - display: flex; - align-items: center; - } - - .gallery-video { - height: 100%; - } - - .media-gif { - display: flex; - } - - .timeline-item:hover { - opacity: 0.85; - } - - .alt-text { - display: none; - } -} - -.timeline.media-gallery-view { - .gallery-masonry { - margin: 10px 0; - column-gap: 10px; - column-width: unquote("clamp(190px, 22vw, 350px)"); - - &[data-col-size="small"] { - column-width: unquote("max(130px, 11vw)"); - } - - &[data-col-size="large"] { - column-width: unquote("clamp(350px, 22vw, 480px)"); - } - - &.masonry-active { - column-width: unset; - column-gap: unset; - position: relative; - - .timeline-item { - animation: none; - position: absolute; - box-sizing: border-box; - margin-bottom: 0; - } - } - - &.compact { - .tweet-body { - padding: 0; - - > .attachments { - margin: 0; - } - } - - .card-image-container img { - max-height: unset; - } - } - } - - @keyframes masonry-init { - to { - opacity: 1; - pointer-events: auto; - } - } - - // Start hidden. CSS animation reveals after a delay as a no-JS fallback. - // With JS, masonry-active cancels the animation and masonry-visible reveals. - .gallery-masonry .timeline-item, - > .show-more, - > .top-ref, - > .timeline-footer { - opacity: 0; - pointer-events: none; - animation: masonry-init 0.2s 0.3s forwards; - } - - .gallery-masonry.masonry-active .timeline-item.masonry-visible, - > .show-more.masonry-visible, - > .top-ref.masonry-visible, - > .timeline-footer.masonry-visible { - opacity: 1; - pointer-events: auto; - transition: opacity 0.15s ease; - animation: none; - } - - .timeline-item { - margin-bottom: 10px; - break-inside: avoid; - flex-direction: column; - padding: 0; - } - - > .show-more, - > .top-ref, - > .timeline-footer, - > .timeline-header { - margin-left: auto; - margin-right: auto; - max-width: 900px; - } - - > .show-more { - padding: 0; - margin-top: 8px; - background-color: unset; - } - - .tweet-content { - margin: 3px 0; - } - - .tweet-body { - display: flex; - flex-direction: column; - height: 100%; - margin-left: 0; - padding: 10px; - - > .attachments { - align-self: stretch; - border-radius: 0; - margin: -10px -10px 10px; - max-height: none; - order: -1; - width: auto; - background-color: var(--bg_elements); - - .gallery-row { - max-height: none; - max-width: none; - align-items: center; - } - - .still-image img, - .attachment > video, - .attachment > img { - max-height: none; - width: 100%; - } - - .attachment:last-child { - max-height: none; - } - - .card-container { - border: unset; - border-radius: unset; - } - } - - .tweet-stat { - padding-top: unset; - } - - .quote { - margin-bottom: 5px; - margin-top: 5px; - } - - .replying-to { - margin: 0; - } - } - - .tweet-header { - align-items: flex-start; - display: flex; - gap: 0.75em; - margin-bottom: 0; - - .tweet-avatar { - img { - float: none; - height: 42px; - margin: 0; - width: 42px; - } - } - - .tweet-name-row { - flex: 1; - } - - .fullname-and-username { - flex-wrap: wrap; - } - - .fullname { - max-width: calc(100% - 18px); - } - - .verified-icon { - margin-left: 4px; - margin-top: 1px; - } - - .username { - display: block; - flex-basis: 100%; - margin-left: 0; - } - } -} - -@media (max-width: 520px) { - .timeline.media-gallery-view { - padding: 8px 0; - } } diff --git a/src/sass/tweet/_base.scss b/src/sass/tweet/_base.scss index 2f6693e..7c1196a 100644 --- a/src/sass/tweet/_base.scss +++ b/src/sass/tweet/_base.scss @@ -1,284 +1,231 @@ -@import "_variables"; -@import "_mixins"; -@import "thread"; -@import "media"; -@import "video"; -@import "embed"; -@import "card"; -@import "poll"; -@import "quote"; +@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; + 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; + font-family: $font_3; + line-height: 1.3em; + pointer-events: all; + display: inline; } .tweet-bidi { - display: block !important; + display: block !important; } .tweet-header { - padding: 0; - vertical-align: bottom; - flex-basis: 100%; - margin-bottom: 0.2em; + padding: 0; + vertical-align: bottom; + flex-basis: 100%; + margin-bottom: .2em; - a { - display: inline-block; - word-break: break-all; - max-width: 100%; - pointer-events: all; - } + 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; - - .verified-icon { - margin-left: 2px; - } + padding: 0; + display: flex; + justify-content: space-between; } .fullname-and-username { - display: flex; - min-width: 0; + display: flex; + min-width: 0; } .fullname { - @include ellipsis; - flex-shrink: 2; - max-width: 80%; - font-size: 14px; - font-weight: 700; - color: var(--fg_color); + @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; + @include ellipsis; + min-width: 1.6em; + margin-left: .4em; + word-wrap: normal; } .tweet-date { - display: flex; - flex-shrink: 0; - margin-left: 4px; + display: flex; + flex-shrink: 0; + margin-left: 4px; } -.tweet-date a, -.username, -.show-more a { - color: var(--fg_dark); +.tweet-date a, .username, .show-more a { + color: var(--fg_dark); } .tweet-published { - margin-top: 6px; - margin-bottom: 0px; - color: var(--grey); + margin: 0; + margin-top: 5px; + color: var(--grey); + pointer-events: all; } .tweet-avatar { - display: contents !important; + display: contents !important; - img { - float: left; - margin-top: 3px; - margin-left: -58px; - width: 48px; - height: 48px; - } + img { + float: left; + margin-top: 3px; + margin-left: -58px; + width: 48px; + height: 48px; + } } .avatar { - &.round { - border-radius: 50%; - user-select: none; - -webkit-user-select: none; - } + position: absolute; - &.mini { - position: unset; - margin-right: 5px; - margin-top: -1px; - width: 20px; - height: 20px; - } + &.round { + border-radius: 50%; + } + + &.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); + } } .attribution { - display: flex; - pointer-events: all; - margin: 5px 0; + display: flex; + pointer-events: all; + margin: 5px 0; - strong { - color: var(--fg_color); - } + 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 { + 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; + font-size: 13px; } .tweet-geo { - color: var(--fg_faded); + color: var(--fg_faded); } .replying-to { - color: var(--fg_faded); - margin: -2px 0 4px; + color: var(--fg_faded); + margin: -2px 0 4px; - a { - pointer-events: all; - } + 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, .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; - } + span { + @include ellipsis; + } } .retweet-header { - margin-top: -5px !important; + margin-top: -5px !important; } .tweet-stats { - margin-bottom: -3px; - user-select: none; - -webkit-user-select: none; + margin-bottom: -3px; } .tweet-stat { - padding-top: 5px; - min-width: 1em; - margin-right: 0.8em; + padding-top: 5px; + min-width: 1em; + margin-right: 0.8em; } .show-thread { - display: block; - pointer-events: all; - padding-top: 2px; + 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; + 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; - user-select: none; - -webkit-user-select: none; + height: 100%; + width: 100%; + left: 0; + top: 0; + position: absolute; - &:hover { - background-color: var(--bg_hover); - } -} - -.latest-post-version { - border-bottom: 1px solid var(--dark_grey); - border-top: 1px solid var(--dark_grey); - padding: 01ch 0px; - margin: 1ch 0px; - color: var(--grey); - - 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; - - &: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; - - .icon-container { - flex-shrink: 0; - color: var(--accent); - } -} - -.community-note-text { - white-space: pre-line; - padding: 10px 10px; - padding-top: 6px; -} - -.disclosures { - display: flex; - flex-direction: column; - color: var(--grey); - font-size: 14px; - margin-top: 4px; - margin-bottom: -2px; - - .icon-attention-circled { - margin-right: -3px; - } + &:hover { + background-color: var(--bg_hover); + } } diff --git a/src/sass/tweet/card.scss b/src/sass/tweet/card.scss index 7441d11..680310c 100644 --- a/src/sass/tweet/card.scss +++ b/src/sass/tweet/card.scss @@ -1,119 +1,118 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; .card { - margin: 5px 0; - pointer-events: all; - max-height: unset; + 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; + 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); - } + &:hover { + border-color: var(--grey); + } - .attachments { - margin: 0; - border-radius: 0; - } + .attachments { + margin: 0; + border-radius: 0; + } } .card-content { - padding: 0.5em; + padding: 0.5em; } .card-title { - @include ellipsis; - white-space: unset; - font-weight: bold; - font-size: 1.1em; + @include ellipsis; + white-space: unset; + font-weight: bold; + font-size: 1.1em; } .card-description { - margin: 0.3em 0; - white-space: pre-wrap; + margin: 0.3em 0; } .card-destination { - @include ellipsis; - color: var(--grey); - display: block; + @include ellipsis; + color: var(--grey); + display: block; } .card-content-container { - color: unset; - overflow: auto; - - &:hover { - text-decoration: none; - } + 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%; - } + 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); + 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; - } + 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; + @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-container { + display: block; } - } - .card-image { - position: unset; - border-style: solid; - border-color: var(--dark_grey); - border-width: 0; - border-bottom-width: 1px; - } + .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/src/sass/tweet/embed.scss b/src/sass/tweet/embed.scss index 9ff8403..227fc5e 100644 --- a/src/sass/tweet/embed.scss +++ b/src/sass/tweet/embed.scss @@ -1,159 +1,17 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; -// Embed page: transparent background, no scrollbars -html:has(body > .embed-wrapper), -html:has(body > .embed-video) { - background: transparent; - overflow: hidden; - - body { - background: transparent; - overflow: hidden; - } -} - -// Tweet embed wrapper -.embed-wrapper { - box-sizing: border-box; - border: 1px solid var(--border_grey); - border-radius: 12px; - overflow: hidden; - - .embed-footer { - display: block; - padding: 12px 16px; - border-top: 1px solid var(--border_grey); - background: var(--bg_panel); - color: var(--accent); - font-size: 14px; - font-weight: 500; - text-align: center; - text-decoration: none; - transition: background-color 0.15s; - - &:hover { - background: var(--bg_hover); - } - } -} - -// Tweet embed content -.tweet-embed { - position: relative; - background-color: var(--bg_panel); - transition: background-color 0.15s; - - &:hover { - background-color: var(--bg_hover); - } - - .timeline-item { - pointer-events: none; - background-color: transparent; - } - - .tweet-link:hover { - background-color: transparent; - } - - .tweet-content { - font-size: 18px; - } - - .avatar:not(.mini) { - position: absolute; - } - - // Cap media height in embeds - .still-image img, - .quote-media-container img, - .quote-media-container video { - max-height: 600px; - } - - &.error-embed { - display: flex; - align-items: center; - justify-content: center; - min-height: 120px; - padding: 20px; - - .error-panel { - margin: 0; - } - } -} - -// Video-only embed .embed-video { - position: relative; - min-height: 300px; - background-color: black; - border: 1px solid var(--border_grey); - - .attachments { - margin: 0; - border-radius: 0; - max-height: 560px; - background-color: unset; - } - - .card { - margin: 0; - } - - .gallery-video { - width: 100%; - } - - .gallery-video>.attachment { - max-height: 560px; - width: 100%; - } - - video { - width: 100%; - height: auto; - max-height: 560px; - object-fit: contain; - } - - .video-download { - display: none; - } - - .video-overlay-link { - position: absolute; - top: 12px; - right: 12px; - padding: 6px 12px; - background: rgba(30, 30, 30, 0.75); - backdrop-filter: blur(4px); - color: #fff; - font-size: 13px; - font-weight: 700; - text-decoration: none; - border-radius: 9999px; - border: 1px solid transparent; - transition: - background 0.15s, - opacity 0.15s; - z-index: 10; - - &:hover { - background: rgba(60, 60, 60, 0.9); + .gallery-video { + width: 100%; + height: 100%; + position: absolute; + background-color: black; + top: 0%; + left: 0%; } - } - // Hide button while playing, show on hover or when paused - &.video-playing .video-overlay-link { - opacity: 0; - pointer-events: none; - } - - &.video-playing:hover .video-overlay-link { - opacity: 1; - pointer-events: auto; - } + .video-container { + max-height: unset; + } } diff --git a/src/sass/tweet/media.scss b/src/sass/tweet/media.scss index 3001a86..91c9dab 100644 --- a/src/sass/tweet/media.scss +++ b/src/sass/tweet/media.scss @@ -1,168 +1,119 @@ -@import "_variables"; +@import '_variables'; .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; - - &.mixed-row { - .attachment { - min-width: 0; - min-height: 0; - flex: 1 1 0; - max-height: 379.5px; - display: flex; - align-items: center; - justify-content: center; - background-color: #101010; - } - - .still-image, - .still-image img, - .attachment > video, - .attachment > img { - width: 100%; - height: 100%; - max-width: none; - max-height: none; - } + 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 { - display: flex; - align-self: stretch; + width: 100%; + display: flex; } - - .still-image img { - flex-basis: auto; - flex-grow: 0; - object-fit: cover; - } - - .attachment > video, - .attachment > img { - object-fit: cover; - } - - .attachment > video { - object-fit: contain; - } - } } .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; + 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; + + .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; + position: relative; + line-height: 0; + overflow: hidden; + margin: 0 .25em 0 0; + flex-grow: 1; + box-sizing: border-box; + min-width: 2em; - &:last-child { - margin: 0; + &:last-child { + margin: 0; + max-height: 530px; + } +} + +.gallery-gif video { max-height: 530px; - } -} - -.media-gif { - display: table; - background-color: unset; - width: unset; - max-height: unset; -} - -.media-gif video, -.media-gif img { - width: 100%; - height: 100%; - max-height: 530px; - background-color: #101010; + background-color: #101010; } .still-image { - max-height: 379.5px; - max-width: 533px; - - img { - object-fit: cover; - max-width: 100%; max-height: 379.5px; - flex-basis: 300px; - flex-grow: 1; - } + max-width: 533px; + justify-content: center; + + 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, 0.5); - backdrop-filter: blur(12px); +.image { + display: inline-block; } -.alt-text:hover { - padding: 7px; - width: Min(230px, calc(100% - 10px * 2)); - max-height: calc(100% - 10px); - line-height: 1.2em; - white-space: pre-wrap; - transition-duration: 0.4s; - transition-property: max-height; -} +// .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; + 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; + 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; + flex: 1; + padding: 0; + white-space: pre-wrap; } diff --git a/src/sass/tweet/poll.scss b/src/sass/tweet/poll.scss index 6d54e00..57590c8 100644 --- a/src/sass/tweet/poll.scss +++ b/src/sass/tweet/poll.scss @@ -1,42 +1,42 @@ -@import "_variables"; +@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; + 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); + 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; + 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; + position: relative; + pointer-events: all; } .poll-info { - color: var(--grey); - pointer-events: all; + color: var(--grey); + pointer-events: all; } .leader .poll-choice-bar { - background: var(--accent_dark); + background: var(--accent_dark); } diff --git a/src/sass/tweet/quote.scss b/src/sass/tweet/quote.scss index f722455..b4bc60e 100644 --- a/src/sass/tweet/quote.scss +++ b/src/sass/tweet/quote.scss @@ -1,121 +1,94 @@ -@import "_variables"; +@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: 8px 10px 6px 10px; - } - - .quote-text { + margin-top: 10px; + border: solid 1px var(--dark_grey); + border-radius: 10px; + background-color: var(--bg_elements); overflow: hidden; - white-space: pre-wrap; - word-wrap: break-word; - padding: 10px; - padding-top: 0; - } - - .show-thread { - padding: 0px 10px 6px 10px; - margin-top: -6px; - } - - .quote-latest { - padding: 0px 10px 6px 10px; - color: var(--grey); - } - - .replying-to { - padding: 0px 10px; - padding-bottom: 4px; - margin: unset; - } - - .community-note { - background-color: var(--bg_panel); - border: unset; - border-top: solid 1px var(--dark_grey); - border-radius: unset; - margin-top: 0; + pointer-events: all; + position: relative; + width: 100%; &:hover { - border-top-color: var(--grey); + border-color: var(--grey); } - .community-note-header { - background-color: var(--bg_panel); - padding-bottom: 0; + &.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; - display: block; + padding: 12px; } .quote-link { - width: 100%; - height: 100%; - left: 0; - top: 0; - position: absolute; + 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; - } - - .media-gif > .attachment { - display: flex; - justify-content: center; - background-color: var(--bg_color); - - video, - img { - height: unset; - width: unset; - max-height: 100%; - max-width: 100%; - } - } - - .gallery-row .attachment, - .gallery-row .attachment > video, - .gallery-row .attachment > img { max-height: 300px; - } + display: flex; - .still-image img { - max-height: 250px; - } + .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/src/sass/tweet/thread.scss b/src/sass/tweet/thread.scss index 134e375..f8ad603 100644 --- a/src/sass/tweet/thread.scss +++ b/src/sass/tweet/thread.scss @@ -1,196 +1,113 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; -.conversation, -.edit-history { - @include panel(100%, 600px); +.conversation { + @include panel(100%, 600px); - .show-more { - margin-bottom: 10px; - } + .show-more { + margin-bottom: 10px; + } } -.main-thread, -.latest-edit { - margin-bottom: 20px; +.main-thread { + margin-bottom: 20px; + background-color: var(--bg_panel); } -.reply { - margin-bottom: 10px; -} - -.reply-sort { - display: flex; - align-items: center; - flex-wrap: wrap; - gap: 2px 14px; - margin-bottom: 10px; - padding: 8px 12px; - background-color: var(--bg_panel); - font-size: 14px; -} - -.reply-sort-label { - color: var(--fg_faded); - margin-right: 2px; -} - -.reply-sort-option { - color: var(--tab); - font-weight: bold; - text-decoration: none; - border-bottom: 0.1rem solid transparent; - - &:hover { - color: var(--fg_color); - text-decoration: none; - } - - &.active { - color: var(--tab_selected); - border-bottom-color: var(--tab_selected); - } -} - -.main-tweet, -.replies, -.edit-history > div { - body.fixed-nav & { +.main-tweet, .replies { 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; + font-size: 18px; } -@media (max-width: 600px) { - .main-tweet .tweet-content { - font-size: 16px; - } + +@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; - } + .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; - } + .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; - } + .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; - } + .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; - } + .earlier-replies { + padding-bottom: 0; + margin-bottom: -5px; + } } .timeline-item.thread-last::before { - background: unset; - min-width: unset; - width: 0; - margin: 0; + background: unset; + min-width: unset; + width: 0; + margin: 0; } .more-replies { - padding-top: 0.3em !important; + 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; - } - } -} - -.related-header { - padding: 8px 12px; - margin-top: 10px; - background-color: var(--bg_panel); - color: var(--fg_faded); - font-size: 14px; - border-bottom: 1px solid var(--border_grey); + @include ellipsis; + display: block; + margin-left: 58px; + padding: 7px 0; } diff --git a/src/sass/tweet/video.scss b/src/sass/tweet/video.scss index 28fc125..2dd257a 100644 --- a/src/sass/tweet/video.scss +++ b/src/sass/tweet/video.scss @@ -1,111 +1,66 @@ -@import "_variables"; -@import "_mixins"; +@import '_variables'; +@import '_mixins'; video { - height: 100%; - width: 100%; + max-height: 100%; + max-width: 100%; } .gallery-video { - display: flex; - overflow: hidden; - - &.card-container { - flex-direction: column; - width: 100%; - } + display: flex; + overflow: hidden; +} - > .attachment { - min-height: 80px; - min-width: 200px; +.gallery-video.card-container { + flex-direction: column; +} + +.video-container { max-height: 530px; margin: 0; + display: flex; + align-items: center; + justify-content: center; img { - max-height: 100%; - max-width: 100%; + max-height: 100%; + max-width: 100%; } - } -} - -.video-download { - position: absolute; - top: 8px; - right: 8px; - z-index: 2; - color: var(--accent); - text-decoration: none; - opacity: 0; - transition: opacity 0.2s; - filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.7)); - - .icon-container { - margin: 0; - } - - .icon-download-alt { - font-size: 18px; - } -} - -.attachment:hover .video-download { - opacity: 1; - - &:hover { - color: var(--fg_color); - } -} - -@media (hover: none) { - .video-download { - opacity: 1; - } } .video-overlay { - @include play-button; - background-color: $shadow; + @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; - } + 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; - } + div { + 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; + } - form { - width: 100%; - height: 100%; - align-items: center; - justify-content: center; - display: flex; - } - - button { - padding: 5px 8px; - font-size: 16px; - } + button { + padding: 5px 8px; + font-size: 16px; + } } diff --git a/src/tid.nim b/src/tid.nim deleted file mode 100644 index ba1f8ec..0000000 --- a/src/tid.nim +++ /dev/null @@ -1,64 +0,0 @@ -import std/[asyncdispatch, base64, httpclient, random, strutils, sequtils, times] -import nimcrypto -import experimental/parser/tid - -randomize() - -const defaultKeyword = "obfiowerehiring"; -const pairsUrl = - "https://raw.githubusercontent.com/fa0311/x-client-transaction-id-pair-dict/refs/heads/main/pair.json"; - -var - cachedPairs: seq[TidPair] = @[] - lastCached = 0 - # refresh every hour - ttlSec = 60 * 60 - -proc getPair(): Future[TidPair] {.async.} = - if cachedPairs.len == 0 or int(epochTime()) - lastCached > ttlSec: - let client = newAsyncHttpClient() - defer: client.close() - - let resp = await client.get(pairsUrl) - if resp.status == $Http200: - cachedPairs = parseTidPairs(await resp.body) - lastCached = int(epochTime()) - - if cachedPairs.len == 0: - raise newException(ValueError, "Failed to fetch x-client-transaction-id pairs") - - return sample(cachedPairs) - -proc encodeSha256(text: string): array[32, byte] = - let - data = cast[ptr byte](addr text[0]) - dataLen = uint(len(text)) - digest = sha256.digest(data, dataLen) - return digest.data - -proc encodeBase64[T](data: T): string = - return encode(data).replace("=", "") - -proc decodeBase64(data: string): seq[byte] = - return cast[seq[byte]](decode(data)) - -proc genTid*(path: string): Future[string] {.async.} = - let - pair = await getPair() - - timeNow = int(epochTime() - 1682924400) - timeNowBytes = @[ - byte(timeNow and 0xff), - byte((timeNow shr 8) and 0xff), - byte((timeNow shr 16) and 0xff), - byte((timeNow shr 24) and 0xff) - ] - - data = "GET!" & path & "!" & $timeNow & defaultKeyword & pair.animationKey - hashBytes = encodeSha256(data) - keyBytes = decodeBase64(pair.verification) - bytesArr = keyBytes & timeNowBytes & hashBytes[0 ..< 16] & @[3'u8] - randomNum = byte(rand(256)) - tid = @[randomNum] & bytesArr.mapIt(it xor randomNum) - - return encodeBase64(tid) diff --git a/src/tokens.nim b/src/tokens.nim new file mode 100644 index 0000000..8a68e46 --- /dev/null +++ b/src/tokens.nim @@ -0,0 +1,154 @@ +# SPDX-License-Identifier: AGPL-3.0-only +import asyncdispatch, httpclient, times, sequtils, json, random +import strutils, tables +import zippy +import types, consts, http_pool + +const + maxConcurrentReqs = 5 # max requests at a time per token, to avoid race conditions + maxLastUse = 1.hours # if a token is unused for 60 minutes, it expires + maxAge = 2.hours + 55.minutes # tokens expire after 3 hours + failDelay = initDuration(minutes=30) + +var + clientPool: HttpPool + tokenPool: seq[Token] + lastFailed: Time + +proc getPoolJson*(): JsonNode = + var + list = newJObject() + totalReqs = 0 + totalPending = 0 + reqsPerApi: Table[string, int] + + for token in tokenPool: + totalPending.inc(token.pending) + list[token.tok] = %*{ + "apis": newJObject(), + "pending": token.pending, + "init": $token.init, + "lastUse": $token.lastUse + } + + for api in token.apis.keys: + list[token.tok]["apis"][$api] = %token.apis[api] + + let + maxReqs = + case api + of Api.listMembers, Api.listBySlug, Api.list, Api.userRestId: 500 + of Api.timeline: 187 + else: 180 + reqs = maxReqs - token.apis[api].remaining + + reqsPerApi[$api] = reqsPerApi.getOrDefault($api, 0) + reqs + totalReqs.inc(reqs) + + return %*{ + "amount": tokenPool.len, + "requests": totalReqs, + "pending": totalPending, + "apis": reqsPerApi, + "tokens": list + } + +proc rateLimitError*(): ref RateLimitError = + newException(RateLimitError, "rate limited") + +proc fetchToken(): Future[Token] {.async.} = + if getTime() - lastFailed < failDelay: + raise rateLimitError() + + let headers = newHttpHeaders({ + "accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8", + "accept-encoding": "gzip", + "accept-language": "en-US,en;q=0.5", + "connection": "keep-alive", + "authorization": auth + }) + + try: + let + resp = clientPool.use(headers): await c.postContent(activate) + tokNode = parseJson(uncompress(resp))["guest_token"] + tok = tokNode.getStr($(tokNode.getInt)) + time = getTime() + + return Token(tok: tok, init: time, lastUse: time) + except Exception as e: + lastFailed = getTime() + echo "fetching token failed: ", e.msg + +proc expired(token: Token): bool = + let time = getTime() + token.init < time - maxAge or token.lastUse < time - maxLastUse + +proc isLimited(token: Token; api: Api): bool = + if token.isNil or token.expired: + return true + + if api in token.apis: + let limit = token.apis[api] + return (limit.remaining <= 10 and limit.reset > epochTime().int) + else: + return false + +proc isReady(token: Token; api: Api): bool = + not (token.isNil or token.pending > maxConcurrentReqs or token.isLimited(api)) + +proc release*(token: Token; used=false; invalid=false) = + if token.isNil: return + if invalid or token.expired: + let idx = tokenPool.find(token) + if idx > -1: tokenPool.delete(idx) + elif used: + dec token.pending + token.lastUse = getTime() + +proc getToken*(api: Api): Future[Token] {.async.} = + for i in 0 ..< tokenPool.len: + if result.isReady(api): break + release(result) + result = tokenPool.sample() + + if not result.isReady(api): + release(result) + result = await fetchToken() + tokenPool.add result + + if not result.isNil: + inc result.pending + else: + raise rateLimitError() + +proc setRateLimit*(token: Token; api: Api; remaining, reset: int) = + # avoid undefined behavior in race conditions + if api in token.apis: + let limit = token.apis[api] + if limit.reset >= reset and limit.remaining < remaining: + return + + token.apis[api] = RateLimit(remaining: remaining, reset: reset) + +proc poolTokens*(amount: int) {.async.} = + var futs: seq[Future[Token]] + for i in 0 ..< amount: + futs.add fetchToken() + + for token in futs: + var newToken: Token + + try: newToken = await token + except: discard + + if not newToken.isNil: + tokenPool.add newToken + +proc initTokenPool*(cfg: Config) {.async.} = + clientPool = HttpPool() + + while true: + if tokenPool.countIt(not it.isLimited(Api.timeline)) < cfg.minTokens: + await poolTokens(min(4, cfg.minTokens - tokenPool.len)) + await sleepAsync(2000) diff --git a/src/types.nim b/src/types.nim index 0a748ba..9a6ad7f 100644 --- a/src/types.nim +++ b/src/types.nim @@ -6,77 +6,45 @@ genPrefsType() type RateLimitError* = object of CatchableError - NoSessionsError* = object of CatchableError InternalError* = object of CatchableError - BadClientError* = object of CatchableError - TimelineKind* {.pure.} = enum - tweets, replies, media, articles - - ApiUrl* = object - endpoint*: string - params*: seq[(string, string)] - skipTid*: bool - - ApiReq* = object - oauth*: ApiUrl - cookie*: ApiUrl + Api* {.pure.} = enum + userShow + timeline + search + tweet + list + listBySlug + listMembers + userRestId + status RateLimit* = object - limit*: int remaining*: int reset*: int - SessionKind* = enum - oauth - cookie - - Session* = ref object - id*: int64 - username*: string + Token* = ref object + tok*: string + init*: Time + lastUse*: Time pending*: int - limited*: bool - limitedAt*: int - apis*: Table[string, RateLimit] - case kind*: SessionKind - of oauth: - oauthToken*: string - oauthSecret*: string - of cookie: - authToken*: string - ct0*: string + apis*: Table[Api, RateLimit] Error* = enum null = 0 noUserMatches = 17 protectedUser = 22 - missingParams = 25 - timeout = 29 couldntAuth = 32 doesntExist = 34 - unauthorized = 37 - invalidParam = 47 userNotFound = 50 suspended = 63 rateLimited = 88 - expiredToken = 89 + invalidToken = 89 listIdOrSlug = 112 - timelineUnavailable = 131 tweetNotFound = 144 - tweetNotAuthorized = 179 forbidden = 200 - badRequest = 214 badToken = 239 - locked = 326 noCsrf = 353 - tweetUnavailable = 421 - tweetCensored = 422 - - VerifiedType* = enum - none = "None" - blue = "Blue" - business = "Business" - government = "Government" User* = object id*: string @@ -93,64 +61,11 @@ type tweets*: int likes*: int media*: int - verifiedType*: VerifiedType + verified*: bool protected*: bool suspended*: bool joinDate*: DateTime - AccountInfo* = object - username*: string - fullname*: string - userPic*: string - joinDate*: DateTime - verifiedType*: VerifiedType - suspended*: bool - basedIn*: string - source*: string - usernameChanges*: int - lastUsernameChange*: DateTime - affiliateUsername*: string - affiliateLabel*: string - isIdentityVerified*: bool - verifiedSince*: DateTime - overrideVerifiedYear*: int - - Broadcast* = object - id*: string - title*: string - state*: string - thumb*: string - mediaKey*: string - m3u8Url*: string - totalWatched*: int - startTime*: DateTime - endTime*: DateTime - replayStart*: int - availableForReplay*: bool - user*: User - - SpaceParticipant* = object - userId*: string - username*: string - displayName*: string - avatarUrl*: string - isVerified*: bool - - AudioSpace* = object - id*: string - title*: string - state*: string - mediaKey*: string - m3u8Url*: string - totalLiveListeners*: int - totalReplayWatched*: int - startTime*: DateTime - endTime*: DateTime - availableForReplay*: bool - creator*: User - admins*: seq[SpaceParticipant] - speakers*: seq[SpaceParticipant] - VideoType* = enum m3u8 = "application/x-mpegURL" mp4 = "video/mp4" @@ -160,12 +75,12 @@ type contentType*: VideoType url*: string bitrate*: int - resolution*: int Video* = object durationMs*: int url*: string thumb*: string + views*: string available*: bool reason*: string title*: string @@ -174,15 +89,10 @@ type variants*: seq[VideoVariant] QueryKind* = enum - posts, replies, media, users, tweets, userList, followers, following, lists, top, - articles - - RankingMode* = enum - Relevance, Recency, Likes + posts, replies, media, users, tweets, userList Query* = object kind*: QueryKind - view*: string text*: string filters*: seq[string] includes*: seq[string] @@ -190,33 +100,12 @@ type fromUser*: seq[string] since*: string until*: string - minLikes*: string + near*: string sep*: string Gif* = object url*: string thumb*: string - altText*: string - - Photo* = object - url*: string - altText*: string - - MediaKind* = enum - photoMedia - videoMedia - gifMedia - - Media* = object - case kind*: MediaKind - of photoMedia: - photo*: Photo - of videoMedia: - video*: Video - of gifMedia: - gif*: Gif - - MediaEntities* = seq[Media] GalleryPhoto* = object url*: string @@ -225,44 +114,6 @@ type PhotoRail* = seq[GalleryPhoto] - Article* = ref object - title*: string - coverImage*: string - user*: User - time*: DateTime - stats*: TweetStats - paragraphs*: seq[ArticleParagraph] - entities*: Table[int, ArticleEntity] - media*: Table[string, ArticleMedia] - - ArticleParagraph* = object - text*: string - kind*: string - inlineStyles*: seq[ArticleStyle] - entityRanges*: seq[ArticleEntityRange] - - ArticleStyle* = object - offset*: int - length*: int - style*: string - - ArticleEntityRange* = object - offset*: int - length*: int - key*: int - - ArticleEntity* = object - kind*: string - url*: string - mediaIds*: seq[string] - tweetId*: string - markdown*: string - caption*: string - - ArticleMedia* = object - kind*: string - url*: string - Poll* = object options*: seq[string] values*: seq[int] @@ -293,10 +144,8 @@ type imageDirectMessage = "image_direct_message" audiospace = "audiospace" newsletterPublication = "newsletter_publication" - jobDetails = "job_details" - hidden unknown - + Card* = object kind*: CardKind url*: string @@ -310,13 +159,7 @@ type replies*: int retweets*: int likes*: int - views*: int - - ArticlePreview* = object - title*: string - previewText*: string - coverImage*: string - tweetId*: int64 + quotes*: int Tweet* = ref object id*: int64 @@ -331,24 +174,16 @@ type available*: bool tombstone*: string location*: string - # Unused, needed for backwards compat - source*: string stats*: TweetStats retweet*: Option[Tweet] attribution*: Option[User] - attributionLink*: string mediaTags*: seq[User] quote*: Option[Tweet] card*: Option[Card] poll*: Option[Poll] - media*: MediaEntities - history*: seq[int64] - note*: string - isAd*: bool - isAI*: bool - articlePreview*: Option[ArticlePreview] - - Tweets* = seq[Tweet] + gif*: Option[Gif] + video*: Option[Video] + photos*: seq[string] Result*[T] = object content*: seq[T] @@ -357,10 +192,9 @@ type query*: Query Chain* = object - content*: Tweets + content*: seq[Tweet] hasMore*: bool cursor*: string - related*: bool Conversation* = ref object tweet*: Tweet @@ -368,18 +202,13 @@ type after*: Chain replies*: Result[Chain] - EditHistory* = object - latest*: Tweet - history*: Tweets - - Timeline* = Result[Tweets] + Timeline* = Result[Tweet] Profile* = object user*: User photoRail*: PhotoRail pinned*: Option[Tweet] tweets*: Timeline - accountInfo*: AccountInfo List* = object id*: string @@ -390,29 +219,6 @@ type members*: int banner*: string - ListSearchResult* = object - list*: List - owner*: User - followersContext*: string - facepiles*: seq[string] - - CommunityRule* = object - name*: string - description*: string - - Community* = object - id*: string - name*: string - description*: string - memberCount*: int - banner*: string - creator*: User - category*: string - joinPolicy*: string - createdAt*: DateTime - rules*: seq[CommunityRule] - hashtags*: seq[string] - GlobalObjects* = ref object tweets*: Table[string, Tweet] users*: Table[string, User] @@ -429,20 +235,10 @@ type hmacKey*: string base64Media*: bool minTokens*: int - enableRSSUserTweets*: bool - enableRSSUserReplies*: bool - enableRSSUserMedia*: bool - enableRSSUserArticles*: bool - enableRSSSearch*: bool - enableRSSList*: bool + enableRss*: bool enableDebug*: bool proxy*: string proxyAuth*: string - apiProxy*: string - disableTid*: bool - maxConcurrentReqs*: int - maxRetries*: int - retryDelayMs*: int rssCacheTime*: int listCacheTime*: int @@ -458,27 +254,3 @@ type proc contains*(thread: Chain; tweet: Tweet): bool = thread.content.anyIt(it.id == tweet.id) - -proc add*(timeline: var seq[Tweets]; tweet: Tweet) = - timeline.add @[tweet] - -proc getPhotos*(tweet: Tweet): seq[Photo] = - tweet.media.filterIt(it.kind == photoMedia).mapIt(it.photo) - -proc getVideos*(tweet: Tweet): seq[Video] = - tweet.media.filterIt(it.kind == videoMedia).mapIt(it.video) - -proc hasPhotos*(tweet: Tweet): bool = - tweet.media.anyIt(it.kind == photoMedia) - -proc hasVideos*(tweet: Tweet): bool = - tweet.media.anyIt(it.kind == videoMedia) - -proc hasGifs*(tweet: Tweet): bool = - tweet.media.anyIt(it.kind == gifMedia) - -proc getThumb*(media: Media): string = - case media.kind - of photoMedia: media.photo.url - of videoMedia: media.video.thumb - of gifMedia: media.gif.thumb diff --git a/src/utils.nim b/src/utils.nim index 95b46de..9c8414d 100644 --- a/src/utils.nim +++ b/src/utils.nim @@ -1,5 +1,5 @@ # SPDX-License-Identifier: AGPL-3.0-only -import sequtils, strutils, strformat, uri, tables, base64 +import strutils, strformat, uri, tables, base64 import nimcrypto var @@ -9,17 +9,14 @@ var const https* = "https://" twimg* = "pbs.twimg.com/" - nitterParams* = ["name", "tab", "id", "list", "referer", "scroll", "prefs"] + nitterParams = ["name", "tab", "id", "list", "referer", "scroll"] twitterDomains = @[ "twitter.com", "pic.twitter.com", "twimg.com", "abs.twimg.com", "pbs.twimg.com", - "video.twimg.com", - "x.com", - "pscp.tv", - "video.pscp.tv" + "video.twimg.com" ] proc setHmacKey*(key: string) = @@ -40,32 +37,18 @@ proc getVidUrl*(link: string): string = &"/video/{sig}/{encodeUrl(link)}" proc getPicUrl*(link: string): string = - if link.len == 0: return if base64Media: &"/pic/enc/{encode(link, safe=true)}" else: &"/pic/{encodeUrl(link)}" -proc getOrigPicUrl*(link: string): string = - if link.len == 0: return - if base64Media: - &"/pic/orig/enc/{encode(link, safe=true)}" - else: - &"/pic/orig/{encodeUrl(link)}" - proc filterParams*(params: Table): seq[(string, string)] = for p in params.pairs(): if p[1].len > 0 and p[0] notin nitterParams: result.add p proc isTwitterUrl*(uri: Uri): bool = - uri.scheme in ["http", "https"] and - (uri.hostname in twitterDomains or uri.hostname.endsWith(".video.pscp.tv")) + uri.hostname in twitterDomains proc isTwitterUrl*(url: string): bool = - isTwitterUrl(parseUri(url)) - -proc validateNumber*(value: string): string = - if value.anyIt(not it.isDigit): - return "" - return value + parseUri(url).hostname in twitterDomains diff --git a/src/views/about_account.nim b/src/views/about_account.nim deleted file mode 100644 index aedd444..0000000 --- a/src/views/about_account.nim +++ /dev/null @@ -1,93 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, times -import karax/[karaxdsl, vdom] - -import renderutils -import ".."/[types, formatters] - -proc renderAboutAccount*(info: AccountInfo): VNode = - let user = User( - username: info.username, - fullname: info.fullname, - userPic: info.userPic, - verifiedType: info.verifiedType - ) - - buildHtml(tdiv(class="about-account")): - tdiv(class="about-account-header"): - a(class="about-account-avatar", href=(&"/{info.username}")): - genImg(getUserPic(info.userPic, "_200x200")) - tdiv(class="about-account-name"): - linkUser(user, class="profile-card-fullname") - verifiedIcon(user) - linkUser(user, class="profile-card-username") - - tdiv(class="about-account-body"): - tdiv(class="about-account-row"): - span: icon "calendar" - tdiv: - span(class="about-account-label"): text "Date joined" - span(class="about-account-value"): - text info.joinDate.format("MMMM YYYY") - - if info.basedIn.len > 0: - tdiv(class="about-account-row"): - span: icon "location" - tdiv: - span(class="about-account-label"): text "Account based in" - span(class="about-account-value"): text info.basedIn - - if info.verifiedType != VerifiedType.none: - if info.overrideVerifiedYear != 0: - tdiv(class="about-account-row"): - span: icon "ok" - tdiv: - span(class="about-account-label"): text "Verified" - span(class="about-account-value"): - let year = abs(info.overrideVerifiedYear) - let era = if info.overrideVerifiedYear < 0: " BCE" else: "" - text "Since " & $year & era - elif info.verifiedSince.year > 0: - tdiv(class="about-account-row"): - span: icon "ok" - tdiv: - span(class="about-account-label"): text "Verified" - span(class="about-account-value"): - text "Since " & info.verifiedSince.format("MMMM YYYY") - - if info.isIdentityVerified: - tdiv(class="about-account-row"): - span: icon "ok" - tdiv: - span(class="about-account-label"): text "ID Verified" - span(class="about-account-value"): text "Yes" - - if info.affiliateUsername.len > 0: - tdiv(class="about-account-row"): - span: icon "group" - tdiv: - span(class="about-account-label"): text "An affiliate of" - span(class="about-account-value"): - a(href=(&"/{info.affiliateUsername}")): - if info.affiliateLabel.len > 0: - text info.affiliateLabel & " (@" & info.affiliateUsername & ")" - else: - text "@" & info.affiliateUsername - - if info.usernameChanges > 0: - tdiv(class="about-account-row"): - span(class="about-account-at"): text "@" - tdiv: - span(class="about-account-label"): - text $info.usernameChanges & " username change" - if info.usernameChanges > 1: text "s" - if info.lastUsernameChange.year > 0: - span(class="about-account-value"): - text "Last on " & info.lastUsernameChange.format("MMMM YYYY") - - if info.source.len > 0: - tdiv(class="about-account-row"): - span: icon "link" - tdiv: - span(class="about-account-label"): text "Connected via" - span(class="about-account-value"): text info.source diff --git a/src/views/article.nim b/src/views/article.nim deleted file mode 100644 index eedd4cf..0000000 --- a/src/views/article.nim +++ /dev/null @@ -1,248 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, tables, unicode, bitops, uri -import karax/[karaxdsl, vdom] - -import renderutils, tweet, timeline -import ".."/[types, utils, formatters] - -proc renderAtomicParagraph(paragraph: ArticleParagraph; article: Article; - tweets: Table[int64, Tweet]; path: string; - prefs: Prefs): VNode = - if paragraph.entityRanges.len == 0: - return text "" - - let er = paragraph.entityRanges[0] - if er.key notin article.entities: - return text "" - - let entity = article.entities[er.key] - - case entity.kind - of "MEDIA": - buildHtml(tdiv(class="article-media")): - for id in entity.mediaIds: - let media = article.media.getOrDefault(id) - if media.url.len == 0: - continue - case media.kind - of "ApiGif": - video(src=getVidUrl(media.url), controls="", autoplay="", loop="", - muted="") - of "ApiVideo": - video(src=getVidUrl(media.url), controls="") - else: - a(href=getOrigPicUrl(media.url), target="_blank"): - img(src=getSmallPic(media.url), alt=entity.caption, loading="lazy") - if entity.caption.len > 0: - p(class="article-media-caption"): text entity.caption - of "TWEET": - let tweet = tweets.getOrDefault( - try: parseBiggestInt(entity.tweetId) - except ValueError: 0, nil) - if tweet != nil: - renderTweet(tweet, prefs, path) - else: - text "" - of "MARKDOWN": - var content = entity.markdown - if content.startsWith("```"): - let firstNl = content.find('\n') - if firstNl >= 0: content = content[firstNl + 1 .. ^1] - if content.endsWith("```"): content = content[0 .. ^4] - content = content.strip - buildHtml(pre()): - code(): text content - of "DIVIDER": - buildHtml(hr(class="article-divider")) - else: - text "" - -proc wrapStyle(node: VNode; style: int): VNode = - result = node - if style.testBit(4): result = buildHtml(code()): result - if style.testBit(0): result = buildHtml(strong()): result - if style.testBit(1): result = buildHtml(em()): result - if style.testBit(2): result = buildHtml(del()): result - if style.testBit(3): result = buildHtml(underlined()): result - -proc addContent(target: VNode; content: string; style = 0) = - var first = true - for line in content.split('\n'): - if not first: - target.add VNode(kind: VNodeKind.br) - first = false - var pos = 0 - while pos < line.len: - let atPos = line.find('@', pos) - if atPos == -1: - target.add wrapStyle(text line[pos .. ^1], style) - break - if atPos > 0 and line[atPos - 1] in Letters + Digits + {'_'}: - target.add wrapStyle(text line[pos .. atPos], style) - pos = atPos + 1 - continue - var j = atPos + 1 - while j < line.len and j - atPos - 1 < 15 and - line[j] in Letters + Digits + {'_'}: - inc j - if j == atPos + 1: - target.add wrapStyle(text line[pos .. atPos], style) - pos = atPos + 1 - continue - if atPos > pos: - target.add wrapStyle(text line[pos ..< atPos], style) - let username = line[atPos + 1 ..< j] - let link = a.newVNode() - link.setAttr("href", "/" & username) - link.add wrapStyle(text ("@" & username), style) - target.add link - pos = j - -proc applyInlineStyles(target: VNode; runes: seq[Rune]; start, length: int; - styles: seq[ArticleStyle]) = - if styles.len == 0: - target.addContent($runes[start ..< start + length]) - return - - var - lastStyle = 0 - lastStart = start - let endPos = start + length - - for i in start ..< endPos: - var style = 0 - for sr in styles: - let - sStart = sr.offset - sEnd = sStart + sr.length - if sStart <= i and sEnd > i: - case sr.style - of "Bold": style.setBit(0) - of "Italic": style.setBit(1) - of "Strikethrough": style.setBit(2) - of "Underline": style.setBit(3) - of "Code": style.setBit(4) - else: discard - - if style != lastStyle: - if i > lastStart: - addContent(target, $runes[lastStart ..< i], lastStyle) - lastStyle = style - lastStart = i - - if lastStart < endPos: - addContent(target, $runes[lastStart ..< endPos], lastStyle) - -proc renderTextParagraph(paragraph: ArticleParagraph; article: Article): VNode = - let text = paragraph.text - - result = case paragraph.kind - of "header-one": h1.newVNode() - of "header-two": h2.newVNode() - of "header-three": h3.newVNode() - of "ordered-list-item", "unordered-list-item": li.newVNode() - of "blockquote": VNode(kind: VNodeKind.blockquote) - of "code-block": - let pre = pre.newVNode() - let code = code.newVNode() - code.add text text - pre.add code - return pre - else: p.newVNode() - - let - runes = text.toRunes - textLen = runes.len - var last = 0 - for er in paragraph.entityRanges: - if er.offset > last: - applyInlineStyles(result, runes, last, er.offset - last, - paragraph.inlineStyles) - - last = er.offset + er.length - - var target = result - if er.key in article.entities: - let entity = article.entities[er.key] - if entity.kind == "LINK": - let parsed = parseUri(entity.url) - if parsed.scheme in ["http", "https"]: - target = a.newVNode() - if parsed.isTwitterUrl: - target.setAttr("href", parsed.path) - else: - target.setAttr("href", entity.url) - - applyInlineStyles(target, runes, er.offset, er.length, - paragraph.inlineStyles) - if target != result: - result.add target - - if last < textLen: - applyInlineStyles(result, runes, last, textLen - last, - paragraph.inlineStyles) - - if paragraph.kind == "blockquote" and result.len > 0: - let lastChild = result[result.len - 1] - if lastChild.kind == VNodeKind.strong and lastChild.len > 0 and - lastChild[0].kind == VNodeKind.text: - lastChild.setAttr("class", "blockquote-attribution") - -proc renderArticle*(article: Article; tweets: Table[int64, Tweet]; - path: string; prefs: Prefs; tweetId=""): VNode = - let author = article.user - - let main = buildHtml(article(class="article-body")): - h1(class="article-title"): text article.title - - tdiv(class="article-author"): - tdiv(class="article-author-row"): - a(class="article-avatar", href=("/" & author.username)): - genImg(author.getUserPic("_bigger"), class=prefs.getAvatarClass) - tdiv(class="article-author-info"): - tdiv(class="article-author-name"): - linkUser(author, class="fullname") - verifiedIcon(author) - tdiv(class="article-author-meta"): - linkUser(author, class="username") - span(class="article-date-sep"): text " · " - a(class="article-date", - href=("/" & author.username & "/status/" & tweetId)): - text article.time.getShortTime - if not prefs.hideTweetStats: - renderStats(article.stats) - - var listKind = "" - var list: VNode = nil - - for paragraph in article.paragraphs: - let isListItem = paragraph.kind in [ - "ordered-list-item", "unordered-list-item"] - - if not isListItem and list != nil: - main.add list - list = nil - listKind = "" - - if paragraph.kind == "atomic": - main.add renderAtomicParagraph(paragraph, article, tweets, path, prefs) - elif isListItem: - if paragraph.kind != listKind: - if list != nil: - main.add list - list = if paragraph.kind == "ordered-list-item": ol.newVNode() - else: ul.newVNode() - listKind = paragraph.kind - list.add renderTextParagraph(paragraph, article) - else: - main.add renderTextParagraph(paragraph, article) - - if list != nil: - main.add list - - buildHtml(tdiv(class="article-page")): - if article.coverImage.len > 0: - a(href=getOrigPicUrl(article.coverImage), target="_blank"): - img(class="article-cover", src=getSmallPic(article.coverImage), alt="") - main - renderToTop() diff --git a/src/views/broadcast.nim b/src/views/broadcast.nim deleted file mode 100644 index bfcb9ba..0000000 --- a/src/views/broadcast.nim +++ /dev/null @@ -1,75 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, times -import karax/[karaxdsl, vdom] - -import renderutils -import ".."/[types, utils, formatters] - -proc renderBroadcast*(bc: Broadcast; prefs: Prefs; path: string): VNode = - let - isLive = bc.state == "RUNNING" - thumb = getPicUrl(bc.thumb) - source = if prefs.proxyVideos and bc.m3u8Url.startsWith("http"): - getVidUrl(bc.m3u8Url) else: bc.m3u8Url - stateText = - if isLive: "LIVE" - elif bc.endTime.year > 1: "Ended " & bc.endTime.format("MMM d, YYYY") - elif bc.state.len > 0: bc.state - else: "Ended" - durationMs = - if bc.startTime.year > 1 and bc.endTime.year > 1: - int((bc.endTime - bc.startTime).inMilliseconds) - bc.replayStart * 1000 - else: 0 - duration = if durationMs > 0: getDuration(durationMs) else: "" - - buildHtml(tdiv(class="broadcast-page")): - tdiv(class="broadcast-panel"): - tdiv(class="broadcast-player"): - if bc.m3u8Url.len > 0 and prefs.hlsPlayback: - video(poster=thumb, data-url=source, data-autoload="false", - data-start=($bc.replayStart), muted=prefs.muteVideos) - verbatim "
" - tdiv(class="overlay-circle"): span(class="overlay-triangle") - if isLive: - tdiv(class="broadcast-live"): text "LIVE" - elif duration.len > 0: - tdiv(class="overlay-duration"): text duration - verbatim "
" - elif bc.m3u8Url.len > 0: - img(src=thumb, alt=bc.title) - tdiv(class="video-overlay"): - buttonReferer "/enablehls", "Enable hls playback", path - if isLive: - tdiv(class="broadcast-live"): text "LIVE" - elif duration.len > 0: - tdiv(class="overlay-duration"): text duration - elif bc.thumb.len > 0: - img(src=thumb, alt=bc.title) - tdiv(class="video-overlay"): - if bc.availableForReplay: - p: text "Stream unavailable" - else: - p: text "Replay is not available" - else: - tdiv(class="video-overlay"): - p: text "Broadcast not found" - - tdiv(class="broadcast-info"): - h2(class="broadcast-title"): text bc.title - - tdiv(class="broadcast-user-row"): - a(class="broadcast-user", href=("/" & bc.user.username)): - genImg(getUserPic(bc.user.userPic, "_bigger")) - tdiv: - tdiv: - strong: text bc.user.fullname - verifiedIcon(bc.user) - span(class="broadcast-username"): text "@" & bc.user.username - - tdiv(class="broadcast-meta"): - if bc.totalWatched > 0: - span: text insertSep($bc.totalWatched, ',') & " views" - if isLive: - span(class="broadcast-live"): text stateText - else: - span: text stateText diff --git a/src/views/community.nim b/src/views/community.nim deleted file mode 100644 index 52f9041..0000000 --- a/src/views/community.nim +++ /dev/null @@ -1,128 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, times -import karax/[karaxdsl, vdom] - -import renderutils -import ".."/[types, utils, formatters] - -proc renderCommunityTabs*(kind: QueryKind; community: Community): VNode = - let - path = &"/i/communities/{community.id}" - q = Query(kind: kind) - buildHtml(tdiv): - ul(class="tab"): - li(class=q.getTabClass(posts)): - a(href=path): text "Top" - li(class=q.getTabClass(replies)): - a(href=(path & "/latest")): text "Latest" - li(class=q.getTabClass(media)): - a(href=(path & "/media")): text "Media" - li(class=q.getTabClass(userList)): - a(href=(path & "/about")): text "About" - if community.hashtags.len > 0: - tdiv(class="community-tags"): - for tag in community.hashtags: - let bare = tag.strip(chars={'#'}) - a(class="community-tag", - href=(&"/i/communities/{community.id}/hashtag/{bare}")): - text tag - -proc renderMemberTabs*(community: Community; isModerators: bool): VNode = - let path = &"/i/communities/{community.id}" - buildHtml(ul(class="tab")): - li(class=(if not isModerators: "tab-item active" else: "tab-item")): - a(href=(path & "/members")): text "All" - li(class=(if isModerators: "tab-item active" else: "tab-item")): - a(href=(path & "/moderators")): text "Moderators" - -proc renderHashtagHeader*(community: Community; tag: string): VNode = - buildHtml(tdiv(class="community-hashtag-header")): - h2(class="community-hashtag-title"): text "#" & tag - -proc renderCommunityAbout*(community: Community; moderators: seq[User]): VNode = - buildHtml(tdiv(class="community-about")): - tdiv(class="community-info"): - h2: text "Community Info" - tdiv(class="community-info-item"): - icon "group" - if community.joinPolicy == "Open": - text "Anyone can join this Community." - else: - text "Membership is by approval only." - - tdiv(class="community-info-item"): - icon "info" - text "All Communities are publicly visible." - - tdiv(class="community-info-item"): - icon "calendar" - let - date = community.createdAt.format("MMMM d, yyyy") - creator = community.creator.username - span: - text &"Created {date} by " - a(href=(&"/{creator}")): text &"@{creator}" - if community.creator.verifiedType != none: - verifiedIcon(community.creator) - - if community.rules.len > 0: - tdiv(class="community-rules"): - h2: text "Rules" - p(class="community-rules-intro"): - text "These are set and enforced by Community admins and are in addition to " - a(href="https://help.x.com/rules-and-policies/x-rules"): text "X's rules" - text "." - - for i, rule in community.rules: - tdiv(class="community-rule"): - span(class="community-rule-number"): text $(i + 1) - tdiv(class="community-rule-content"): - strong: text rule.name - if rule.description.len > 0: - p: text rule.description - - if moderators.len > 0: - tdiv(class="community-moderators"): - h2: - text "Moderators" - a(class="community-mods-link", - href=(&"/i/communities/{community.id}/moderators")): - text "See all" - for user in moderators: - tdiv(class="community-moderator"): - a(href=(&"/{user.username}")): - genImg(user.getUserPic("_bigger"), class="community-mod-avatar") - tdiv(class="community-mod-info"): - a(href=(&"/{user.username}"), class="community-mod-name"): - text user.fullname - if user.verifiedType != none: - verifiedIcon(user) - a(href=(&"/{user.username}"), class="community-mod-username"): - text &"@{user.username}" - -proc renderCommunity*(body, nav: VNode; community: Community): VNode = - buildHtml(tdiv(class="timeline-container")): - if community.banner.len > 0: - tdiv(class="timeline-banner"): - a(href=getPicUrl(community.banner), target="_blank"): - genImg(community.banner) - - tdiv(class="community-header"): - h1(class="community-name"): - a(href=(&"/i/communities/{community.id}")): text community.name - - if community.category.len > 0: - span(class="community-category"): text community.category - - if community.description.len > 0: - tdiv(class="community-description"): - text community.description - - tdiv(class="community-stats"): - a(class="community-member-count", - href=(&"/i/communities/{community.id}/members")): - text insertSep($community.memberCount, ',') - text " Members" - - nav - body diff --git a/src/views/embed.nim b/src/views/embed.nim index 5136a35..e6afffd 100644 --- a/src/views/embed.nim +++ b/src/views/embed.nim @@ -1,76 +1,21 @@ # SPDX-License-Identifier: AGPL-3.0-only +import options import karax/[karaxdsl, vdom] from jester import Request -import ".."/[types, formatters, prefs] +import ".."/[types, formatters] import general, tweet -const - doctype = "\n" - embedResizeJs = staticRead("../../public/js/embedResize.js") +const doctype = "\n" proc renderVideoEmbed*(tweet: Tweet; cfg: Config; req: Request): string = - let - video = tweet.getVideos()[0] - thumb = video.thumb - vidUrl = getVideoEmbed(cfg, tweet.id) - prefs = Prefs(hlsPlayback: true, mp4Playback: true, proxyVideos: defaultPrefs.proxyVideos) - tweetUrl = getLink(tweet) - + let thumb = get(tweet.video).thumb + let vidUrl = getVideoEmbed(cfg, tweet.id) + let prefs = Prefs(hlsPlayback: true) let node = buildHtml(html(lang="en")): renderHead(prefs, cfg, req, video=vidUrl, images=(@[thumb])) - base(target="_blank") - body: - tdiv(class="embed-video"): - renderVideo(video, prefs, "") - a(class="video-overlay-link", href=tweetUrl): - text "Watch on " & cfg.hostname - - script: - verbatim embedResizeJs - - result = doctype & $node - -proc renderTweetEmbed*(tweet: Tweet; path: string; prefs: Prefs; cfg: Config; req: Request): string = - let node = buildHtml(html(lang="en")): - renderHead(prefs, cfg, req) - base(target="_blank") - - body: - tdiv(class="embed-wrapper"): - tdiv(class="tweet-embed"): - a(class="tweet-link", href=getLink(tweet)) - renderTweet(tweet, prefs, path, mainTweet=true) - a(class="embed-footer", href=getLink(tweet)): - text "Read more on " & cfg.hostname - - script: - verbatim embedResizeJs - - result = doctype & $node - -proc renderErrorEmbed*(error: string; prefs: Prefs; cfg: Config; req: Request; - tweetId = ""; username = ""): string = - let link = if tweetId.len > 0: - if username.len > 0: "/" & username & "/status/" & tweetId - else: "/i/status/" & tweetId - else: "/" - - let node = buildHtml(html(lang="en")): - renderHead(prefs, cfg, req) - base(target="_blank") - - body: - tdiv(class="embed-wrapper"): - tdiv(class="tweet-embed error-embed"): - a(class="tweet-link", href=link) - tdiv(class="error-panel"): - span: text error - a(class="embed-footer", href=link): - text "Read more on " & cfg.hostname - - script: - verbatim embedResizeJs + tdiv(class="embed-video"): + renderVideo(get(tweet.video), prefs, "") result = doctype & $node diff --git a/src/views/general.nim b/src/views/general.nim index d979898..82902d4 100644 --- a/src/views/general.nim +++ b/src/views/general.nim @@ -29,17 +29,19 @@ proc renderNavbar(cfg: Config; req: Request; rss, canonical: string): VNode = tdiv(class="nav-item right"): icon "search", title="Search", href="/search" - if rss.len > 0: - icon "rss", title="RSS Feed", href=rss - icon "bird", title="Open in X", href=canonical + if cfg.enableRss and rss.len > 0: + icon "rss-feed", title="RSS Feed", href=rss + icon "bird", title="Open in Twitter", href=canonical a(href="https://liberapay.com/zedeus"): verbatim lp icon "info", title="About", href="/about" icon "cog", title="Preferences", href=("/settings?referer=" & encodeUrl(path)) proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; video=""; images: seq[string] = @[]; banner=""; ogTitle=""; - rss=""; alternate=""; oembed=""): VNode = - let theme = prefs.theme.toTheme + rss=""; canonical=""): VNode = + var theme = prefs.theme.toTheme + if "theme" in req.params: + theme = req.params["theme"].toTheme let ogType = if video.len > 0: "video" @@ -50,8 +52,8 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; let opensearchUrl = getUrlPrefix(cfg) & "/opensearch" buildHtml(head): - link(rel="stylesheet", type="text/css", href="/css/style.css?v=106") - link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=7") + link(rel="stylesheet", type="text/css", href="/css/style.css?v=16") + link(rel="stylesheet", type="text/css", href="/css/fontello.css?v=2") if theme.len > 0: link(rel="stylesheet", type="text/css", href=(&"/css/themes/{theme}.css")) @@ -64,19 +66,15 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; link(rel="search", type="application/opensearchdescription+xml", title=cfg.title, href=opensearchUrl) - if alternate.len > 0: - link(rel="alternate", href=alternate, title="View on X") + if canonical.len > 0: + link(rel="canonical", href=canonical) - if rss.len > 0: + if cfg.enableRss and rss.len > 0: link(rel="alternate", type="application/rss+xml", href=rss, title="RSS feed") - if oembed.len > 0: - let oembedTitle = if titleText.len > 0: titleText else: "oEmbed" - link(rel="alternate", type="application/json+oembed", href=oembed, title=oembedTitle) - if prefs.hlsPlayback: - script(src="/js/hls.min.js", `defer`="") - script(src="/js/hlsPlayback.js?v=1", `defer`="") + script(src="/js/hls.light.min.js", `defer`="") + script(src="/js/hlsPlayback.js", `defer`="") if prefs.infiniteScroll: script(src="/js/infiniteScroll.js", `defer`="") @@ -88,7 +86,6 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; text cfg.title meta(name="viewport", content="width=device-width, initial-scale=1.0") - meta(name="referrer", content="same-origin") meta(name="theme-color", content="#1F1F1F") meta(property="og:type", content=ogType) meta(property="og:title", content=(if ogTitle.len > 0: ogTitle else: titleText)) @@ -96,14 +93,14 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; meta(property="og:site_name", content="Nitter") meta(property="og:locale", content="en_US") - if banner.len > 0 and not banner.startsWith('#'): + if banner.len > 0: let bannerUrl = getPicUrl(banner) link(rel="preload", type="image/png", href=bannerUrl, `as`="image") for url in images: - if url.len == 0: continue - let preloadUrl = if "400x400" in url: getPicUrl(url) - else: getSmallPic(url) + let suffix = if "400x400" in url or url.endsWith("placeholder.png"): "" + else: "?name=small" + let preloadUrl = getPicUrl(url & suffix) link(rel="preload", type="image/png", href=preloadUrl, `as`="image") let image = getUrlPrefix(cfg) & getPicUrl(url) @@ -123,24 +120,20 @@ proc renderHead*(prefs: Prefs; cfg: Config; req: Request; titleText=""; desc=""; # this is last so images are also preloaded # if this is done earlier, Chrome only preloads one image for some reason link(rel="preload", type="font/woff2", `as`="font", - href="/fonts/fontello.woff2?59696369", crossorigin="anonymous") + href="/fonts/fontello.woff2?21002321", crossorigin="anonymous") proc renderMain*(body: VNode; req: Request; cfg: Config; prefs=defaultPrefs; titleText=""; desc=""; ogTitle=""; rss=""; video=""; - images: seq[string] = @[]; banner=""; - twitterLink=""; oembed=""): string = + images: seq[string] = @[]; banner=""): string = - let twitterLink = - if twitterLink.len > 0: twitterLink - else: getTwitterLink(req.path, req.params) + let canonical = getTwitterLink(req.path, req.params) let node = buildHtml(html(lang="en")): renderHead(prefs, cfg, req, titleText, desc, video, images, banner, ogTitle, - rss, twitterLink, oembed) + rss, canonical) - let bodyClass = if prefs.stickyNav: "fixed-nav" else: "" - body(class=bodyClass): - renderNavbar(cfg, req, rss, twitterLink) + body: + renderNavbar(cfg, req, rss, canonical) tdiv(class="container"): body diff --git a/src/views/oembed.nimf b/src/views/oembed.nimf deleted file mode 100644 index 4f7f947..0000000 --- a/src/views/oembed.nimf +++ /dev/null @@ -1,7 +0,0 @@ -#? stdtmpl(subsChar = '$', metaChar = '#') -## SPDX-License-Identifier: AGPL-3.0-only -#proc renderOembedIframe*(embedUrl: string; maxwidth = 550): string = -# result = "" - -# result = result.strip() -#end proc diff --git a/src/views/preferences.nim b/src/views/preferences.nim index b051a01..1787704 100644 --- a/src/views/preferences.nim +++ b/src/views/preferences.nim @@ -32,8 +32,7 @@ macro renderPrefs*(): untyped = result[2].add stmt -proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]; - prefsUrl: string): VNode = +proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]): VNode = buildHtml(tdiv(class="overlay-panel")): fieldset(class="preferences"): form(`method`="post", action="/saveprefs", autocomplete="off"): @@ -41,14 +40,6 @@ proc renderPreferences*(prefs: Prefs; path: string; themes: seq[string]; renderPrefs() - legend: text "Bookmark" - p(class="bookmark-note"): - text "Save this URL to restore your preferences (?prefs works on all pages)" - pre(class="prefs-code"): - text prefsUrl - p(class="bookmark-note"): - verbatim "You can override preferences with query parameters (e.g. ?hlsPlayback=on). These overrides aren't saved to cookies, and links won't retain the parameters. Intended for configuring RSS feeds and other cookieless environments. Hover over a preference to see its name." - h4(class="note"): text "Preferences are stored client-side using cookies without any personal information." diff --git a/src/views/profile.nim b/src/views/profile.nim index c9012ed..9eda46d 100644 --- a/src/views/profile.nim +++ b/src/views/profile.nim @@ -2,7 +2,7 @@ import strutils, strformat import karax/[karaxdsl, vdom, vstyles] -import renderutils, search, timeline +import renderutils, search import ".."/[types, utils, formatters] proc renderStat(num: int; class: string; text=""): VNode = @@ -12,14 +12,7 @@ proc renderStat(num: int; class: string; text=""): VNode = span(class="profile-stat-num"): text insertSep($num, ',') -proc renderStatLink(num: int; class, href: string): VNode = - buildHtml(li(class=class)): - a(href=href): - span(class="profile-stat-header"): text capitalizeAscii(class) - span(class="profile-stat-num"): - text insertSep($num, ',') - -proc renderUserCard*(user: User; prefs: Prefs; info: AccountInfo): VNode = +proc renderUserCard*(user: User; prefs: Prefs): VNode = buildHtml(tdiv(class="profile-card")): tdiv(class="profile-card-info"): let @@ -33,7 +26,6 @@ proc renderUserCard*(user: User; prefs: Prefs; info: AccountInfo): VNode = tdiv(class="profile-card-tabs-name"): linkUser(user, class="profile-card-fullname") - verifiedIcon(user) linkUser(user, class="profile-card-username") tdiv(class="profile-card-extra"): @@ -53,27 +45,22 @@ proc renderUserCard*(user: User; prefs: Prefs; info: AccountInfo): VNode = else: span: text place - if info.basedIn.len > 0: - tdiv(class="profile-location"): - span: icon "location" - span: text "Based in " & info.basedIn - if user.website.len > 0: tdiv(class="profile-website"): span: let url = replaceUrls(user.website, prefs) icon "link" - a(href=url): text url.shortLink + a(href=url): text shortLink(url) tdiv(class="profile-joindate"): - a(href=(&"/{user.username}/about"), title=getJoinDateFull(user)): + span(title=getJoinDateFull(user)): icon "calendar", getJoinDate(user) tdiv(class="profile-card-extra-links"): ul(class="profile-statlist"): renderStat(user.tweets, "posts", text="Tweets") - renderStatLink(user.following, "following", &"/{user.username}/following") - renderStatLink(user.followers, "followers", &"/{user.username}/followers") + renderStat(user.following, "following") + renderStat(user.followers, "followers") renderStat(user.likes, "likes") proc renderPhotoRail(profile: Profile): VNode = @@ -91,11 +78,8 @@ proc renderPhotoRail(profile: Profile): VNode = tdiv(class="photo-rail-grid"): for i, photo in profile.photoRail: if i == 16: break - let photoSuffix = - if "format" in photo.url or "placeholder" in photo.url: "" - else: ":thumb" a(href=(&"/{profile.user.username}/status/{photo.tweetId}#m")): - genImg(photo.url & photoSuffix) + genImg(photo.url & (if "format" in photo.url: "" else: ":thumb")) proc renderBanner(banner: string): VNode = buildHtml(): @@ -106,7 +90,7 @@ proc renderBanner(banner: string): VNode = else: a(href=getPicUrl(banner), target="_blank"): genImg(banner) -proc renderProtected*(username: string): VNode = +proc renderProtected(username: string): VNode = buildHtml(tdiv(class="timeline-container")): tdiv(class="timeline-header timeline-protected"): h2: text "This account's tweets are protected." @@ -114,52 +98,19 @@ proc renderProtected*(username: string): VNode = proc renderProfile*(profile: var Profile; prefs: Prefs; path: string): VNode = profile.tweets.query.fromUser = @[profile.user.username] - let - isGalleryView = profile.tweets.query.kind == QueryKind.media and - profile.tweets.query.view == "gallery" - viewClass = if isGalleryView: " media-only" else: "" - buildHtml(tdiv(class=("profile-tabs" & viewClass))): - if not isGalleryView and not prefs.hideBanner: + buildHtml(tdiv(class="profile-tabs")): + if not prefs.hideBanner: tdiv(class="profile-banner"): renderBanner(profile.user.banner) - if not isGalleryView: - let sticky = if prefs.stickyProfile: " sticky" else: "" - tdiv(class=("profile-tab" & sticky)): - renderUserCard(profile.user, prefs, profile.accountInfo) - if profile.photoRail.len > 0: - renderPhotoRail(profile) + let sticky = if prefs.stickyProfile: " sticky" else: "" + tdiv(class=(&"profile-tab{sticky}")): + renderUserCard(profile.user, prefs) + if profile.photoRail.len > 0: + renderPhotoRail(profile) if profile.user.protected: renderProtected(profile.user.username) else: renderTweetSearch(profile.tweets, prefs, path, profile.pinned) - -proc renderFollowTabs(user: User; activeTab: string): VNode = - buildHtml(ul(class="tab")): - for tab in ["Following", "Followers"]: - li(class=(if activeTab == tab: "tab-item active" else: "tab-item")): - a(href=(&"/{user.username}/{tab.toLowerAscii()}")): text tab - -proc renderUserList*(user: User; results: Result[User]; prefs: Prefs; - path, activeTab: string): VNode = - # Check if we've loaded all results (for page 1) - var displayResults = results - if results.beginning: - let expectedCount = if activeTab == "Followers": user.followers else: user.following - if results.content.len >= expectedCount: - displayResults.bottom = "" - - buildHtml(tdiv(class="profile-tabs")): - if not prefs.hideBanner: - tdiv(class="profile-banner"): - renderBanner(user.banner) - - let sticky = if prefs.stickyProfile: " sticky" else: "" - tdiv(class=("profile-tab" & sticky)): - renderUserCard(user, prefs, AccountInfo()) - - tdiv(class="timeline-container"): - renderFollowTabs(user, activeTab) - renderTimelineUsers(displayResults, prefs, path) diff --git a/src/views/renderutils.nim b/src/views/renderutils.nim index af2f05d..3e0cd19 100644 --- a/src/views/renderutils.nim +++ b/src/views/renderutils.nim @@ -1,45 +1,19 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat +import strutils import karax/[karaxdsl, vdom, vstyles] import ".."/[types, utils] -const smallWebp* = "?name=small&format=webp" -const mediumWebp* = "?name=medium&format=webp" - -proc getSmallPic*(url: string): string = - if url.len == 0: return - result = url - if "?" notin url and not url.endsWith("placeholder.png"): - result &= smallWebp - result = getPicUrl(result) - -proc getMediumPic*(url: string): string = - if url.len == 0: return - result = url - if "?" notin url and not url.endsWith("placeholder.png"): - result &= mediumWebp - result = getPicUrl(result) - -proc icon*(icon: string; label=""; title=""; class=""; href=""): VNode = +proc icon*(icon: string; text=""; title=""; class=""; href=""): VNode = var c = "icon-" & icon - if class.len > 0: c = &"{c} {class}" + if class.len > 0: c = c & " " & class buildHtml(tdiv(class="icon-container")): if href.len > 0: a(class=c, title=title, href=href) else: span(class=c, title=title) - if label.len > 0: - text " " & label - -template verifiedIcon*(user: User): untyped {.dirty.} = - if user.verifiedType != VerifiedType.none: - let lower = ($user.verifiedType).toLowerAscii() - buildHtml(tdiv(class=(&"verified-icon {lower}"))): - icon "circle", class="verified-icon-circle", title=(&"Verified {lower} account") - icon "ok", class="verified-icon-check", title=(&"Verified {lower} account") - else: - text "" + if text.len > 0: + text " " & text proc linkUser*(user: User, class=""): VNode = let @@ -50,10 +24,11 @@ proc linkUser*(user: User, class=""): VNode = buildHtml(a(href=href, class=class, title=nameText)): text nameText - if isName: - if user.protected: - text " " - icon "lock", title="Protected account" + if isName and user.verified: + icon "ok", class="verified-icon", title="Verified account" + if isName and user.protected: + text " " + icon "lock", title="Protected account" proc linkText*(text: string; class=""): VNode = let url = if "http" notin text: https & text else: text @@ -74,46 +49,48 @@ proc buttonReferer*(action, text, path: string; class=""; `method`="post"): VNod text text proc genCheckbox*(pref, label: string; state: bool): VNode = - buildHtml(label(class="pref-group checkbox-container", title=pref)): + buildHtml(label(class="pref-group checkbox-container")): text label - input(name=pref, `type`="checkbox", checked=state) + if state: input(name=pref, `type`="checkbox", checked="") + else: input(name=pref, `type`="checkbox") span(class="checkbox") -proc genInput*(pref, label, state, placeholder: string; class=""; autofocus=true): VNode = +proc genInput*(pref, label, state, placeholder: string; class=""): VNode = let p = placeholder - buildHtml(tdiv(class=("pref-group pref-input " & class), title=pref)): + buildHtml(tdiv(class=("pref-group pref-input " & class))): if label.len > 0: label(`for`=pref): text label - input(name=pref, `type`="text", placeholder=p, value=state, autofocus=(autofocus and state.len == 0)) + if state.len == 0: + input(name=pref, `type`="text", placeholder=p, value=state, autofocus="") + else: + input(name=pref, `type`="text", placeholder=p, value=state) proc genSelect*(pref, label, state: string; options: seq[string]): VNode = - buildHtml(tdiv(class="pref-group pref-input", title=pref)): + buildHtml(tdiv(class="pref-group pref-input")): label(`for`=pref): text label select(name=pref): for opt in options: - option(value=opt, selected=(opt == state)): - text opt + if opt == state: + option(value=opt, selected=""): text opt + else: + option(value=opt): text opt proc genDate*(pref, state: string): VNode = buildHtml(span(class="date-input")): input(name=pref, `type`="date", value=state) icon "calendar" -proc genNumberInput*(pref, label, state, placeholder: string; class=""; autofocus=true; min="0"): VNode = - let p = placeholder - buildHtml(tdiv(class=("pref-group pref-input " & class))): - if label.len > 0: - label(`for`=pref): text label - input(name=pref, `type`="number", placeholder=p, value=state, autofocus=(autofocus and state.len == 0), min=min, step="1") - -proc genImg*(url: string; class=""; alt=""): VNode = +proc genImg*(url: string; class=""): VNode = buildHtml(): - img(src=getPicUrl(url), class=class, alt=alt, loading="lazy") + img(src=getPicUrl(url), class=class, alt="") proc getTabClass*(query: Query; tab: QueryKind): string = - if query.kind == tab: "tab-item active" - else: "tab-item" + result = "tab-item" + if query.kind == tab: + result &= " active" proc getAvatarClass*(prefs: Prefs): string = - if prefs.squareAvatars: "avatar" - else: "avatar round" + if prefs.squareAvatars: + "avatar" + else: + "avatar round" diff --git a/src/views/rss.nimf b/src/views/rss.nimf index 4738705..cf69be1 100644 --- a/src/views/rss.nimf +++ b/src/views/rss.nimf @@ -1,231 +1,83 @@ #? stdtmpl(subsChar = '$', metaChar = '#') ## SPDX-License-Identifier: AGPL-3.0-only -#import strutils, sequtils, xmltree, strformat, options, unicode +#import strutils, xmltree, strformat, options, unicode #import ../types, ../utils, ../formatters, ../prefs -## Snowflake ID cutoff for RSS GUID format transition -## Corresponds to approximately December 14, 2025 UTC -#const guidCutoff = 2000000000000000000'i64 # #proc getTitle(tweet: Tweet; retweet: string): string = -#var prefix = "" -#if tweet.pinned: prefix = "Pinned: " -#elif retweet.len > 0: prefix = &"RT by @{retweet}: " -#elif tweet.reply.len > 0: prefix = &"R to @{tweet.reply[0]}: " +#if tweet.pinned: result = "Pinned: " +#elif retweet.len > 0: result = &"RT by @{retweet}: " +#elif tweet.reply.len > 0: result = &"R to @{tweet.reply[0]}: " #end if -#var text = strutils.splitWhitespace(stripHtml(tweet.text)).join(" ") +#var text = stripHtml(tweet.text) ##if unicode.runeLen(text) > 32: ## text = unicode.runeSubStr(text, 0, 32) & "..." ##end if -#text = xmltree.escape(text) -## article tweets' text is just the article link; the title says more -#if tweet.articlePreview.isSome and tweet.articlePreview.get().title.len > 0: -# result = prefix & xmltree.escape(tweet.articlePreview.get().title) -# return +#result &= xmltree.escape(text) +#if result.len > 0: return #end if -#if text.len > 0: -# result = prefix & text -# return -#end if -#if tweet.media.len > 0: -# result = prefix -# let firstKind = tweet.media[0].kind -# if tweet.media.anyIt(it.kind != firstKind): -# result &= "Media" -# else: -# case firstKind -# of photoMedia: result &= "Image" -# of videoMedia: result &= "Video" -# of gifMedia: result &= "Gif" -# end case -# end if -#end if -#if result.len == 0 and tweet.card.isSome: -# let card = tweet.card.get() -# if card.kind notin {hidden, unknown} and card.title.len > 0: -# result = prefix & xmltree.escape(card.title) -# end if +#if tweet.photos.len > 0: +# result &= "Image" +#elif tweet.video.isSome: +# result &= "Video" +#elif tweet.gif.isSome: +# result &= "Gif" #end if #end proc # #proc getDescription(desc: string; cfg: Config): string = -Twitter feed for: ${desc}. Generated by ${getUrlPrefix(cfg)} +Twitter feed for: ${desc}. Generated by ${cfg.hostname} #end proc # -#proc renderRssMedia(media: Media; tweet: Tweet; urlPrefix: string): string = -#case media.kind -#of photoMedia: -# let photo = media.photo - -#of videoMedia: -# let video = media.video - -
Video
- -
-#of gifMedia: -# let gif = media.gif -# let thumb = &"{urlPrefix}{getPicUrl(gif.thumb)}" -# let url = &"{urlPrefix}{getPicUrl(gif.url)}" - -#end case -#end proc -# -#proc renderRssCard(card: Card; prefs: Prefs; urlPrefix: string): string = -#let cardLink = if card.url.startsWith("/"): urlPrefix & card.url else: replaceUrls(card.url, prefs) -#let title = xmltree.escape(card.title) -
-Link
-#if cardLink.len > 0: - -#end if -#if card.image.len > 0: - -#end if -#if title.len > 0: -# if card.image.len > 0: -
-# end if -${title} -#end if -#if cardLink.len > 0: -
-#end if -#if card.text.len > 0: -

${xmltree.escape(card.text)}

-#end if -#if cardLink.len > 0: -# let destText = if card.dest.len > 0: xmltree.escape(card.dest) else: xmltree.escape(cardLink) -${destText} -#elif card.dest.len > 0: -${xmltree.escape(card.dest)} -#end if -#end proc -# -#proc renderRssArticle(article: ArticlePreview; urlPrefix: string): string = -#let link = urlPrefix & "/i/article/" & $article.tweetId -
-Article
- -#if article.coverImage.len > 0: - -#end if -#if article.title.len > 0: -# if article.coverImage.len > 0: -
-# end if -${xmltree.escape(article.title)} -#end if -
-#if article.previewText.len > 0: -

${xmltree.escape(article.previewText)}

-#end if -#end proc -# -#proc renderRssPoll(poll: Poll): string = -
-Poll -

-#for i in 0 ..< poll.options.len: -# let perc = if poll.votes > 0: poll.values[i] / poll.votes * 100 else: 0.0 -# let pct = (&"{perc:.0f}").strip(chars={'.'}) -# let line = pct & "% — " & xmltree.escape(poll.options[i]) -${line}
-#end for -#let votesStr = insertSep($poll.votes, ',') -${votesStr} votes • ${xmltree.escape(poll.status)} -

-#end proc -# -#proc getTweetsWithPinned(profile: Profile): seq[Tweets] = -#result = profile.tweets.content -#if profile.pinned.isSome and result.len > 0: -# let pinnedTweet = profile.pinned.get -# var inserted = false -# for threadIdx in 0 ..< result.len: -# if not inserted: -# for tweetIdx in 0 ..< result[threadIdx].len: -# if result[threadIdx][tweetIdx].id < pinnedTweet.id: -# result[threadIdx].insert(pinnedTweet, tweetIdx) -# inserted = true -# end if -# end for -# end if -# end for -#end if -#end proc -# -#proc renderRssTweet(tweet: Tweet; cfg: Config; prefs: Prefs): string = +#proc renderRssTweet(tweet: Tweet; cfg: Config): string = #let tweet = tweet.retweet.get(tweet) #let urlPrefix = getUrlPrefix(cfg) -#let text = replaceUrls(tweet.text, prefs, absolute=urlPrefix) -#if text.len > 0: +#let text = replaceUrls(tweet.text, defaultPrefs, absolute=urlPrefix)

${text.replace("\n", "
\n")}

-#end if -#if tweet.media.len > 0: -# for media in tweet.media: -${renderRssMedia(media, tweet, urlPrefix)} -# end for -#elif tweet.card.isSome and tweet.card.get().kind notin {hidden, unknown}: -${renderRssCard(tweet.card.get(), prefs, urlPrefix)} -#end if -#if tweet.articlePreview.isSome: -${renderRssArticle(tweet.articlePreview.get(), urlPrefix)} -#end if -#if tweet.poll.isSome: -${renderRssPoll(tweet.poll.get())} -#end if -#if tweet.note.len > 0 and not prefs.hideCommunityNotes: -

Community note: ${replaceUrls(tweet.note, prefs, absolute=urlPrefix)}

-#end if #if tweet.quote.isSome and get(tweet.quote).available: -# let quoteTweet = get(tweet.quote) -# let quoteLink = urlPrefix & getLink(quoteTweet) -
-
-${quoteTweet.user.fullname} (@${quoteTweet.user.username}) -

-${renderRssTweet(quoteTweet, cfg, prefs)} -

- -
+# let quoteLink = getLink(get(tweet.quote)) +

${cfg.hostname}${quoteLink}

+#end if +#if tweet.photos.len > 0: +# for photo in tweet.photos: + +# end for +#elif tweet.video.isSome: + +#elif tweet.gif.isSome: +# let thumb = &"{urlPrefix}{getPicUrl(get(tweet.gif).thumb)}" +# let url = &"{urlPrefix}{getPicUrl(get(tweet.gif).url)}" + +#elif tweet.card.isSome: +# let card = tweet.card.get() +# if card.image.len > 0: + +# end if #end if #end proc # -#proc renderRssTweets(tweets: seq[Tweets]; cfg: Config; prefs: Prefs; userId=""): string = +#proc renderRssTweets(tweets: seq[Tweet]; cfg: Config): string = #let urlPrefix = getUrlPrefix(cfg) #var links: seq[string] -#for thread in tweets: -# for tweet in thread: -# if userId.len > 0 and tweet.user.id != userId: continue -# end if -# -# let retweet = if tweet.retweet.isSome: tweet.user.username else: "" -# let tweet = if retweet.len > 0: tweet.retweet.get else: tweet -# let link = getLink(tweet) -# if link in links: continue -# end if -# links.add link -# let useGlobalGuid = tweet.id >= guidCutoff - - ${getTitle(tweet, retweet)} - @${tweet.user.username} - - ${getRfc822Time(tweet)} -#if useGlobalGuid: - ${tweet.id} -#else: - ${urlPrefix & link} -#end if - ${urlPrefix & link} - -# end for +#for t in tweets: +# let retweet = if t.retweet.isSome: t.user.username else: "" +# let tweet = if retweet.len > 0: t.retweet.get else: t +# let link = getLink(tweet) +# if link in links: continue +# end if +# links.add link + + ${getTitle(tweet, retweet)} + @${tweet.user.username} + + ${getRfc822Time(tweet)} + ${urlPrefix & link} + ${urlPrefix & link} + #end for #end proc # -#proc renderTimelineRss*(profile: Profile; cfg: Config; prefs: Prefs; multi=false): string = +#proc renderTimelineRss*(profile: Profile; cfg: Config; multi=false): string = #let urlPrefix = getUrlPrefix(cfg) #result = "" #let handle = (if multi: "" else: "@") & profile.user.username @@ -249,15 +101,14 @@ ${renderRssTweet(quoteTweet, cfg, prefs)} 128 128 -#let tweetsList = getTweetsWithPinned(profile) -#if tweetsList.len > 0: -${renderRssTweets(tweetsList, cfg, prefs, userId=profile.user.id)} +#if profile.tweets.content.len > 0: +${renderRssTweets(profile.tweets.content, cfg)} #end if #end proc # -#proc renderListRss*(tweets: seq[Tweets]; list: List; cfg: Config; prefs: Prefs): string = +#proc renderListRss*(tweets: seq[Tweet]; list: List; cfg: Config): string = #let link = &"{getUrlPrefix(cfg)}/i/lists/{list.id}" #result = "" @@ -266,15 +117,15 @@ ${renderRssTweets(tweetsList, cfg, prefs, userId=profile.user.id)} ${xmltree.escape(list.name)} / @${list.username} ${link} - ${getDescription(&"{list.name} by @{list.username}", cfg)} + ${getDescription(list.name & " by @" & list.username, cfg)} en-us 40 -${renderRssTweets(tweets, cfg, prefs)} +${renderRssTweets(tweets, cfg)} #end proc # -#proc renderSearchRss*(tweets: seq[Tweets]; name, param: string; cfg: Config; prefs: Prefs): string = +#proc renderSearchRss*(tweets: seq[Tweet]; name, param: string; cfg: Config): string = #let link = &"{getUrlPrefix(cfg)}/search" #let escName = xmltree.escape(name) #result = "" @@ -284,10 +135,10 @@ ${renderRssTweets(tweets, cfg, prefs)} Search results for "${escName}" ${link} - ${getDescription(&"Search \"{escName}\"", cfg)} + ${getDescription("Search \"" & escName & "\"", cfg)} en-us 40 -${renderRssTweets(tweets, cfg, prefs)} +${renderRssTweets(tweets, cfg)} #end proc diff --git a/src/views/search.nim b/src/views/search.nim index 4d4ed5e..94bbac8 100644 --- a/src/views/search.nim +++ b/src/views/search.nim @@ -10,21 +10,23 @@ const toggles = { "media": "Media", "videos": "Videos", "news": "News", + "verified": "Verified", "native_video": "Native videos", "replies": "Replies", "links": "Links", "images": "Images", + "safe": "Safe", "quote": "Quotes", - "spaces": "Spaces" + "pro_video": "Pro videos" }.toOrderedTable proc renderSearch*(): VNode = buildHtml(tdiv(class="panel-container")): tdiv(class="search-bar"): form(`method`="get", action="/search", autocomplete="off"): - hiddenField("f", "tweets") + hiddenField("f", "users") input(`type`="text", name="q", autofocus="", - placeholder="Search...", dir="auto") + placeholder="Enter username...", dir="auto") button(`type`="submit"): icon "search" proc renderProfileTabs*(query: Query; username: string): VNode = @@ -36,68 +38,37 @@ proc renderProfileTabs*(query: Query; username: string): VNode = a(href=(link & "/with_replies")): text "Tweets & Replies" li(class=query.getTabClass(media)): a(href=(link & "/media")): text "Media" - if query.fromUser.len == 1: - li(class=query.getTabClass(QueryKind.articles)): - a(href=(link & "/articles")): text "Articles" li(class=query.getTabClass(tweets)): a(href=(link & "/search")): text "Search" -proc mediaViewUrl(query: Query; view: string): string = - var q = query - q.view = view - "?" & genQueryUrl(q) - -proc renderMediaViewTabs*(query: Query): VNode = - let currentView = if query.view.len > 0: query.view else: "timeline" - func cls(view: string): string = - if currentView == view: "tab-item active" else: "tab-item" - buildHtml(ul(class="tab media-view-tabs")): - li(class=cls("timeline")): - a(href=query.mediaViewUrl("timeline")): text "Timeline" - li(class=cls("grid")): - a(href=query.mediaViewUrl("grid")): text "Grid" - li(class=cls("gallery")): - a(href=query.mediaViewUrl("gallery")): text "Gallery" - proc renderSearchTabs*(query: Query): VNode = var q = query - # the media view mode only applies to the Media tab - q.view = "" buildHtml(ul(class="tab")): - li(class=query.getTabClass(top)): - q.kind = top - a(href=("?" & genQueryUrl(q))): text "Top" li(class=query.getTabClass(tweets)): q.kind = tweets - a(href=("?" & genQueryUrl(q))): text "Latest" - li(class=query.getTabClass(media)): - q.kind = media - q.view = query.view - a(href=("?" & genQueryUrl(q))): text "Media" + a(href=("?" & genQueryUrl(q))): text "Tweets" li(class=query.getTabClass(users)): q.kind = users - q.view = "" a(href=("?" & genQueryUrl(q))): text "Users" - li(class=query.getTabClass(lists)): - q.kind = lists - a(href=("?" & genQueryUrl(q))): text "Lists" proc isPanelOpen(q: Query): bool = q.fromUser.len == 0 and (q.filters.len > 0 or q.excludes.len > 0 or - @[q.minLikes, q.until, q.since].anyIt(it.len > 0)) + @[q.near, q.until, q.since].anyIt(it.len > 0)) proc renderSearchPanel*(query: Query): VNode = let user = query.fromUser.join(",") let action = if user.len > 0: &"/{user}/search" else: "/search" buildHtml(form(`method`="get", action=action, class="search-field", autocomplete="off")): - hiddenField("f", $query.kind) + hiddenField("f", "tweets") genInput("q", "", query.text, "Enter search...", class="pref-inline") button(`type`="submit"): icon "search" - - input(id="search-panel-toggle", `type`="checkbox", checked=isPanelOpen(query)) - label(`for`="search-panel-toggle"): icon "down" - + if isPanelOpen(query): + input(id="search-panel-toggle", `type`="checkbox", checked="") + else: + input(id="search-panel-toggle", `type`="checkbox") + label(`for`="search-panel-toggle"): + icon "down" tdiv(class="search-panel"): for f in @["filter", "exclude"]: span(class="search-title"): text capitalize(f) @@ -116,58 +87,36 @@ proc renderSearchPanel*(query: Query): VNode = span(class="search-title"): text "-" genDate("until", query.until) tdiv: - span(class="search-title"): text "Minimum likes" - genNumberInput("min_faves", "", query.minLikes, "Number...", autofocus=false) + span(class="search-title"): text "Near" + genInput("near", "", query.near, placeholder="Location...") -proc renderTweetSearch*(results: Timeline; prefs: Prefs; path: string; +proc renderTweetSearch*(results: Result[Tweet]; prefs: Prefs; path: string; pinned=none(Tweet)): VNode = let query = results.query - let containerClass = - if query.fromUser.len == 0 and query.kind == QueryKind.media and - query.view == "gallery": "timeline-container media-only" - else: "timeline-container" - buildHtml(tdiv(class=containerClass)): + buildHtml(tdiv(class="timeline-container")): if query.fromUser.len > 1: tdiv(class="timeline-header"): text query.fromUser.join(" | ") if query.fromUser.len > 0: - if query.kind != QueryKind.media or query.view != "gallery": - renderProfileTabs(query, query.fromUser.join(",")) - if query.kind == QueryKind.media and query.fromUser.len == 1: - renderMediaViewTabs(query) + renderProfileTabs(query, query.fromUser.join(",")) - if query.fromUser.len == 0 or query.kind == QueryKind.tweets: + if query.fromUser.len == 0 or query.kind == tweets: tdiv(class="timeline-header"): renderSearchPanel(query) if query.fromUser.len == 0: renderSearchTabs(query) - if query.kind == QueryKind.media: - renderMediaViewTabs(query) renderTimelineTweets(results, prefs, path, pinned) -proc renderSearchForm(kind, placeholder, value: string): VNode = - buildHtml(form(`method`="get", action="/search", - class="search-field", autocomplete="off")): - hiddenField("f", kind) - genInput("q", "", value, placeholder, class="pref-inline") - button(`type`="submit"): icon "search" - proc renderUserSearch*(results: Result[User]; prefs: Prefs): VNode = buildHtml(tdiv(class="timeline-container")): tdiv(class="timeline-header"): - renderSearchForm("users", "Enter username...", results.query.text) + form(`method`="get", action="/search", class="search-field", autocomplete="off"): + hiddenField("f", "users") + genInput("q", "", results.query.text, "Enter username...", class="pref-inline") + button(`type`="submit"): icon "search" renderSearchTabs(results.query) renderTimelineUsers(results, prefs) - -proc renderListSearch*(results: Result[ListSearchResult]; prefs: Prefs; - path: string): VNode = - buildHtml(tdiv(class="timeline-container")): - tdiv(class="timeline-header"): - renderSearchForm("lists", "Enter search...", results.query.text) - - renderSearchTabs(results.query) - renderTimelineLists(results, prefs, path) diff --git a/src/views/space.nim b/src/views/space.nim deleted file mode 100644 index a5cac7b..0000000 --- a/src/views/space.nim +++ /dev/null @@ -1,86 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -import strutils, times -import karax/[karaxdsl, vdom] - -import renderutils -import ".."/[types, utils, formatters] - -proc renderParticipant(p: SpaceParticipant; role: string): VNode = - buildHtml(tdiv(class="space-participant")): - a(href=("/" & p.username)): - genImg(p.avatarUrl.replace("_normal", "_bigger")) - tdiv(class="participant-info"): - tdiv(class="participant-name"): - strong: text p.displayName - if p.isVerified: - tdiv(class="verified-icon blue"): - icon "circle", class="verified-icon-circle", title="Verified account" - icon "ok", class="verified-icon-check", title="Verified account" - if role.len > 0: - span(class="host-badge"): text role - span(class="participant-username"): text "@" & p.username - -proc renderSpace*(sp: AudioSpace; prefs: Prefs; path: string): VNode = - let - isLive = sp.state == "RUNNING" - source = if prefs.proxyVideos and sp.m3u8Url.startsWith("http"): - getVidUrl(sp.m3u8Url) else: sp.m3u8Url - stateText = - if isLive: "LIVE" - elif sp.endTime.year > 1: "Ended " & sp.endTime.format("MMM d, YYYY") - elif sp.state.len > 0: sp.state - else: "Ended" - durationMs = - if sp.startTime.year > 1 and sp.endTime.year > 1: - int((sp.endTime - sp.startTime).inMilliseconds) - else: 0 - duration = if durationMs > 0: getDuration(durationMs) else: "" - totalListeners = - if sp.totalReplayWatched > 0: sp.totalReplayWatched - else: sp.totalLiveListeners - - buildHtml(tdiv(class="space-page")): - tdiv(class="space-panel"): - tdiv(class="space-player"): - if sp.m3u8Url.len > 0 and prefs.hlsPlayback: - audio(data-url=source, data-autoload="false") - verbatim "
" - tdiv(class="overlay-circle"): span(class="overlay-triangle") - if isLive: - tdiv(class="space-live"): text "LIVE" - elif duration.len > 0: - tdiv(class="overlay-duration"): text duration - verbatim "
" - elif sp.m3u8Url.len > 0: - tdiv(class="video-overlay"): - buttonReferer "/enablehls", "Enable hls playback", path - if isLive: - tdiv(class="space-live"): text "LIVE" - elif duration.len > 0: - tdiv(class="overlay-duration"): text duration - elif sp.availableForReplay: - tdiv(class="video-overlay"): - p: text "Audio stream unavailable" - else: - tdiv(class="video-overlay"): - p: text "Replay is not available" - - tdiv(class="space-info"): - tdiv(class="space-header"): - h2(class="space-title"): text sp.title - tdiv(class="space-meta"): - if totalListeners > 0: - span(class="listener-count"): text insertSep($totalListeners, ',') & " listeners" - if isLive: - span(class="space-live"): text stateText - else: - span(class="space-state"): text stateText - - if sp.admins.len > 0 or sp.speakers.len > 0: - tdiv(class="space-participants"): - for admin in sp.admins: - let role = if admin.username == sp.creator.username: "Host" - else: "Co-host" - renderParticipant(admin, role) - for speaker in sp.speakers: - renderParticipant(speaker, "") diff --git a/src/views/status.nim b/src/views/status.nim index b16c211..71c2c67 100644 --- a/src/views/status.nim +++ b/src/views/status.nim @@ -1,5 +1,4 @@ # SPDX-License-Identifier: AGPL-3.0-only -import sequtils import karax/[karaxdsl, vdom] import ".."/[types, formatters] @@ -29,46 +28,16 @@ proc renderReplyThread(thread: Chain; prefs: Prefs; path: string): VNode = if thread.hasMore: renderMoreReplies(thread) -proc renderReplySort(sort: RankingMode): VNode = - buildHtml(tdiv(class="reply-sort")): - span(class="reply-sort-label"): text "Sort replies:" - for mode in RankingMode: - let - cls = if mode == sort: "reply-sort-option active" - else: "reply-sort-option" - label = case mode - of Relevance: "Relevant" - of Recency: "Recent" - of Likes: "Liked" - a(class=cls, href=("?sort=" & $mode & "#r")): - text label - -proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string; - tweet: Tweet = nil; sort = Relevance): VNode = +proc renderReplies*(replies: Result[Chain]; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="replies", id="r")): - var hasReplies = false - var replyCount = 0 for thread in replies.content: - if thread.content.len == 0 or thread.related: continue - hasReplies = true - replyCount += thread.content.len + if thread.content.len == 0: continue renderReplyThread(thread, prefs, path) - if hasReplies and replies.bottom.len > 0: - if tweet == nil or not replies.beginning or replyCount < tweet.stats.replies: - let extra = if sort == Relevance: "" else: "sort=" & $sort & "&" - renderMore(Query(), replies.bottom, focus="#r", extra=extra) + if replies.bottom.len > 0: + renderMore(Query(), replies.bottom, focus="#r") -proc renderRelated(replies: Result[Chain]; prefs: Prefs; path: string): VNode = - buildHtml(tdiv(class="related-tweets")): - tdiv(class="related-header"): - text "Related tweets" - for thread in replies.content: - if thread.content.len == 0 or not thread.related: continue - renderReplyThread(thread, prefs, path) - -proc renderConversation*(conv: Conversation; prefs: Prefs; path: string; - sort = Relevance): VNode = +proc renderConversation*(conv: Conversation; prefs: Prefs; path: string): VNode = let hasAfter = conv.after.content.len > 0 let threadId = conv.tweet.threadId buildHtml(tdiv(class="conversation")): @@ -101,25 +70,6 @@ proc renderConversation*(conv: Conversation; prefs: Prefs; path: string; if not conv.replies.beginning: renderNewer(Query(), getLink(conv.tweet), focus="#r") if conv.replies.content.len > 0 or conv.replies.bottom.len > 0: - renderReplySort(sort) - renderReplies(conv.replies, prefs, path, conv.tweet, sort) - - if not prefs.hideRelated: - if conv.replies.content.anyIt(it.related and it.content.len > 0): - renderRelated(conv.replies, prefs, path) + renderReplies(conv.replies, prefs, path) renderToTop(focus="#m") - -proc renderEditHistory*(edits: EditHistory; prefs: Prefs; path: string): VNode = - buildHtml(tdiv(class="edit-history")): - tdiv(class="latest-edit"): - tdiv(class="edit-history-header"): - text "Latest post" - renderTweet(edits.latest, prefs, path) - - tdiv(class="previous-edits"): - tdiv(class="edit-history-header"): - text "Version history" - for tweet in edits.history: - tdiv(class="tweet-edit"): - renderTweet(tweet, prefs, path) diff --git a/src/views/timeline.nim b/src/views/timeline.nim index 2890dd1..54cad7a 100644 --- a/src/views/timeline.nim +++ b/src/views/timeline.nim @@ -1,42 +1,16 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, strformat, algorithm, uri, options +import strutils, strformat, sequtils, algorithm, uri, options import karax/[karaxdsl, vdom] import ".."/[types, query, formatters] import tweet, renderutils -proc timelineViewClass(query: Query): string = - if query.kind != QueryKind.media: - return "timeline" - - case query.view - of "grid": "timeline media-grid-view" - of "gallery": "timeline media-gallery-view" - else: "timeline" - proc getQuery(query: Query): string = if query.kind != posts: result = genQueryUrl(query) if result.len > 0: result &= "&" -proc getSearchMaxId(results: Timeline; path: string): string = - if results.query.kind != tweets or results.content.len == 0 or - results.query.until.len == 0: - return - - let lastThread = results.content[^1] - if lastThread.len == 0 or lastThread[^1].id == 0: - return - - # 2000000 is the minimum decrement to guarantee no result overlap - var maxId = lastThread[^1].id - 2_000_000'i64 - if maxId <= 0: - maxId = lastThread[^1].id - 1 - - if maxId > 0: - return "maxid:" & $maxId - proc renderToTop*(focus="#"): VNode = buildHtml(tdiv(class="top-ref")): icon "down", href=focus @@ -50,9 +24,9 @@ proc renderNewer*(query: Query; path: string; focus=""): VNode = a(href=(p & url)): text "Load newest" -proc renderMore*(query: Query; cursor: string; focus=""; extra=""): VNode = +proc renderMore*(query: Query; cursor: string; focus=""): VNode = buildHtml(tdiv(class="show-more")): - a(href=(&"?{extra}{getQuery(query)}cursor={encodeUrl(cursor, usePlus=false)}{focus}")): + a(href=(&"?{getQuery(query)}cursor={encodeUrl(cursor, usePlus=false)}{focus}")): text "Load more" proc renderNoMore(): VNode = @@ -65,24 +39,26 @@ proc renderNoneFound(): VNode = h2(class="timeline-none"): text "No items found" -proc renderThread(thread: Tweets; prefs: Prefs; path: string; bigThumb=false): VNode = +proc renderThread(thread: seq[Tweet]; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="thread-line")): let sortedThread = thread.sortedByIt(it.id) for i, tweet in sortedThread: - # thread has a gap, display "more replies" link - if i > 0 and tweet.replyId != sortedThread[i - 1].id: - tdiv(class="timeline-item thread more-replies-thread"): - tdiv(class="more-replies"): - a(class="more-replies-text", href=getLink(tweet)): - text "more replies" - let show = i == thread.high and sortedThread[0].id != tweet.threadId let header = if tweet.pinned or tweet.retweet.isSome: "with-header " else: "" renderTweet(tweet, prefs, path, class=(header & "thread"), - index=i, last=(i == thread.high), bigThumb=bigThumb) + index=i, last=(i == thread.high), showThread=show) + +proc threadFilter(tweets: openArray[Tweet]; threads: openArray[int64]; it: Tweet): seq[Tweet] = + result = @[it] + if it.retweet.isSome or it.replyId in threads: return + for t in tweets: + if t.id == result[0].replyId: + result.insert t + elif t.replyId == result[0].id: + result.add t proc renderUser(user: User; prefs: Prefs): VNode = - buildHtml(tdiv(class="timeline-item", data-username=user.username)): + buildHtml(tdiv(class="timeline-item")): a(class="tweet-link", href=("/" & user.username)) tdiv(class="tweet-body profile-result"): tdiv(class="tweet-header"): @@ -92,7 +68,6 @@ proc renderUser(user: User; prefs: Prefs): VNode = tdiv(class="tweet-name-row"): tdiv(class="fullname-and-username"): linkUser(user, class="fullname") - verifiedIcon(user) linkUser(user, class="username") tdiv(class="tweet-content media-body", dir="auto"): @@ -114,106 +89,15 @@ proc renderTimelineUsers*(results: Result[User]; prefs: Prefs; path=""): VNode = else: renderNoMore() -proc mentionUsername(word: string): string = - # "@user" -> "user" for well-formed mentions, "" otherwise - if word.len > 1 and word[0] == '@' and - word[1 .. ^1].allCharsInSet({'A'..'Z', 'a'..'z', '0'..'9', '_'}): - word[1 .. ^1] - else: "" - -proc mentionedUser(s: string): string = - # last @mention in strings like "65 followers including @user" - let words = s.split(' ') - for i in countdown(words.high, 0): - result = mentionUsername(words[i]) - if result.len > 0: return - -proc renderMentionedText(s: string): VNode = - # linkify @mentions in plain API strings like "65 followers including @user" - let words = s.split(' ') - buildHtml(span): - for i in 0 ..< words.len: - if i > 0: text " " - let username = mentionUsername(words[i]) - if username.len > 0: - a(href=("/" & username)): text words[i] - else: - text words[i] - -proc renderListCard(r: ListSearchResult): VNode = - let listUrl = "/i/lists/" & r.list.id - buildHtml(tdiv(class="timeline-item list-result")): - a(class="tweet-link", href=listUrl) - a(class="list-result-banner", href=listUrl): - if r.list.banner.len > 0: - genImg(r.list.banner) - tdiv(class="list-result-body"): - tdiv(class="list-result-title fullname-and-username"): - a(class="list-name fullname", href=listUrl): text r.list.name - span(class="list-members"): - text &"· {insertSep($r.list.members, ',')} members" - tdiv(class="list-result-context"): - if r.followersContext.len > 0: - # the first facepile belongs to the "including @user" account - let mentioned = mentionedUser(r.followersContext) - for i in 0 ..< r.facepiles.len: - if i == 0 and mentioned.len > 0: - a(class="facepile-link", href=("/" & mentioned)): - genImg(r.facepiles[i], class="list-facepile") - else: - genImg(r.facepiles[i], class="list-facepile") - renderMentionedText(r.followersContext) - else: - if r.owner.username.len > 0: - a(class="facepile-link", href=("/" & r.owner.username)): - genImg(r.owner.getUserPic("_mini"), class="list-facepile") - else: - genImg(r.owner.getUserPic("_mini"), class="list-facepile") - linkUser(r.owner, class="fullname") - linkUser(r.owner, class="username") - if r.list.description.len > 0: - tdiv(class="list-result-description"): - text r.list.description - -proc renderTimelineLists*(results: Result[ListSearchResult]; prefs: Prefs; - path=""): VNode = - buildHtml(tdiv(class="timeline")): - if not results.beginning: - renderNewer(results.query, path) - - if results.content.len > 0: - for list in results.content: - renderListCard(list) - if results.bottom.len > 0: - renderMore(results.query, results.bottom) - renderToTop() - elif results.beginning: - renderNoneFound() - else: - renderNoMore() - -proc filterThreads(threads: seq[Tweets]; prefs: Prefs): seq[Tweets] = - var retweets: seq[int64] - for thread in threads: - if thread.len == 1: - let tweet = thread[0] - let retweetId = if tweet.retweet.isSome: get(tweet.retweet).id else: 0 - if retweetId in retweets or tweet.id in retweets or - tweet.pinned and prefs.hidePins: - continue - if retweetId != 0 and tweet.retweet.isSome: - retweets &= retweetId - result.add(thread) - -proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string; +proc renderTimelineTweets*(results: Result[Tweet]; prefs: Prefs; path: string; pinned=none(Tweet)): VNode = - buildHtml(tdiv(class=results.query.timelineViewClass)): + buildHtml(tdiv(class="timeline")): if not results.beginning: renderNewer(results.query, parseUri(path).path) if not prefs.hidePins and pinned.isSome: let tweet = get pinned - renderTweet(tweet, prefs, path) + renderTweet(tweet, prefs, path, showThread=tweet.hasThread) if results.content.len == 0: if not results.beginning: @@ -221,24 +105,26 @@ proc renderTimelineTweets*(results: Timeline; prefs: Prefs; path: string; else: renderNoneFound() else: - let filtered = filterThreads(results.content, prefs) + var + threads: seq[int64] + retweets: seq[int64] - if results.query.view == "gallery": - let bigThumb = prefs.gallerySize == "Large" - let galClass = if prefs.compactGallery: "gallery-masonry compact" else: "gallery-masonry" - tdiv(class=galClass, `data-col-size`=prefs.gallerySize.toLowerAscii): - for thread in filtered: - if thread.len == 1: renderTweet(thread[0], prefs, path, bigThumb=bigThumb) - else: renderThread(thread, prefs, path, bigThumb) - else: - for thread in filtered: - if thread.len == 1: - renderTweet(thread[0], prefs, path) - else: renderThread(thread, prefs, path) + for tweet in results.content: + let rt = if tweet.retweet.isSome: get(tweet.retweet).id else: 0 - var cursor = getSearchMaxId(results, path) - if cursor.len > 0: - renderMore(results.query, cursor) - elif results.bottom.len > 0: - renderMore(results.query, results.bottom) + if tweet.id in threads or rt in retweets or tweet.id in retweets or + tweet.pinned and prefs.hidePins: continue + + let thread = results.content.threadFilter(threads, tweet) + if thread.len < 2: + var hasThread = tweet.hasThread + if rt != 0: + retweets &= rt + hasThread = get(tweet.retweet).hasThread + renderTweet(tweet, prefs, path, showThread=hasThread) + else: + renderThread(thread, prefs, path) + threads &= thread.mapIt(it.id) + + renderMore(results.query, results.bottom) renderToTop() diff --git a/src/views/tweet.nim b/src/views/tweet.nim index 8971ab8..8b712a6 100644 --- a/src/views/tweet.nim +++ b/src/views/tweet.nim @@ -1,43 +1,32 @@ # SPDX-License-Identifier: AGPL-3.0-only -import strutils, sequtils, strformat, options, algorithm +import strutils, sequtils, strformat, options import karax/[karaxdsl, vdom, vstyles] from jester import Request import renderutils import ".."/[types, utils, formatters] +import general -const doctype = "\n" +proc getSmallPic(url: string): string = + result = url + if "?" notin url and not url.endsWith("placeholder.png"): + result &= ":small" + result = getPicUrl(result) -proc renderMiniAvatar*(user: User; prefs: Prefs): VNode = - genImg(user.getUserPic("_mini"), class=(prefs.getAvatarClass & " mini")) +proc renderMiniAvatar(user: User; prefs: Prefs): VNode = + let url = getPicUrl(user.getUserPic("_mini")) + buildHtml(): + img(class=(prefs.getAvatarClass & " mini"), src=url) -proc renderArticleCard(preview: ArticlePreview; prefs: Prefs): VNode = - let url = "/i/article/" & $preview.tweetId - buildHtml(tdiv(class="article-card card large")): - a(class="card-container", href=url): - if preview.coverImage.len > 0: - tdiv(class="card-image-container"): - tdiv(class="card-image"): - genImg(preview.coverImage) - span(class="article-card-badge"): text "Article" - tdiv(class="card-content-container"): - tdiv(class="card-content"): - h2(class="card-title"): text preview.title - if preview.previewText.len > 0: - p(class="card-description"): text preview.previewText - -proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs; - path = ""): VNode = +proc renderHeader(tweet: Tweet; retweet: string; prefs: Prefs): VNode = buildHtml(tdiv): - if pinned: - let pinnedLabel = - if "/i/communities/" in path: "Pinned by Community mods" - else: "Pinned Tweet" - tdiv(class="pinned"): - span: icon("pin", pinnedLabel) - elif retweet.len > 0: + if retweet.len > 0: tdiv(class="retweet-header"): - span: icon("retweet", retweet & " retweeted") + span: icon "retweet", retweet & " retweeted" + + if tweet.pinned: + tdiv(class="pinned"): + span: icon "pin", "Pinned Tweet" tdiv(class="tweet-header"): a(class="tweet-avatar", href=("/" & tweet.user.username)): @@ -49,42 +38,40 @@ proc renderHeader(tweet: Tweet; retweet: string; pinned: bool; prefs: Prefs; tdiv(class="tweet-name-row"): tdiv(class="fullname-and-username"): linkUser(tweet.user, class="fullname") - verifiedIcon(tweet.user) linkUser(tweet.user, class="username") span(class="tweet-date"): a(href=getLink(tweet), title=tweet.getTime): text tweet.getShortTime -proc renderAltText(altText: string): VNode = - buildHtml(p(class="alt-text")): - text "ALT " & altText +proc renderAlbum(tweet: Tweet): VNode = + let + groups = if tweet.photos.len < 3: @[tweet.photos] + else: tweet.photos.distribute(2) -proc renderPhotoAttachment(photo: Photo; bigThumb=false): VNode = - buildHtml(tdiv(class="attachment")): - let - named = "name=" in photo.url - thumb = if named: photo.url - elif bigThumb: photo.url & mediumWebp - else: photo.url & smallWebp - a(href=getOrigPicUrl(photo.url), class="still-image", target="_blank"): - genImg(thumb, alt=photo.altText) - if photo.altText.len > 0: - renderAltText(photo.altText) + buildHtml(tdiv(class="attachments")): + for i, photos in groups: + let margin = if i > 0: ".25em" else: "" + tdiv(class="gallery-row", style={marginTop: margin}): + for photo in photos: + tdiv(class="attachment image"): + let + named = "name=" in photo + orig = if named: photo else: photo & "?name=orig" + small = if named: photo else: photo & "?name=small" + a(href=getPicUrl(orig), class="still-image", target="_blank"): + genImg(small) -proc isPlaybackEnabled(prefs: Prefs; playbackType: VideoType): bool = - case playbackType +proc isPlaybackEnabled(prefs: Prefs; video: Video): bool = + case video.playbackType of mp4: prefs.mp4Playback of m3u8, vmap: prefs.hlsPlayback -proc hasMp4Url(video: Video): bool = - video.variants.anyIt(it.contentType == mp4) - -proc renderVideoDisabled(playbackType: VideoType; path=""): VNode = +proc renderVideoDisabled(video: Video; path: string): VNode = buildHtml(tdiv(class="video-overlay")): - case playbackType + case video.playbackType of mp4: - buttonReferer "/enablemp4", "Enable mp4 playback", path + p: text "mp4 playback disabled in preferences" of m3u8, vmap: buttonReferer "/enablehls", "Enable hls playback", path @@ -96,109 +83,55 @@ proc renderVideoUnavailable(video: Video): VNode = else: p: text "This media is unavailable" -proc getVideoDownloadUrl(videoData: Video): string = - let mp4Vars = videoData.variants.filterIt(it.contentType == mp4) - if mp4Vars.len == 0: return "" - let best = mp4Vars.sortedByIt(it.bitrate)[^1].url - if best.startsWith("http"): getVidUrl(best) else: best - -proc renderVideoAttachment(videoData: Video; prefs: Prefs; path=""; bigThumb=false): VNode = - let - playbackType = if not prefs.proxyVideos and videoData.hasMp4Url: mp4 - else: videoData.playbackType - thumb = if bigThumb: getMediumPic(videoData.thumb) else: getSmallPic(videoData.thumb) - - buildHtml(tdiv(class="attachment")): - if not videoData.available: - img(src=thumb, loading="lazy") - renderVideoUnavailable(videoData) - elif not prefs.isPlaybackEnabled(playbackType): - img(src=thumb, loading="lazy") - renderVideoDisabled(playbackType, path) - else: - let - vars = videoData.variants.filterIt(it.contentType == playbackType) - vidUrl = vars.sortedByIt(it.resolution)[^1].url - source = if prefs.proxyVideos and vidUrl.startsWith("http"): - getVidUrl(vidUrl) else: vidUrl - case playbackType - of mp4: - video(poster=thumb, controls="", muted=prefs.muteVideos): - source(src=source, `type`="video/mp4") - of m3u8, vmap: - video(poster=thumb, data-url=source, data-autoload="false", muted=prefs.muteVideos) - verbatim "
" - tdiv(class="overlay-circle"): span(class="overlay-triangle") - if videoData.durationMs > 0: - tdiv(class="overlay-duration"): text getDuration(videoData) - verbatim "
" - if videoData.available: - let dlUrl = getVideoDownloadUrl(videoData) - if dlUrl.len > 0: - a(class="video-download", href=dlUrl, download="", - title="Download video"): icon "download-alt" - -proc renderVideo*(video: Video; prefs: Prefs; path: string; bigThumb=false): VNode = - let hasCardContent = video.description.len > 0 or video.title.len > 0 +proc renderVideo*(video: Video; prefs: Prefs; path: string): VNode = + let container = + if video.description.len > 0 or video.title.len > 0: " card-container" + else: "" buildHtml(tdiv(class="attachments card")): - tdiv(class=("gallery-video" & (if hasCardContent: " card-container" else: ""))): - renderVideoAttachment(video, prefs, path, bigThumb) - if hasCardContent: + tdiv(class="gallery-video" & container): + tdiv(class="attachment video-container"): + let thumb = getSmallPic(video.thumb) + if not video.available: + img(src=thumb) + renderVideoUnavailable(video) + elif not prefs.isPlaybackEnabled(video): + img(src=thumb) + renderVideoDisabled(video, path) + else: + let vid = video.variants.filterIt(it.contentType == video.playbackType) + let source = getVidUrl(vid[0].url) + case video.playbackType + of mp4: + if prefs.muteVideos: + video(poster=thumb, controls="", muted=""): + source(src=source, `type`="video/mp4") + else: + video(poster=thumb, controls=""): + source(src=source, `type`="video/mp4") + of m3u8, vmap: + video(poster=thumb, data-url=source, data-autoload="false") + verbatim "
" + tdiv(class="overlay-circle"): span(class="overlay-triangle") + verbatim "
" + if container.len > 0: tdiv(class="card-content"): h2(class="card-title"): text video.title if video.description.len > 0: p(class="card-description"): text video.description -proc renderGifAttachment(gif: Gif; prefs: Prefs; path=""): VNode = - let thumb = getSmallPic(gif.thumb) - - buildHtml(tdiv(class="attachment")): - if not prefs.mp4Playback: - img(src=thumb, loading="lazy") - renderVideoDisabled(mp4, path) - elif prefs.autoplayGifs: - video(class="gif", poster=thumb, autoplay="", muted="", loop=""): - source(src=getPicUrl(gif.url), `type`="video/mp4") - else: - video(class="gif", poster=thumb, controls="", muted="", loop=""): - source(src=getPicUrl(gif.url), `type`="video/mp4") - if gif.altText.len > 0: - renderAltText(gif.altText) - -proc renderGif(gif: Gif; prefs: Prefs; path=""): VNode = +proc renderGif(gif: Gif; prefs: Prefs): VNode = buildHtml(tdiv(class="attachments media-gif")): - renderGifAttachment(gif, prefs, path) - -proc renderMedia(media: seq[Media]; prefs: Prefs; path: string; bigThumb=false): VNode = - if media.len == 0: - return nil - - if media.len == 1: - let item = media[0] - if item.kind == videoMedia: - return renderVideo(item.video, prefs, path, bigThumb) - if item.kind == gifMedia: - return renderGif(item.gif, prefs, path) - - let - groups = if media.len < 3: @[media] - else: media.distribute(2) - - buildHtml(tdiv(class="attachments")): - for i, mediaGroup in groups: - let margin = if i > 0: ".25em" else: "" - let rowClass = "gallery-row" & - (if mediaGroup.allIt(it.kind == photoMedia): "" else: " mixed-row") - tdiv(class=rowClass, style={marginTop: margin}): - for mediaItem in mediaGroup: - case mediaItem.kind - of photoMedia: - renderPhotoAttachment(mediaItem.photo, bigThumb) - of videoMedia: - renderVideoAttachment(mediaItem.video, prefs, path, bigThumb) - of gifMedia: - renderGifAttachment(mediaItem.gif, prefs, path) + tdiv(class="gallery-gif", style={maxHeight: "unset"}): + tdiv(class="attachment"): + let thumb = getSmallPic(gif.thumb) + let url = getPicUrl(gif.url) + if prefs.autoplayGifs: + video(class="gif", poster=thumb, controls="", autoplay="", muted="", loop=""): + source(src=url, `type`="video/mp4") + else: + video(class="gif", poster=thumb, controls="", muted="", loop=""): + source(src=url, `type`="video/mp4") proc renderPoll(poll: Poll): VNode = buildHtml(tdiv(class="poll")): @@ -213,12 +146,12 @@ proc renderPoll(poll: Poll): VNode = span(class="poll-choice-value"): text percStr span(class="poll-choice-option"): text poll.options[i] span(class="poll-info"): - text &"{insertSep($poll.votes, ',')} votes • {poll.status}" + text insertSep($poll.votes, ',') & " votes • " & poll.status proc renderCardImage(card: Card): VNode = buildHtml(tdiv(class="card-image-container")): tdiv(class="card-image"): - genImg(card.image) + img(src=getPicUrl(card.image), alt="") if card.kind == player: tdiv(class="card-overlay"): tdiv(class="overlay-circle"): @@ -254,12 +187,14 @@ func formatStat(stat: int): string = if stat > 0: insertSep($stat, ',') else: "" -proc renderStats*(stats: TweetStats): VNode = +proc renderStats(stats: TweetStats; views: string): VNode = buildHtml(tdiv(class="tweet-stats")): span(class="tweet-stat"): icon "comment", formatStat(stats.replies) span(class="tweet-stat"): icon "retweet", formatStat(stats.retweets) + span(class="tweet-stat"): icon "quote", formatStat(stats.quotes) span(class="tweet-stat"): icon "heart", formatStat(stats.likes) - span(class="tweet-stat"): icon "views", formatStat(stats.views) + if views.len > 0: + span(class="tweet-stat"): icon "play", insertSep(views, ',') proc renderReply(tweet: Tweet): VNode = buildHtml(tdiv(class="replying-to")): @@ -268,12 +203,12 @@ proc renderReply(tweet: Tweet): VNode = if i > 0: text " " a(href=("/" & u)): text "@" & u -proc renderAttribution(user: User; prefs: Prefs; link = ""): VNode = - let href = if link.len > 0: link else: "/" & user.username - buildHtml(a(class="attribution", href=href)): +proc renderAttribution(user: User; prefs: Prefs): VNode = + buildHtml(a(class="attribution", href=("/" & user.username))): renderMiniAvatar(user, prefs) strong: text user.fullname - verifiedIcon(user) + if user.verified: + icon "ok", class="verified-icon", title="Verified account" proc renderMediaTags(tags: seq[User]): VNode = buildHtml(tdiv(class="media-tag-block")): @@ -284,28 +219,19 @@ proc renderMediaTags(tags: seq[User]): VNode = if i < tags.high: text ", " -proc renderLatestPost(username: string; id: int64): VNode = - buildHtml(tdiv(class="latest-post-version")): - text "There's a new version of this post. " - a(href=getLink(id, username)): - text "See the latest post" - -proc renderCommunityNote(note: string; prefs: Prefs): VNode = - buildHtml(tdiv(class="community-note")): - tdiv(class="community-note-header"): - icon "group" - span: text "Community note" - tdiv(class="community-note-text", dir="auto"): - verbatim replaceUrls(note, prefs) - proc renderQuoteMedia(quote: Tweet; prefs: Prefs; path: string): VNode = buildHtml(tdiv(class="quote-media-container")): - renderMedia(quote.media, prefs, path) + if quote.photos.len > 0: + renderAlbum(quote) + elif quote.video.isSome: + renderVideo(quote.video.get(), prefs, path) + elif quote.gif.isSome: + renderGif(quote.gif.get(), prefs) proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = if not quote.available: return buildHtml(tdiv(class="quote unavailable")): - a(class="unavailable-quote", href=getLink(quote, focus=false)): + tdiv(class="unavailable-quote"): if quote.tombstone.len > 0: text quote.tombstone elif quote.text.len > 0: @@ -320,7 +246,6 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = tdiv(class="fullname-and-username"): renderMiniAvatar(quote.user, prefs) linkUser(quote.user, class="fullname") - verifiedIcon(quote.user) linkUser(quote.user, class="username") span(class="tweet-date"): @@ -334,31 +259,12 @@ proc renderQuote(quote: Tweet; prefs: Prefs; path: string): VNode = tdiv(class="quote-text", dir="auto"): verbatim replaceUrls(quote.text, prefs) - if quote.media.len > 0: - renderQuoteMedia(quote, prefs, path) - - if quote.articlePreview.isSome: - renderArticleCard(quote.articlePreview.get(), prefs) - - if quote.note.len > 0 and not prefs.hideCommunityNotes: - renderCommunityNote(quote.note, prefs) - if quote.hasThread: a(class="show-thread", href=getLink(quote)): text "Show this thread" - if quote.history.len > 0 and quote.id != max(quote.history): - tdiv(class="quote-latest"): - text "There's a new version of this post" - -proc renderDisclosures*(tweet: Tweet): VNode = - buildHtml(tdiv(class="disclosures")): - if tweet.isAI: - span(data-disclosure="ai"): - icon "attention-circled", "Made with AI" - if tweet.isAd: - span(data-disclosure="ad"): - icon "attention-circled", "Paid partnership (ad)" + if quote.photos.len > 0 or quote.video.isSome or quote.gif.isSome: + renderQuoteMedia(quote, prefs, path) proc renderLocation*(tweet: Tweet): string = let (place, url) = tweet.getLocation() @@ -372,15 +278,14 @@ proc renderLocation*(tweet: Tweet): string = return $node proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; - last=false; mainTweet=false; afterTweet=false; - bigThumb=false): VNode = + last=false; showThread=false; mainTweet=false; afterTweet=false): VNode = var divClass = class if index == -1 or last: divClass = "thread-last " & class if not tweet.available: - return buildHtml(tdiv(class=divClass & "unavailable timeline-item", data-username=tweet.user.username)): - a(class="unavailable-box", href=getLink(tweet)): + return buildHtml(tdiv(class=divClass & "unavailable timeline-item")): + tdiv(class="unavailable-box"): if tweet.tombstone.len > 0: text tweet.tombstone elif tweet.text.len > 0: @@ -391,25 +296,23 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; if tweet.quote.isSome: renderQuote(tweet.quote.get(), prefs, path) - let - fullTweet = tweet - pinned = tweet.pinned - + let fullTweet = tweet var retweet: string var tweet = fullTweet if tweet.retweet.isSome: tweet = tweet.retweet.get retweet = fullTweet.user.fullname - buildHtml(tdiv(class=("timeline-item " & divClass), data-username=tweet.user.username)): + buildHtml(tdiv(class=("timeline-item " & divClass))): if not mainTweet: a(class="tweet-link", href=getLink(tweet)) tdiv(class="tweet-body"): - renderHeader(tweet, retweet, pinned, prefs, path) + var views = "" + renderHeader(tweet, retweet, prefs) if not afterTweet and index == 0 and tweet.reply.len > 0 and - (tweet.reply.len > 1 or tweet.reply[0] != tweet.user.username or pinned): + (tweet.reply.len > 1 or tweet.reply[0] != tweet.user.username): renderReply(tweet) var tweetClass = "tweet-content media-body" @@ -420,16 +323,19 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; verbatim replaceUrls(tweet.text, prefs) & renderLocation(tweet) if tweet.attribution.isSome: - renderAttribution(tweet.attribution.get(), prefs, tweet.attributionLink) + renderAttribution(tweet.attribution.get(), prefs) - if tweet.card.isSome and tweet.card.get().kind != hidden: + if tweet.card.isSome: renderCard(tweet.card.get(), prefs, path) - if tweet.articlePreview.isSome: - renderArticleCard(tweet.articlePreview.get(), prefs) - - if tweet.media.len > 0: - renderMedia(tweet.media, prefs, path, bigThumb) + if tweet.photos.len > 0: + renderAlbum(tweet) + elif tweet.video.isSome: + renderVideo(tweet.video.get(), prefs, path) + views = tweet.video.get().views + elif tweet.gif.isSome: + renderGif(tweet.gif.get(), prefs) + views = "GIF" if tweet.poll.isSome: renderPoll(tweet.poll.get()) @@ -437,29 +343,20 @@ proc renderTweet*(tweet: Tweet; prefs: Prefs; path: string; class=""; index=0; if tweet.quote.isSome: renderQuote(tweet.quote.get(), prefs, path) - if tweet.note.len > 0 and not prefs.hideCommunityNotes: - renderCommunityNote(tweet.note, prefs) - - if tweet.isAI or tweet.isAd: - renderDisclosures(tweet) - - let - hasEdits = tweet.history.len > 1 - isLatest = hasEdits and tweet.id == max(tweet.history) - if mainTweet: - p(class="tweet-published"): - if hasEdits and isLatest: - a(href=(getLink(tweet, focus=false) & "/history")): - text &"Last edited {getTime(tweet)}" - else: - text &"{getTime(tweet)}" - - if hasEdits and not isLatest: - renderLatestPost(tweet.user.username, max(tweet.history)) + p(class="tweet-published"): text getTime(tweet) if tweet.mediaTags.len > 0: renderMediaTags(tweet.mediaTags) if not prefs.hideTweetStats: - renderStats(tweet.stats) + renderStats(tweet.stats, views) + + if showThread: + a(class="show-thread", href=("/i/status/" & $tweet.threadId)): + text "Show this thread" + +proc renderTweetEmbed*(tweet: Tweet; path: string; prefs: Prefs; cfg: Config; req: Request): VNode = + buildHtml(tdiv(class="tweet-embed")): + renderHead(prefs, cfg, req) + renderTweet(tweet, prefs, path, mainTweet=true) diff --git a/tests/base.py b/tests/base.py index 841094d..010dfbb 100644 --- a/tests/base.py +++ b/tests/base.py @@ -54,18 +54,6 @@ class Timeline(object): none = '.timeline-none' protected = '.timeline-protected' photo_rail = '.photo-rail-grid' - media_view_tabs = '.media-view-tabs' - media_view_timeline = '.media-view-tabs a[href*="view=timeline"]' - media_view_grid = '.media-view-tabs a[href*="view=grid"]' - media_view_gallery = '.media-view-tabs a[href*="view=gallery"]' - media_view_active = '.media-view-tabs .tab-item.active a' - grid_view = '.timeline.media-grid-view' - gallery_view = '.timeline.media-gallery-view' - - -class Search(object): - tab_item = '.tab .tab-item' - tab_active = '.tab .tab-item.active a' class Conversation(object): @@ -76,8 +64,6 @@ class Conversation(object): thread = '.reply' tweet = '.timeline-item' tweet_text = '.tweet-content' - reply_sort = '.reply-sort' - reply_sort_active = '.reply-sort-option.active' class Poll(object): @@ -93,7 +79,7 @@ class Media(object): row = '.gallery-row' image = '.still-image' video = '.gallery-video' - gif = '.media-gif' + gif = '.gallery-gif' class BaseTestCase(BaseCase): diff --git a/tests/conftest.py b/tests/conftest.py deleted file mode 100644 index 3d87c74..0000000 --- a/tests/conftest.py +++ /dev/null @@ -1,4 +0,0 @@ -from seleniumbase.config import settings - -settings.SKIP_JS_WAITS = True -settings.WAIT_FOR_RSC_ON_PAGE_LOADS = False diff --git a/tests/poetry.lock b/tests/poetry.lock deleted file mode 100644 index d13bfe0..0000000 --- a/tests/poetry.lock +++ /dev/null @@ -1,1716 +0,0 @@ -# This file is automatically @generated by Poetry 2.2.1 and should not be changed by hand. - -[[package]] -name = "async-generator" -version = "1.10" -description = "Async generators and context managers for Python 3.5+" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "async_generator-1.10-py3-none-any.whl", hash = "sha256:01c7bf666359b4967d2cda0000cc2e4af16a0ae098cbffcb8472fb9e8ad6585b"}, - {file = "async_generator-1.10.tar.gz", hash = "sha256:6ebb3d106c12920aaae42ccb6f787ef5eefdcdd166ea3d628fa8476abe712144"}, -] - -[[package]] -name = "attrs" -version = "25.4.0" -description = "Classes Without Boilerplate" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "attrs-25.4.0-py3-none-any.whl", hash = "sha256:adcf7e2a1fb3b36ac48d97835bb6d8ade15b8dcce26aba8bf1d14847b57a3373"}, - {file = "attrs-25.4.0.tar.gz", hash = "sha256:16d5969b87f0859ef33a48b35d55ac1be6e42ae49d5e853b597db70c35c57e11"}, -] - -[[package]] -name = "beautifulsoup4" -version = "4.14.3" -description = "Screen-scraping library" -optional = false -python-versions = ">=3.7.0" -groups = ["main"] -files = [ - {file = "beautifulsoup4-4.14.3-py3-none-any.whl", hash = "sha256:0918bfe44902e6ad8d57732ba310582e98da931428d231a5ecb9e7c703a735bb"}, - {file = "beautifulsoup4-4.14.3.tar.gz", hash = "sha256:6292b1c5186d356bba669ef9f7f051757099565ad9ada5dd630bd9de5fa7fb86"}, -] - -[package.dependencies] -soupsieve = ">=1.6.1" -typing-extensions = ">=4.0.0" - -[package.extras] -cchardet = ["cchardet"] -chardet = ["chardet"] -charset-normalizer = ["charset-normalizer"] -html5lib = ["html5lib"] -lxml = ["lxml"] - -[[package]] -name = "behave" -version = "1.2.6" -description = "behave is behaviour-driven development, Python style" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" -groups = ["main"] -files = [ - {file = "behave-1.2.6-py2.py3-none-any.whl", hash = "sha256:ebda1a6c9e5bfe95c5f9f0a2794e01c7098b3dde86c10a95d8621c5907ff6f1c"}, - {file = "behave-1.2.6.tar.gz", hash = "sha256:b9662327aa53294c1351b0a9c369093ccec1d21026f050c3bd9b3e5cccf81a86"}, -] - -[package.dependencies] -parse = ">=1.8.2" -parse-type = ">=0.4.2" -six = ">=1.11" - -[package.extras] -develop = ["coverage", "invoke (>=0.21.0)", "modernize (>=0.5)", "path.py (>=8.1.2)", "pathlib", "pycmd", "pylint", "pytest (>=3.0)", "pytest-cov", "tox"] -docs = ["sphinx (>=1.6)", "sphinx-bootstrap-theme (>=0.6)"] - -[[package]] -name = "certifi" -version = "2026.1.4" -description = "Python package for providing Mozilla's CA Bundle." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "certifi-2026.1.4-py3-none-any.whl", hash = "sha256:9943707519e4add1115f44c2bc244f782c0249876bf51b6599fee1ffbedd685c"}, - {file = "certifi-2026.1.4.tar.gz", hash = "sha256:ac726dd470482006e014ad384921ed6438c457018f4b3d204aea4281258b2120"}, -] - -[[package]] -name = "cffi" -version = "2.0.0" -description = "Foreign Function Interface for Python calling C code." -optional = false -python-versions = ">=3.9" -groups = ["main"] -markers = "os_name == \"nt\" and implementation_name != \"pypy\"" -files = [ - {file = "cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44"}, - {file = "cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4"}, - {file = "cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5"}, - {file = "cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb"}, - {file = "cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a"}, - {file = "cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe"}, - {file = "cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664"}, - {file = "cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414"}, - {file = "cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743"}, - {file = "cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5"}, - {file = "cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5"}, - {file = "cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d"}, - {file = "cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037"}, - {file = "cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94"}, - {file = "cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187"}, - {file = "cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18"}, - {file = "cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5"}, - {file = "cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb"}, - {file = "cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3"}, - {file = "cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c"}, - {file = "cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b"}, - {file = "cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27"}, - {file = "cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75"}, - {file = "cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5"}, - {file = "cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef"}, - {file = "cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205"}, - {file = "cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1"}, - {file = "cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f"}, - {file = "cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25"}, - {file = "cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9"}, - {file = "cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc"}, - {file = "cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512"}, - {file = "cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4"}, - {file = "cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e"}, - {file = "cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6"}, - {file = "cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:fe562eb1a64e67dd297ccc4f5addea2501664954f2692b69a76449ec7913ecbf"}, - {file = "cffi-2.0.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:de8dad4425a6ca6e4e5e297b27b5c824ecc7581910bf9aee86cb6835e6812aa7"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:4647afc2f90d1ddd33441e5b0e85b16b12ddec4fca55f0d9671fef036ecca27c"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3f4d46d8b35698056ec29bca21546e1551a205058ae1a181d871e278b0b28165"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:e6e73b9e02893c764e7e8d5bb5ce277f1a009cd5243f8228f75f842bf937c534"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:cb527a79772e5ef98fb1d700678fe031e353e765d1ca2d409c92263c6d43e09f"}, - {file = "cffi-2.0.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61d028e90346df14fedc3d1e5441df818d095f3b87d286825dfcbd6459b7ef63"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:0f6084a0ea23d05d20c3edcda20c3d006f9b6f3fefeac38f59262e10cef47ee2"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:1cd13c99ce269b3ed80b417dcd591415d3372bcac067009b6e0f59c7d4015e65"}, - {file = "cffi-2.0.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:89472c9762729b5ae1ad974b777416bfda4ac5642423fa93bd57a09204712322"}, - {file = "cffi-2.0.0-cp39-cp39-win32.whl", hash = "sha256:2081580ebb843f759b9f617314a24ed5738c51d2aee65d31e02f6f7a2b97707a"}, - {file = "cffi-2.0.0-cp39-cp39-win_amd64.whl", hash = "sha256:b882b3df248017dba09d6b16defe9b5c407fe32fc7c65a9c69798e6175601be9"}, - {file = "cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529"}, -] - -[package.dependencies] -pycparser = {version = "*", markers = "implementation_name != \"PyPy\""} - -[[package]] -name = "chardet" -version = "5.2.0" -description = "Universal encoding detector for Python 3" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "chardet-5.2.0-py3-none-any.whl", hash = "sha256:e1cf59446890a00105fe7b7912492ea04b6e6f06d4b742b2c788469e34c82970"}, - {file = "chardet-5.2.0.tar.gz", hash = "sha256:1b3b6ff479a8c414bc3fa2c0852995695c4a026dcd6d0633b2dd092ca39c1cf7"}, -] - -[[package]] -name = "charset-normalizer" -version = "3.4.4" -description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "charset_normalizer-3.4.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e824f1492727fa856dd6eda4f7cee25f8518a12f3c4a56a74e8095695089cf6d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4bd5d4137d500351a30687c2d3971758aac9a19208fc110ccb9d7188fbe709e8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:027f6de494925c0ab2a55eab46ae5129951638a49a34d87f4c3eda90f696b4ad"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f820802628d2694cb7e56db99213f930856014862f3fd943d290ea8438d07ca8"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:798d75d81754988d2565bff1b97ba5a44411867c0cf32b77a7e8f8d84796b10d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d1bb833febdff5c8927f922386db610b49db6e0d4f4ee29601d71e7c2694313"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9cd98cdc06614a2f768d2b7286d66805f94c48cde050acdbbb7db2600ab3197e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:077fbb858e903c73f6c9db43374fd213b0b6a778106bc7032446a8e8b5b38b93"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:244bfb999c71b35de57821b8ea746b24e863398194a4014e4c76adc2bbdfeff0"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:64b55f9dce520635f018f907ff1b0df1fdc31f2795a922fb49dd14fbcdf48c84"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:faa3a41b2b66b6e50f84ae4a68c64fcd0c44355741c6374813a800cd6695db9e"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:6515f3182dbe4ea06ced2d9e8666d97b46ef4c75e326b79bb624110f122551db"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cc00f04ed596e9dc0da42ed17ac5e596c6ccba999ba6bd92b0e0aef2f170f2d6"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win32.whl", hash = "sha256:f34be2938726fc13801220747472850852fe6b1ea75869a048d6f896838c896f"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_amd64.whl", hash = "sha256:a61900df84c667873b292c3de315a786dd8dac506704dea57bc957bd31e22c7d"}, - {file = "charset_normalizer-3.4.4-cp310-cp310-win_arm64.whl", hash = "sha256:cead0978fc57397645f12578bfd2d5ea9138ea0fac82b2f63f7f7c6877986a69"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6e1fcf0720908f200cd21aa4e6750a48ff6ce4afe7ff5a79a90d5ed8a08296f8"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f819d5fe9234f9f82d75bdfa9aef3a3d72c4d24a6e57aeaebba32a704553aa0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a59cb51917aa591b1c4e6a43c132f0cdc3c76dbad6155df4e28ee626cc77a0a3"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8ef3c867360f88ac904fd3f5e1f902f13307af9052646963ee08ff4f131adafc"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d9e45d7faa48ee908174d8fe84854479ef838fc6a705c9315372eacbc2f02897"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:840c25fb618a231545cbab0564a799f101b63b9901f2569faecd6b222ac72381"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ca5862d5b3928c4940729dacc329aa9102900382fea192fc5e52eb69d6093815"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d9c7f57c3d666a53421049053eaacdd14bbd0a528e2186fcb2e672effd053bb0"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:277e970e750505ed74c832b4bf75dac7476262ee2a013f5574dd49075879e161"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:31fd66405eaf47bb62e8cd575dc621c56c668f27d46a61d975a249930dd5e2a4"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:0d3d8f15c07f86e9ff82319b3d9ef6f4bf907608f53fe9d92b28ea9ae3d1fd89"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:9f7fcd74d410a36883701fafa2482a6af2ff5ba96b9a620e9e0721e28ead5569"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ebf3e58c7ec8a8bed6d66a75d7fb37b55e5015b03ceae72a8e7c74495551e224"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016"}, - {file = "charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525"}, - {file = "charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e1f185f86a6f3403aa2420e815904c67b2f9ebc443f045edd0de921108345794"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b39f987ae8ccdf0d2642338faf2abb1862340facc796048b604ef14919e55ed"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3162d5d8ce1bb98dd51af660f2121c55d0fa541b46dff7bb9b9f86ea1d87de72"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:81d5eb2a312700f4ecaa977a8235b634ce853200e828fbadf3a9c50bab278328"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5bd2293095d766545ec1a8f612559f6b40abc0eb18bb2f5d1171872d34036ede"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a8a8b89589086a25749f471e6a900d3f662d1d3b6e2e59dcecf787b1cc3a1894"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc7637e2f80d8530ee4a78e878bce464f70087ce73cf7c1caf142416923b98f1"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f8bf04158c6b607d747e93949aa60618b61312fe647a6369f88ce2ff16043490"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:554af85e960429cf30784dd47447d5125aaa3b99a6f0683589dbd27e2f45da44"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:74018750915ee7ad843a774364e13a3db91682f26142baddf775342c3f5b1133"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c0463276121fdee9c49b98908b3a89c39be45d86d1dbaa22957e38f6321d4ce3"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:362d61fd13843997c1c446760ef36f240cf81d3ebf74ac62652aebaf7838561e"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a26f18905b8dd5d685d6d07b0cdf98a79f3c7a918906af7cc143ea2e164c8bc"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win32.whl", hash = "sha256:9b35f4c90079ff2e2edc5b26c0c77925e5d2d255c42c74fdb70fb49b172726ac"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:b435cba5f4f750aa6c0a0d92c541fb79f69a387c91e61f1795227e4ed9cece14"}, - {file = "charset_normalizer-3.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:542d2cee80be6f80247095cc36c418f7bddd14f4a6de45af91dfad36d817bba2"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:da3326d9e65ef63a817ecbcc0df6e94463713b754fe293eaa03da99befb9a5bd"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8af65f14dc14a79b924524b1e7fffe304517b2bff5a58bf64f30b98bbc5079eb"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74664978bb272435107de04e36db5a9735e78232b85b77d45cfb38f758efd33e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:752944c7ffbfdd10c074dc58ec2d5a8a4cd9493b314d367c14d24c17684ddd14"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d1f13550535ad8cff21b8d757a3257963e951d96e20ec82ab44bc64aeb62a191"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecaae4149d99b1c9e7b88bb03e3221956f68fd6d50be2ef061b2381b61d20838"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb6254dc36b47a990e59e1068afacdcd02958bdcce30bb50cc1700a8b9d624a6"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:c8ae8a0f02f57a6e61203a31428fa1d677cbe50c93622b4149d5c0f319c1d19e"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:47cc91b2f4dd2833fddaedd2893006b0106129d4b94fdb6af1f4ce5a9965577c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:82004af6c302b5d3ab2cfc4cc5f29db16123b1a8417f2e25f9066f91d4411090"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:2b7d8f6c26245217bd2ad053761201e9f9680f8ce52f0fcd8d0755aeae5b2152"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:799a7a5e4fb2d5898c60b640fd4981d6a25f1c11790935a44ce38c54e985f828"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99ae2cffebb06e6c22bdc25801d7b30f503cc87dbd283479e7b606f70aff57ec"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win32.whl", hash = "sha256:f9d332f8c2a2fcbffe1378594431458ddbef721c1769d78e2cbc06280d8155f9"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:8a6562c3700cce886c5be75ade4a5db4214fda19fede41d9792d100288d8f94c"}, - {file = "charset_normalizer-3.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:de00632ca48df9daf77a2c65a484531649261ec9f25489917f09e455cb09ddb2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:ce8a0633f41a967713a59c4139d29110c07e826d131a316b50ce11b1d79b4f84"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaabd426fe94daf8fd157c32e571c85cb12e66692f15516a83a03264b08d06c3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c4ef880e27901b6cc782f1b95f82da9313c0eb95c3af699103088fa0ac3ce9ac"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2aaba3b0819274cc41757a1da876f810a3e4d7b6eb25699253a4effef9e8e4af"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:778d2e08eda00f4256d7f672ca9fef386071c9202f5e4607920b86d7803387f2"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f155a433c2ec037d4e8df17d18922c3a0d9b3232a396690f17175d2946f0218d"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a8bf8d0f749c5757af2142fe7903a9df1d2e8aa3841559b2bad34b08d0e2bcf3"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:194f08cbb32dc406d6e1aea671a68be0823673db2832b38405deba2fb0d88f63"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_armv7l.whl", hash = "sha256:6aee717dcfead04c6eb1ce3bd29ac1e22663cdea57f943c87d1eab9a025438d7"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:cd4b7ca9984e5e7985c12bc60a6f173f3c958eae74f3ef6624bb6b26e2abbae4"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_riscv64.whl", hash = "sha256:b7cf1017d601aa35e6bb650b6ad28652c9cd78ee6caff19f3c28d03e1c80acbf"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:e912091979546adf63357d7e2ccff9b44f026c075aeaf25a52d0e95ad2281074"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:5cb4d72eea50c8868f5288b7f7f33ed276118325c1dfd3957089f6b519e1382a"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win32.whl", hash = "sha256:837c2ce8c5a65a2035be9b3569c684358dfbf109fd3b6969630a87535495ceaa"}, - {file = "charset_normalizer-3.4.4-cp38-cp38-win_amd64.whl", hash = "sha256:44c2a8734b333e0578090c4cd6b16f275e07aa6614ca8715e6c038e865e70576"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:a9768c477b9d7bd54bc0c86dbaebdec6f03306675526c9927c0e8a04e8f94af9"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bee1e43c28aa63cb16e5c14e582580546b08e535299b8b6158a7c9c768a1f3d"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:fd44c878ea55ba351104cb93cc85e74916eb8fa440ca7903e57575e97394f608"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f04b14ffe5fdc8c4933862d8306109a2c51e0704acfa35d51598eb45a1e89fc"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:cd09d08005f958f370f539f186d10aec3377d55b9eeb0d796025d4886119d76e"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4fe7859a4e3e8457458e2ff592f15ccb02f3da787fcd31e0183879c3ad4692a1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa09f53c465e532f4d3db095e0c55b615f010ad81803d383195b6b5ca6cbf5f3"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:7fa17817dc5625de8a027cb8b26d9fefa3ea28c8253929b8d6649e705d2835b6"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_armv7l.whl", hash = "sha256:5947809c8a2417be3267efc979c47d76a079758166f7d43ef5ae8e9f92751f88"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:4902828217069c3c5c71094537a8e623f5d097858ac6ca8252f7b4d10b7560f1"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:7c308f7e26e4363d79df40ca5b2be1c6ba9f02bdbccfed5abddb7859a6ce72cf"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:2c9d3c380143a1fedbff95a312aa798578371eb29da42106a29019368a475318"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:cb01158d8b88ee68f15949894ccc6712278243d95f344770fa7593fa2d94410c"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win32.whl", hash = "sha256:2677acec1a2f8ef614c6888b5b4ae4060cc184174a938ed4e8ef690e15d3e505"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_amd64.whl", hash = "sha256:f8e160feb2aed042cd657a72acc0b481212ed28b1b9a95c0cee1621b524e1966"}, - {file = "charset_normalizer-3.4.4-cp39-cp39-win_arm64.whl", hash = "sha256:b5d84d37db046c5ca74ee7bb47dd6cbc13f80665fdde3e8040bdd3fb015ecb50"}, - {file = "charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f"}, - {file = "charset_normalizer-3.4.4.tar.gz", hash = "sha256:94537985111c35f28720e43603b8e7b43a6ecfb2ce1d3058bbe955b73404e21a"}, -] - -[[package]] -name = "colorama" -version = "0.4.6" -description = "Cross-platform colored terminal text." -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" -groups = ["main"] -files = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] - -[[package]] -name = "cssselect" -version = "1.4.0" -description = "cssselect parses CSS3 Selectors and translates them to XPath 1.0" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8"}, - {file = "cssselect-1.4.0.tar.gz", hash = "sha256:fdaf0a1425e17dfe8c5cf66191d211b357cf7872ae8afc4c6762ddd8ac47fc92"}, -] - -[[package]] -name = "exceptiongroup" -version = "1.3.1" -description = "Backport of PEP 654 (exception groups)" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598"}, - {file = "exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219"}, -] - -[package.extras] -test = ["pytest (>=6)"] - -[[package]] -name = "execnet" -version = "2.1.2" -description = "execnet: rapid multi-Python deployment" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec"}, - {file = "execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd"}, -] - -[package.extras] -testing = ["hatch", "pre-commit", "pytest", "tox"] - -[[package]] -name = "fasteners" -version = "0.20" -description = "A python package that provides useful locks" -optional = false -python-versions = ">=3.6" -groups = ["main"] -files = [ - {file = "fasteners-0.20-py3-none-any.whl", hash = "sha256:9422c40d1e350e4259f509fb2e608d6bc43c0136f79a00db1b49046029d0b3b7"}, - {file = "fasteners-0.20.tar.gz", hash = "sha256:55dce8792a41b56f727ba6e123fcaee77fd87e638a6863cec00007bfea84c8d8"}, -] - -[[package]] -name = "filelock" -version = "3.24.3" -description = "A platform independent file lock." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "filelock-3.24.3-py3-none-any.whl", hash = "sha256:426e9a4660391f7f8a810d71b0555bce9008b0a1cc342ab1f6947d37639e002d"}, - {file = "filelock-3.24.3.tar.gz", hash = "sha256:011a5644dc937c22699943ebbfc46e969cdde3e171470a6e40b9533e5a72affa"}, -] - -[[package]] -name = "h11" -version = "0.16.0" -description = "A pure-Python, bring-your-own-I/O implementation of HTTP/1.1" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86"}, - {file = "h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1"}, -] - -[[package]] -name = "idna" -version = "3.11" -description = "Internationalized Domain Names in Applications (IDNA)" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea"}, - {file = "idna-3.11.tar.gz", hash = "sha256:795dafcc9c04ed0c1fb032c2aa73654d8e8c5023a7df64a53f39190ada629902"}, -] - -[package.extras] -all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] - -[[package]] -name = "importlib-metadata" -version = "8.7.1" -description = "Read metadata from Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "importlib_metadata-8.7.1-py3-none-any.whl", hash = "sha256:5a1f80bf1daa489495071efbb095d75a634cf28a8bc299581244063b53176151"}, - {file = "importlib_metadata-8.7.1.tar.gz", hash = "sha256:49fef1ae6440c182052f407c8d34a68f72efc36db9ca90dc0113398f2fdde8bb"}, -] - -[package.dependencies] -zipp = ">=3.20" - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=3.4)"] -perf = ["ipython"] -test = ["flufl.flake8", "jaraco.test (>=5.4)", "packaging", "pyfakefs", "pytest (>=6,!=8.1.*)", "pytest-perf (>=0.9.2)"] -type = ["mypy (<1.19) ; platform_python_implementation == \"PyPy\"", "pytest-mypy (>=1.0.1)"] - -[[package]] -name = "iniconfig" -version = "2.3.0" -description = "brain-dead simple config-ini parsing" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12"}, - {file = "iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730"}, -] - -[[package]] -name = "jinja2" -version = "3.1.6" -description = "A very fast and expressive template engine." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, - {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, -] - -[package.dependencies] -MarkupSafe = ">=2.0" - -[package.extras] -i18n = ["Babel (>=2.7)"] - -[[package]] -name = "markdown-it-py" -version = "4.0.0" -description = "Python port of markdown-it. Markdown parsing, done right!" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147"}, - {file = "markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3"}, -] - -[package.dependencies] -mdurl = ">=0.1,<1.0" - -[package.extras] -benchmarking = ["psutil", "pytest", "pytest-benchmark"] -compare = ["commonmark (>=0.9,<1.0)", "markdown (>=3.4,<4.0)", "markdown-it-pyrs", "mistletoe (>=1.0,<2.0)", "mistune (>=3.0,<4.0)", "panflute (>=2.3,<3.0)"] -linkify = ["linkify-it-py (>=1,<3)"] -plugins = ["mdit-py-plugins (>=0.5.0)"] -profiling = ["gprof2dot"] -rtd = ["ipykernel", "jupyter_sphinx", "mdit-py-plugins (>=0.5.0)", "myst-parser", "pyyaml", "sphinx", "sphinx-book-theme (>=1.0,<2.0)", "sphinx-copybutton", "sphinx-design"] -testing = ["coverage", "pytest", "pytest-cov", "pytest-regressions", "requests"] - -[[package]] -name = "markupsafe" -version = "3.0.3" -description = "Safely add untrusted strings to HTML/XML markup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "markupsafe-3.0.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f981d352f04553a7171b8e44369f2af4055f888dfb147d55e42d29e29e74559"}, - {file = "markupsafe-3.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:e1c1493fb6e50ab01d20a22826e57520f1284df32f2d8601fdd90b6304601419"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1ba88449deb3de88bd40044603fafffb7bc2b055d626a330323a9ed736661695"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f42d0984e947b8adf7dd6dde396e720934d12c506ce84eea8476409563607591"}, - {file = "markupsafe-3.0.3-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0c0b3ade1c0b13b936d7970b1d37a57acde9199dc2aecc4c336773e1d86049c"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:0303439a41979d9e74d18ff5e2dd8c43ed6c6001fd40e5bf2e43f7bd9bbc523f"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:d2ee202e79d8ed691ceebae8e0486bd9a2cd4794cec4824e1c99b6f5009502f6"}, - {file = "markupsafe-3.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:177b5253b2834fe3678cb4a5f0059808258584c559193998be2601324fdeafb1"}, - {file = "markupsafe-3.0.3-cp310-cp310-win32.whl", hash = "sha256:2a15a08b17dd94c53a1da0438822d70ebcd13f8c3a95abe3a9ef9f11a94830aa"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:c4ffb7ebf07cfe8931028e3e4c85f0357459a3f9f9490886198848f4fa002ec8"}, - {file = "markupsafe-3.0.3-cp310-cp310-win_arm64.whl", hash = "sha256:e2103a929dfa2fcaf9bb4e7c091983a49c9ac3b19c9061b6d5427dd7d14d81a1"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1cc7ea17a6824959616c525620e387f6dd30fec8cb44f649e31712db02123dad"}, - {file = "markupsafe-3.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4bd4cd07944443f5a265608cc6aab442e4f74dff8088b0dfc8238647b8f6ae9a"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b5420a1d9450023228968e7e6a9ce57f65d148ab56d2313fcd589eee96a7a50"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0bf2a864d67e76e5c9a34dc26ec616a66b9888e25e7b9460e1c76d3293bd9dbf"}, - {file = "markupsafe-3.0.3-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:bc51efed119bc9cfdf792cdeaa4d67e8f6fcccab66ed4bfdd6bde3e59bfcbb2f"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:068f375c472b3e7acbe2d5318dea141359e6900156b5b2ba06a30b169086b91a"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:7be7b61bb172e1ed687f1754f8e7484f1c8019780f6f6b0786e76bb01c2ae115"}, - {file = "markupsafe-3.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:f9e130248f4462aaa8e2552d547f36ddadbeaa573879158d721bbd33dfe4743a"}, - {file = "markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01"}, - {file = "markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e"}, - {file = "markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d"}, - {file = "markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f"}, - {file = "markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b"}, - {file = "markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c"}, - {file = "markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795"}, - {file = "markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676"}, - {file = "markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc"}, - {file = "markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12"}, - {file = "markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5"}, - {file = "markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73"}, - {file = "markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025"}, - {file = "markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb"}, - {file = "markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218"}, - {file = "markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe"}, - {file = "markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97"}, - {file = "markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf"}, - {file = "markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe"}, - {file = "markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581"}, - {file = "markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab"}, - {file = "markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50"}, - {file = "markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523"}, - {file = "markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9"}, - {file = "markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15d939a21d546304880945ca1ecb8a039db6b4dc49b2c5a400387cdae6a62e26"}, - {file = "markupsafe-3.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:f71a396b3bf33ecaa1626c255855702aca4d3d9fea5e051b41ac59a9c1c41edc"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0f4b68347f8c5eab4a13419215bdfd7f8c9b19f2b25520968adfad23eb0ce60c"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8fc20152abba6b83724d7ff268c249fa196d8259ff481f3b1476383f8f24e42"}, - {file = "markupsafe-3.0.3-cp39-cp39-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:949b8d66bc381ee8b007cd945914c721d9aba8e27f71959d750a46f7c282b20b"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:3537e01efc9d4dccdf77221fb1cb3b8e1a38d5428920e0657ce299b20324d758"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:591ae9f2a647529ca990bc681daebdd52c8791ff06c2bfa05b65163e28102ef2"}, - {file = "markupsafe-3.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:a320721ab5a1aba0a233739394eb907f8c8da5c98c9181d1161e77a0c8e36f2d"}, - {file = "markupsafe-3.0.3-cp39-cp39-win32.whl", hash = "sha256:df2449253ef108a379b8b5d6b43f4b1a8e81a061d6537becd5582fba5f9196d7"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:7c3fb7d25180895632e5d3148dbdc29ea38ccb7fd210aa27acbd1201a1902c6e"}, - {file = "markupsafe-3.0.3-cp39-cp39-win_arm64.whl", hash = "sha256:38664109c14ffc9e7437e86b4dceb442b0096dfe3541d7864d9cbe1da4cf36c8"}, - {file = "markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698"}, -] - -[[package]] -name = "mdurl" -version = "0.1.2" -description = "Markdown URL utilities" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8"}, - {file = "mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba"}, -] - -[[package]] -name = "mouseinfo" -version = "0.1.3" -description = "An application to display XY position and RGB color information for the pixel currently under the mouse. Works on Python 2 and 3." -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "MouseInfo-0.1.3.tar.gz", hash = "sha256:2c62fb8885062b8e520a3cce0a297c657adcc08c60952eb05bc8256ef6f7f6e7"}, -] - -[package.dependencies] -pyperclip = "*" -python3-Xlib = {version = "*", markers = "platform_system == \"Linux\" and python_version >= \"3.0\""} - -[[package]] -name = "mycdp" -version = "1.3.2" -description = "Autogenerated CDP utilities for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "mycdp-1.3.2-py3-none-any.whl", hash = "sha256:d097b12a494223b89a666c87915d8acd48d08b92770ff9f5955ab764676790a0"}, - {file = "mycdp-1.3.2.tar.gz", hash = "sha256:945c405eb35d9759bd24c3676b4633124fac222ac132f735e9d2d812b49f1b3d"}, -] - -[[package]] -name = "mypy-extensions" -version = "1.1.0" -description = "Type system extensions for programs checked with the mypy type checker." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505"}, - {file = "mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558"}, -] - -[[package]] -name = "nest-asyncio" -version = "1.6.0" -description = "Patch asyncio to allow nested event loops" -optional = false -python-versions = ">=3.5" -groups = ["main"] -files = [ - {file = "nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c"}, - {file = "nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe"}, -] - -[[package]] -name = "outcome" -version = "1.3.0.post0" -description = "Capture the outcome of Python function calls." -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "outcome-1.3.0.post0-py2.py3-none-any.whl", hash = "sha256:e771c5ce06d1415e356078d3bdd68523f284b4ce5419828922b6871e65eda82b"}, - {file = "outcome-1.3.0.post0.tar.gz", hash = "sha256:9dcf02e65f2971b80047b377468e72a268e15c0af3cf1238e6ff14f7f91143b8"}, -] - -[package.dependencies] -attrs = ">=19.2.0" - -[[package]] -name = "packaging" -version = "26.0" -description = "Core utilities for Python packages" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529"}, - {file = "packaging-26.0.tar.gz", hash = "sha256:00243ae351a257117b6a241061796684b084ed1c516a08c48a3f7e147a9d80b4"}, -] - -[[package]] -name = "parameterized" -version = "0.9.0" -description = "Parameterized testing with any Python test framework" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "parameterized-0.9.0-py2.py3-none-any.whl", hash = "sha256:4e0758e3d41bea3bbd05ec14fc2c24736723f243b28d702081aef438c9372b1b"}, - {file = "parameterized-0.9.0.tar.gz", hash = "sha256:7fc905272cefa4f364c1a3429cbbe9c0f98b793988efb5bf90aac80f08db09b1"}, -] - -[package.extras] -dev = ["jinja2"] - -[[package]] -name = "parse" -version = "1.21.1" -description = "parse() is the opposite of format()" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "parse-1.21.1-py2.py3-none-any.whl", hash = "sha256:55339ca698019815df3b8e8b550e5933933527e623b0cdf1ca2f404da35ffb47"}, - {file = "parse-1.21.1.tar.gz", hash = "sha256:825e1a88e9d9fb481b8d2ca709c6195558b6eaa97c559ad3a9a20aa2d12815a3"}, -] - -[[package]] -name = "parse-type" -version = "0.6.6" -description = "Simplifies to build parse types based on the parse module" -optional = false -python-versions = "!=3.0.*,!=3.1.*,>=2.7" -groups = ["main"] -files = [ - {file = "parse_type-0.6.6-py2.py3-none-any.whl", hash = "sha256:3ca79bbe71e170dfccc8ec6c341edfd1c2a0fc1e5cfd18330f93af938de2348c"}, - {file = "parse_type-0.6.6.tar.gz", hash = "sha256:513a3784104839770d690e04339a8b4d33439fcd5dd99f2e4580f9fc1097bfb2"}, -] - -[package.dependencies] -parse = {version = ">=1.18.0", markers = "python_version >= \"3.0\""} -six = ">=1.15" - -[package.extras] -develop = ["build (>=0.5.1)", "coverage (>=4.4)", "pylint", "pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-cov", "pytest-html (>=1.19.0)", "ruff ; python_version >= \"3.7\"", "setuptools", "setuptools-scm", "tox (>=2.8,<4.0)", "twine (>=1.13.0)", "virtualenv (<20.22.0) ; python_version <= \"3.6\"", "virtualenv (>=20.0.0) ; python_version > \"3.6\"", "wheel"] -docs = ["Sphinx (>=1.6)", "sphinx_bootstrap_theme (>=0.6.0)"] -testing = ["pytest (<5.0) ; python_version < \"3.0\"", "pytest (>=5.0) ; python_version >= \"3.0\"", "pytest-html (>=1.19.0)"] - -[[package]] -name = "pdbp" -version = "1.8.2" -description = "pdbp (Pdb+): A drop-in replacement for pdb and pdbpp." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pdbp-1.8.2-py3-none-any.whl", hash = "sha256:d4fd05e177636b5ccd0b2e03e378cec57afc06149e5fd975de6f8ddb3d0109a8"}, - {file = "pdbp-1.8.2.tar.gz", hash = "sha256:367c25c17555d3ac1f024b9ad494ff50e6e20f6494a84741487f3e6596d88f94"}, -] - -[package.dependencies] -colorama = {version = ">=0.4.6", markers = "platform_system == \"Windows\""} -pygments = ">=2.19.2" -tabcompleter = ">=1.4.0" - -[[package]] -name = "pip" -version = "26.0.1" -description = "The PyPA recommended tool for installing Python packages." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pip-26.0.1-py3-none-any.whl", hash = "sha256:bdb1b08f4274833d62c1aa29e20907365a2ceb950410df15fc9521bad440122b"}, - {file = "pip-26.0.1.tar.gz", hash = "sha256:c4037d8a277c89b320abe636d59f91e6d0922d08a05b60e85e53b296613346d8"}, -] - -[[package]] -name = "platformdirs" -version = "4.9.2" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "platformdirs-4.9.2-py3-none-any.whl", hash = "sha256:9170634f126f8efdae22fb58ae8a0eaa86f38365bc57897a6c4f781d1f5875bd"}, - {file = "platformdirs-4.9.2.tar.gz", hash = "sha256:9a33809944b9db043ad67ca0db94b14bf452cc6aeaac46a88ea55b26e2e9d291"}, -] - -[[package]] -name = "pluggy" -version = "1.6.0" -description = "plugin and hook calling mechanisms for python" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746"}, - {file = "pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3"}, -] - -[package.extras] -dev = ["pre-commit", "tox"] -testing = ["coverage", "pytest", "pytest-benchmark"] - -[[package]] -name = "pyautogui" -version = "0.9.54" -description = "PyAutoGUI lets Python control the mouse and keyboard, and other GUI automation tasks. For Windows, macOS, and Linux, on Python 3 and 2." -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "PyAutoGUI-0.9.54.tar.gz", hash = "sha256:dd1d29e8fd118941cb193f74df57e5c6ff8e9253b99c7b04f39cfc69f3ae04b2"}, -] - -[package.dependencies] -mouseinfo = "*" -pygetwindow = ">=0.0.5" -pymsgbox = "*" -pyscreeze = ">=0.1.21" -python3-Xlib = {version = "*", markers = "platform_system == \"Linux\" and python_version >= \"3.0\""} -pytweening = ">=1.0.4" - -[[package]] -name = "pycparser" -version = "3.0" -description = "C parser in Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -markers = "os_name == \"nt\" and implementation_name != \"pypy\" and implementation_name != \"PyPy\"" -files = [ - {file = "pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992"}, - {file = "pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29"}, -] - -[[package]] -name = "pygetwindow" -version = "0.0.9" -description = "A simple, cross-platform module for obtaining GUI information on application's windows." -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "PyGetWindow-0.0.9.tar.gz", hash = "sha256:17894355e7d2b305cd832d717708384017c1698a90ce24f6f7fbf0242dd0a688"}, -] - -[package.dependencies] -pyrect = "*" - -[[package]] -name = "pygments" -version = "2.19.2" -description = "Pygments is a syntax highlighting package written in Python." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b"}, - {file = "pygments-2.19.2.tar.gz", hash = "sha256:636cb2477cec7f8952536970bc533bc43743542f70392ae026374600add5b887"}, -] - -[package.extras] -windows-terminal = ["colorama (>=0.4.6)"] - -[[package]] -name = "pymsgbox" -version = "2.0.1" -description = "A simple, cross-platform, pure Python module for JavaScript-like message boxes." -optional = false -python-versions = ">=3.4" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "pymsgbox-2.0.1-py3-none-any.whl", hash = "sha256:5de8ec19bca2ca7e6c09d39c817c83f17c75cee80275235f43a9931db699f73b"}, - {file = "pymsgbox-2.0.1.tar.gz", hash = "sha256:98d055c49a511dcc10fa08c3043e7102d468f5e4b3a83c6d3c61df722c7d798d"}, -] - -[[package]] -name = "pynose" -version = "1.5.5" -description = "pynose fixes nose to extend unittest and make testing easier" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "pynose-1.5.5-py3-none-any.whl", hash = "sha256:673751d53fcfc79b1e48c14f36c7a24779ad43676eeb85736934de6a2b3d8ec8"}, - {file = "pynose-1.5.5.tar.gz", hash = "sha256:81da4e26473f98dd37497248eef4352d3221d1d56edf874a00c6bdda6daf7f49"}, -] - -[[package]] -name = "pyotp" -version = "2.9.0" -description = "Python One Time Password Library" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "pyotp-2.9.0-py3-none-any.whl", hash = "sha256:81c2e5865b8ac55e825b0358e496e1d9387c811e85bb40e71a3b29b288963612"}, - {file = "pyotp-2.9.0.tar.gz", hash = "sha256:346b6642e0dbdde3b4ff5a930b664ca82abfa116356ed48cc42c7d6590d36f63"}, -] - -[package.extras] -test = ["coverage", "mypy", "ruff", "wheel"] - -[[package]] -name = "pyperclip" -version = "1.11.0" -description = "A cross-platform clipboard module for Python. (Only handles plain text for now.)" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "pyperclip-1.11.0-py3-none-any.whl", hash = "sha256:299403e9ff44581cb9ba2ffeed69c7aa96a008622ad0c46cb575ca75b5b84273"}, - {file = "pyperclip-1.11.0.tar.gz", hash = "sha256:244035963e4428530d9e3a6101a1ef97209c6825edab1567beac148ccc1db1b6"}, -] - -[[package]] -name = "pyreadline3" -version = "3.5.4" -description = "A python implementation of GNU readline." -optional = false -python-versions = ">=3.8" -groups = ["main"] -markers = "platform_system == \"Windows\"" -files = [ - {file = "pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6"}, - {file = "pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7"}, -] - -[package.extras] -dev = ["build", "flake8", "mypy", "pytest", "twine"] - -[[package]] -name = "pyrect" -version = "0.2.0" -description = "PyRect is a simple module with a Rect class for Pygame-like rectangular areas." -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "PyRect-0.2.0.tar.gz", hash = "sha256:f65155f6df9b929b67caffbd57c0947c5ae5449d3b580d178074bffb47a09b78"}, -] - -[[package]] -name = "pyscreeze" -version = "1.0.1" -description = "A simple, cross-platform screenshot module for Python 2 and 3." -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "pyscreeze-1.0.1.tar.gz", hash = "sha256:cf1662710f1b46aa5ff229ee23f367da9e20af4a78e6e365bee973cad0ead4be"}, -] - -[[package]] -name = "pysocks" -version = "1.7.1" -description = "A Python SOCKS client module. See https://github.com/Anorov/PySocks for more information." -optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" -groups = ["main"] -files = [ - {file = "PySocks-1.7.1-py27-none-any.whl", hash = "sha256:08e69f092cc6dbe92a0fdd16eeb9b9ffbc13cadfe5ca4c7bd92ffb078b293299"}, - {file = "PySocks-1.7.1-py3-none-any.whl", hash = "sha256:2725bd0a9925919b9b51739eea5f9e2bae91e83288108a9ad338b2e3a4435ee5"}, - {file = "PySocks-1.7.1.tar.gz", hash = "sha256:3f8804571ebe159c380ac6de37643bb4685970655d3bba243530d6558b799aa0"}, -] - -[[package]] -name = "pytest" -version = "9.0.2" -description = "pytest: simple powerful testing with Python" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b"}, - {file = "pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11"}, -] - -[package.dependencies] -colorama = {version = ">=0.4", markers = "sys_platform == \"win32\""} -iniconfig = ">=1.0.1" -packaging = ">=22" -pluggy = ">=1.5,<2" -pygments = ">=2.7.2" - -[package.extras] -dev = ["argcomplete", "attrs (>=19.2)", "hypothesis (>=3.56)", "mock", "requests", "setuptools", "xmlschema"] - -[[package]] -name = "pytest-html" -version = "4.0.2" -description = "pytest plugin for generating HTML reports" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pytest_html-4.0.2-py3-none-any.whl", hash = "sha256:907c3e68462df129d3ee96dee58bd63f70216b06421836b22fd3fd57ef314acb"}, - {file = "pytest_html-4.0.2.tar.gz", hash = "sha256:88682b9e8e51392472546a70a2139b27d6bc1834a4afd3e41da33c9d9f91e4a4"}, -] - -[package.dependencies] -jinja2 = ">=3.0.0" -pytest = ">=7.0.0" -pytest-metadata = ">=2.0.0" - -[package.extras] -docs = ["pip-tools (>=6.13.0)"] -test = ["assertpy (>=1.1)", "beautifulsoup4 (>=4.11.1)", "black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "pytest-mock (>=3.7.0)", "pytest-rerunfailures (>=11.1.2)", "pytest-xdist (>=2.4.0)", "selenium (>=4.3.0)", "tox (>=3.24.5)"] - -[[package]] -name = "pytest-metadata" -version = "3.1.1" -description = "pytest plugin for test session metadata" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "pytest_metadata-3.1.1-py3-none-any.whl", hash = "sha256:c8e0844db684ee1c798cfa38908d20d67d0463ecb6137c72e91f418558dd5f4b"}, - {file = "pytest_metadata-3.1.1.tar.gz", hash = "sha256:d2a29b0355fbc03f168aa96d41ff88b1a3b44a3b02acbe491801c98a048017c8"}, -] - -[package.dependencies] -pytest = ">=7.0.0" - -[package.extras] -test = ["black (>=22.1.0)", "flake8 (>=4.0.1)", "pre-commit (>=2.17.0)", "tox (>=3.24.5)"] - -[[package]] -name = "pytest-ordering" -version = "0.6" -description = "pytest plugin to run your tests in a specific order" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "pytest-ordering-0.6.tar.gz", hash = "sha256:561ad653626bb171da78e682f6d39ac33bb13b3e272d406cd555adb6b006bda6"}, - {file = "pytest_ordering-0.6-py2-none-any.whl", hash = "sha256:27fba3fc265f5d0f8597e7557885662c1bdc1969497cd58aff6ed21c3b617de2"}, - {file = "pytest_ordering-0.6-py3-none-any.whl", hash = "sha256:3f314a178dbeb6777509548727dc69edf22d6d9a2867bf2d310ab85c403380b6"}, -] - -[package.dependencies] -pytest = "*" - -[[package]] -name = "pytest-rerunfailures" -version = "16.1" -description = "pytest plugin to re-run tests to eliminate flaky failures" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "pytest_rerunfailures-16.1-py3-none-any.whl", hash = "sha256:5d11b12c0ca9a1665b5054052fcc1084f8deadd9328962745ef6b04e26382e86"}, - {file = "pytest_rerunfailures-16.1.tar.gz", hash = "sha256:c38b266db8a808953ebd71ac25c381cb1981a78ff9340a14bcb9f1b9bff1899e"}, -] - -[package.dependencies] -packaging = ">=17.1" -pytest = ">=7.4,<8.2.2 || >8.2.2" - -[[package]] -name = "pytest-xdist" -version = "3.8.0" -description = "pytest xdist plugin for distributed testing, most importantly across multiple CPUs" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88"}, - {file = "pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1"}, -] - -[package.dependencies] -execnet = ">=2.1" -pytest = ">=7.0.0" - -[package.extras] -psutil = ["psutil (>=3.0)"] -setproctitle = ["setproctitle"] -testing = ["filelock"] - -[[package]] -name = "python-xlib" -version = "0.33" -description = "Python X Library" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "python-xlib-0.33.tar.gz", hash = "sha256:55af7906a2c75ce6cb280a584776080602444f75815a7aff4d287bb2d7018b32"}, - {file = "python_xlib-0.33-py2.py3-none-any.whl", hash = "sha256:c3534038d42e0df2f1392a1b30a15a4ff5fdc2b86cfa94f072bf11b10a164398"}, -] - -[package.dependencies] -six = ">=1.10.0" - -[[package]] -name = "python3-xlib" -version = "0.15" -description = "Python3 X Library" -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "python3-xlib-0.15.tar.gz", hash = "sha256:dc4245f3ae4aa5949c1d112ee4723901ade37a96721ba9645f2bfa56e5b383f8"}, -] - -[[package]] -name = "pytweening" -version = "1.2.0" -description = "A collection of tweening (aka easing) functions." -optional = false -python-versions = "*" -groups = ["main"] -markers = "platform_system == \"Linux\"" -files = [ - {file = "pytweening-1.2.0.tar.gz", hash = "sha256:243318b7736698066c5f362ec5c2b6434ecf4297c3c8e7caa8abfe6af4cac71b"}, -] - -[[package]] -name = "pyyaml" -version = "6.0.3" -description = "YAML parser and emitter for Python" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "PyYAML-6.0.3-cp38-cp38-macosx_10_13_x86_64.whl", hash = "sha256:c2514fceb77bc5e7a2f7adfaa1feb2fb311607c9cb518dbc378688ec73d8292f"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9c57bb8c96f6d1808c030b1687b9b5fb476abaa47f0db9c0101f5e9f394e97f4"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:efd7b85f94a6f21e4932043973a7ba2613b059c4a000551892ac9f1d11f5baf3"}, - {file = "PyYAML-6.0.3-cp38-cp38-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:22ba7cfcad58ef3ecddc7ed1db3409af68d023b7f940da23c6c2a1890976eda6"}, - {file = "PyYAML-6.0.3-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:6344df0d5755a2c9a276d4473ae6b90647e216ab4757f8426893b5dd2ac3f369"}, - {file = "PyYAML-6.0.3-cp38-cp38-win32.whl", hash = "sha256:3ff07ec89bae51176c0549bc4c63aa6202991da2d9a6129d7aef7f1407d3f295"}, - {file = "PyYAML-6.0.3-cp38-cp38-win_amd64.whl", hash = "sha256:5cf4e27da7e3fbed4d6c3d8e797387aaad68102272f8f9752883bc32d61cb87b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b"}, - {file = "pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198"}, - {file = "pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0"}, - {file = "pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69"}, - {file = "pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e"}, - {file = "pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e"}, - {file = "pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00"}, - {file = "pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a"}, - {file = "pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4"}, - {file = "pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b"}, - {file = "pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196"}, - {file = "pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c"}, - {file = "pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e"}, - {file = "pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea"}, - {file = "pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b"}, - {file = "pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8"}, - {file = "pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5"}, - {file = "pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6"}, - {file = "pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be"}, - {file = "pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c"}, - {file = "pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac"}, - {file = "pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788"}, - {file = "pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764"}, - {file = "pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac"}, - {file = "pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3"}, - {file = "pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702"}, - {file = "pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065"}, - {file = "pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9"}, - {file = "pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_10_13_x86_64.whl", hash = "sha256:b865addae83924361678b652338317d1bd7e79b1f4596f96b96c77a5a34b34da"}, - {file = "pyyaml-6.0.3-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:c3355370a2c156cffb25e876646f149d5d68f5e0a3ce86a5084dd0b64a994917"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c5677e12444c15717b902a5798264fa7909e41153cdf9ef7ad571b704a63dd9"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5ed875a24292240029e4483f9d4a4b8a1ae08843b9c54f43fcc11e404532a8a5"}, - {file = "pyyaml-6.0.3-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0150219816b6a1fa26fb4699fb7daa9caf09eb1999f3b70fb6e786805e80375a"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:fa160448684b4e94d80416c0fa4aac48967a969efe22931448d853ada8baf926"}, - {file = "pyyaml-6.0.3-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:27c0abcb4a5dac13684a37f76e701e054692a9b2d3064b70f5e4eb54810553d7"}, - {file = "pyyaml-6.0.3-cp39-cp39-win32.whl", hash = "sha256:1ebe39cb5fc479422b83de611d14e2c0d3bb2a18bbcb01f229ab3cfbd8fee7a0"}, - {file = "pyyaml-6.0.3-cp39-cp39-win_amd64.whl", hash = "sha256:2e71d11abed7344e42a8849600193d15b6def118602c4c176f748e4583246007"}, - {file = "pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f"}, -] - -[[package]] -name = "requests" -version = "2.32.5" -description = "Python HTTP for Humans." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "requests-2.32.5-py3-none-any.whl", hash = "sha256:2462f94637a34fd532264295e186976db0f5d453d1cdd31473c85a6a161affb6"}, - {file = "requests-2.32.5.tar.gz", hash = "sha256:dbba0bac56e100853db0ea71b82b4dfd5fe2bf6d3754a8893c3af500cec7d7cf"}, -] - -[package.dependencies] -certifi = ">=2017.4.17" -charset_normalizer = ">=2,<4" -idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<3" - -[package.extras] -socks = ["PySocks (>=1.5.6,!=1.5.7)"] -use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] - -[[package]] -name = "rich" -version = "14.3.2" -description = "Render rich text, tables, progress bars, syntax highlighting, markdown and more to the terminal" -optional = false -python-versions = ">=3.8.0" -groups = ["main"] -files = [ - {file = "rich-14.3.2-py3-none-any.whl", hash = "sha256:08e67c3e90884651da3239ea668222d19bea7b589149d8014a21c633420dbb69"}, - {file = "rich-14.3.2.tar.gz", hash = "sha256:e712f11c1a562a11843306f5ed999475f09ac31ffb64281f73ab29ffdda8b3b8"}, -] - -[package.dependencies] -markdown-it-py = ">=2.2.0" -pygments = ">=2.13.0,<3.0.0" - -[package.extras] -jupyter = ["ipywidgets (>=7.5.1,<9)"] - -[[package]] -name = "sbvirtualdisplay" -version = "1.4.0" -description = "A customized pyvirtualdisplay for SeleniumBase." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "sbvirtualdisplay-1.4.0-py3-none-any.whl", hash = "sha256:516de155219aa342c4e090a3c5126cfe6b12416334bcba3255268e44a5e8a206"}, - {file = "sbvirtualdisplay-1.4.0.tar.gz", hash = "sha256:29a365b509cd7bfde4f758603b7b75703909b11cdf4245abc8f828ed35660d9b"}, -] - -[package.extras] -coverage = ["coverage (>=7.6.1) ; python_version < \"3.9\"", "coverage (>=7.6.9) ; python_version >= \"3.9\"", "pytest-cov (>=5.0.0) ; python_version < \"3.9\"", "pytest-cov (>=6.0.0) ; python_version >= \"3.9\""] -flake8 = ["flake8 (==5.0.4) ; python_version < \"3.9\"", "flake8 (==7.1.1) ; python_version >= \"3.9\"", "mccabe (==0.7.0)", "pycodestyle (==2.12.1) ; python_version >= \"3.9\"", "pycodestyle (==2.9.1) ; python_version < \"3.9\"", "pyflakes (==2.5.0) ; python_version < \"3.9\"", "pyflakes (==3.2.0) ; python_version >= \"3.9\""] - -[[package]] -name = "selenium" -version = "4.40.0" -description = "Official Python bindings for Selenium WebDriver" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "selenium-4.40.0-py3-none-any.whl", hash = "sha256:c8823fc02e2c771d9ad9a0cf899cee7de1a57a6697e3d0b91f67566129f2b729"}, - {file = "selenium-4.40.0.tar.gz", hash = "sha256:a88f5905d88ad0b84991c2386ea39e2bbde6d6c334be38df5842318ba98eaa8c"}, -] - -[package.dependencies] -certifi = ">=2026.1.4" -trio = ">=0.31.0,<1.0" -trio-typing = ">=0.10.0" -trio-websocket = ">=0.12.2,<1.0" -types-certifi = ">=2021.10.8.3" -types-urllib3 = ">=1.26.25.14" -typing_extensions = ">=4.15.0,<5.0" -urllib3 = {version = ">=2.6.3,<3.0", extras = ["socks"]} -websocket-client = ">=1.8.0,<2.0" - -[[package]] -name = "seleniumbase" -version = "4.46.5" -description = "A complete web automation framework for end-to-end testing." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "seleniumbase-4.46.5-py3-none-any.whl", hash = "sha256:d87ca08ed642c2ed5ddb0be74259f570ecdc443e0a99ea0076abde0d013c8586"}, - {file = "seleniumbase-4.46.5.tar.gz", hash = "sha256:a1e22217874da901d361ce7577bf8d4eda990563d3dd2cfada612663380036c6"}, -] - -[package.dependencies] -attrs = ">=25.4.0" -beautifulsoup4 = ">=4.14.3,<4.15.0" -behave = "1.2.6" -certifi = ">=2026.1.4" -chardet = "5.2.0" -charset-normalizer = ">=3.4.4,<4" -colorama = ">=0.4.6" -cssselect = {version = ">=1.4.0,<2", markers = "python_version >= \"3.10\""} -exceptiongroup = ">=1.3.1" -execnet = {version = "2.1.2", markers = "python_version >= \"3.10\""} -fasteners = ">=0.20" -filelock = {version = ">=3.20.3", markers = "python_version >= \"3.10\""} -h11 = "0.16.0" -idna = ">=3.11" -iniconfig = {version = "2.3.0", markers = "python_version >= \"3.10\""} -Jinja2 = ">=3.1.6" -markdown-it-py = {version = "4.0.0", markers = "python_version >= \"3.10\""} -MarkupSafe = ">=3.0.3" -mdurl = "0.1.2" -mycdp = ">=1.3.2" -nest-asyncio = "1.6.0" -outcome = "1.3.0.post0" -packaging = ">=26.0" -parameterized = "0.9.0" -parse = ">=1.21.0" -parse-type = ">=0.6.6" -pdbp = ">=1.8.2" -pip = ">=26.0.1" -platformdirs = {version = ">=4.5.1", markers = "python_version >= \"3.10\""} -pluggy = "1.6.0" -PyAutoGUI = {version = ">=0.9.54", markers = "platform_system == \"Linux\""} -pygments = ">=2.19.2" -pynose = ">=1.5.5" -pyotp = "2.9.0" -pyreadline3 = {version = ">=3.5.4", markers = "platform_system == \"Windows\""} -pytest = {version = "9.0.2", markers = "python_version >= \"3.11\""} -pytest-html = "4.0.2" -pytest-metadata = "3.1.1" -pytest-ordering = "0.6" -pytest-rerunfailures = {version = "16.1", markers = "python_version >= \"3.10\""} -pytest-xdist = "3.8.0" -python-xlib = {version = "0.33", markers = "platform_system == \"Linux\""} -pyyaml = ">=6.0.3" -requests = ">=2.32.5,<2.33.0" -rich = ">=14.3.2,<15" -sbvirtualdisplay = ">=1.4.0" -selenium = {version = "4.40.0", markers = "python_version >= \"3.10\""} -setuptools = {version = ">=82.0.0", markers = "python_version >= \"3.10\""} -six = ">=1.17.0" -sniffio = "1.3.1" -sortedcontainers = "2.4.0" -soupsieve = ">=2.8.3,<2.9.0" -tabcompleter = ">=1.4.0" -trio = {version = ">=0.32.0,<1", markers = "python_version >= \"3.10\""} -trio-websocket = ">=0.12.2,<0.13.0" -typing-extensions = ">=4.15.0" -urllib3 = {version = ">=1.26.20,<3", markers = "python_version >= \"3.10\""} -websocket-client = ">=1.9.0,<1.10.0" -websockets = {version = ">=16.0", markers = "python_version >= \"3.10\""} -wheel = ">=0.46.3" -wsproto = {version = ">=1.3.2,<1.4.0", markers = "python_version >= \"3.10\""} - -[package.extras] -allure = ["allure-behave (>=2.13.5)", "allure-pytest (>=2.13.5)", "allure-python-commons (>=2.13.5)"] -coverage = ["coverage (>=7.10.7) ; python_version < \"3.10\"", "coverage (>=7.13.4) ; python_version >= \"3.10\"", "pytest-cov (>=7.0.0)"] -flake8 = ["flake8 (==7.3.0)", "mccabe (==0.7.0)", "pycodestyle (==2.14.0)", "pyflakes (==3.4.0)"] -ipdb = ["ipdb (==0.13.13)", "ipython (==7.34.0)"] -mss = ["mss (==10.1.0)"] -pdfminer = ["cffi (==2.0.0)", "cryptography (==46.0.5)", "pdfminer.six (==20251107) ; python_version < \"3.10\"", "pdfminer.six (==20260107) ; python_version >= \"3.10\"", "pycparser (==2.23) ; python_version < \"3.10\"", "pycparser (==3.0) ; python_version >= \"3.10\""] -pillow = ["Pillow (>=11.3.0) ; python_version < \"3.10\"", "Pillow (>=12.1.1) ; python_version >= \"3.10\""] -pip-system-certs = ["pip-system-certs (==4.0) ; platform_system == \"Windows\""] -playwright = ["playwright (>=1.58.0)"] -proxy = ["proxy.py (==2.4.3)"] -psutil = ["psutil (>=7.2.2)"] -pyautogui = ["PyAutoGUI (>=0.9.54) ; platform_system != \"Linux\""] -selenium-stealth = ["selenium-stealth (==1.0.6)"] -selenium-wire = ["Brotli (==1.1.0)", "blinker (==1.7.0)", "h2 (==4.1.0)", "hpack (==4.0.0)", "hyperframe (==6.0.1)", "kaitaistruct (==0.10)", "pyOpenSSL (>=24.2.1)", "pyasn1 (==0.6.1)", "pyparsing (>=3.1.4)", "selenium-wire (==5.1.0)", "zstandard (>=0.23.0)"] - -[[package]] -name = "setuptools" -version = "82.0.0" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "setuptools-82.0.0-py3-none-any.whl", hash = "sha256:70b18734b607bd1da571d097d236cfcfacaf01de45717d59e6e04b96877532e0"}, - {file = "setuptools-82.0.0.tar.gz", hash = "sha256:22e0a2d69474c6ae4feb01951cb69d515ed23728cf96d05513d36e42b62b37cb"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\"", "ruff (>=0.13.0) ; sys_platform != \"cygwin\""] -core = ["importlib_metadata (>=6) ; python_version < \"3.10\"", "jaraco.functools (>=4)", "jaraco.text (>=3.7)", "more_itertools", "more_itertools (>=8.8)", "packaging (>=24.2)", "platformdirs (>=4.2.2)", "tomli (>=2.0.1) ; python_version < \"3.11\"", "wheel (>=0.43.0)"] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "pyproject-hooks (!=1.1)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-inline-tabs", "sphinx-lint", "sphinx-notfound-page (>=1,<2)", "sphinx-reredirects", "sphinxcontrib-towncrier", "towncrier (<24.7)"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["build[virtualenv] (>=1.0.3)", "filelock (>=3.4.0)", "ini2toml[lite] (>=0.14)", "jaraco.develop (>=7.21) ; python_version >= \"3.9\" and sys_platform != \"cygwin\"", "jaraco.envs (>=2.2)", "jaraco.path (>=3.7.2)", "jaraco.test (>=5.5)", "packaging (>=24.2)", "pip (>=19.1)", "pyproject-hooks (!=1.1)", "pytest (>=6,!=8.1.*)", "pytest-home (>=0.5)", "pytest-perf ; sys_platform != \"cygwin\"", "pytest-subprocess", "pytest-timeout", "pytest-xdist (>=3)", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel (>=0.44.0)"] -type = ["importlib_metadata (>=7.0.2) ; python_version < \"3.10\"", "jaraco.develop (>=7.21) ; sys_platform != \"cygwin\"", "mypy (==1.18.*)", "pytest-mypy"] - -[[package]] -name = "six" -version = "1.17.0" -description = "Python 2 and 3 compatibility utilities" -optional = false -python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,>=2.7" -groups = ["main"] -files = [ - {file = "six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274"}, - {file = "six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81"}, -] - -[[package]] -name = "sniffio" -version = "1.3.1" -description = "Sniff out which async library your code is running under" -optional = false -python-versions = ">=3.7" -groups = ["main"] -files = [ - {file = "sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2"}, - {file = "sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc"}, -] - -[[package]] -name = "sortedcontainers" -version = "2.4.0" -description = "Sorted Containers -- Sorted List, Sorted Dict, Sorted Set" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "sortedcontainers-2.4.0-py2.py3-none-any.whl", hash = "sha256:a163dcaede0f1c021485e957a39245190e74249897e2ae4b2aa38595db237ee0"}, - {file = "sortedcontainers-2.4.0.tar.gz", hash = "sha256:25caa5a06cc30b6b83d11423433f65d1f9d76c4c6a0c90e3379eaa43b9bfdb88"}, -] - -[[package]] -name = "soupsieve" -version = "2.8.3" -description = "A modern CSS selector implementation for Beautiful Soup." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "soupsieve-2.8.3-py3-none-any.whl", hash = "sha256:ed64f2ba4eebeab06cc4962affce381647455978ffc1e36bb79a545b91f45a95"}, - {file = "soupsieve-2.8.3.tar.gz", hash = "sha256:3267f1eeea4251fb42728b6dfb746edc9acaffc4a45b27e19450b676586e8349"}, -] - -[[package]] -name = "tabcompleter" -version = "1.4.0" -description = "tabcompleter --- Autocompletion in the Python console." -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "tabcompleter-1.4.0-py3-none-any.whl", hash = "sha256:d744aa735b49c0a6cc2fb8fcd40077fec47425e4388301010b14e6ce3311368b"}, - {file = "tabcompleter-1.4.0.tar.gz", hash = "sha256:7562a9938e62f8e7c3be612c3ac4e14c5ec4307b58ba9031c148260e866e8814"}, -] - -[package.dependencies] -pyreadline3 = {version = "*", markers = "platform_system == \"Windows\""} - -[[package]] -name = "trio" -version = "0.33.0" -description = "A friendly Python library for async concurrency and I/O" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "trio-0.33.0-py3-none-any.whl", hash = "sha256:3bd5d87f781d9b0192d592aef28691f8951d6c2e41b7e1da4c25cde6c180ae9b"}, - {file = "trio-0.33.0.tar.gz", hash = "sha256:a29b92b73f09d4b48ed249acd91073281a7f1063f09caba5dc70465b5c7aa970"}, -] - -[package.dependencies] -attrs = ">=23.2.0" -cffi = {version = ">=1.14", markers = "os_name == \"nt\" and implementation_name != \"pypy\""} -idna = "*" -outcome = "*" -sniffio = ">=1.3.0" -sortedcontainers = "*" - -[[package]] -name = "trio-typing" -version = "0.10.0" -description = "Static type checking support for Trio and related projects" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "trio-typing-0.10.0.tar.gz", hash = "sha256:065ee684296d52a8ab0e2374666301aec36ee5747ac0e7a61f230250f8907ac3"}, - {file = "trio_typing-0.10.0-py3-none-any.whl", hash = "sha256:6d0e7ec9d837a2fe03591031a172533fbf4a1a95baf369edebfc51d5a49f0264"}, -] - -[package.dependencies] -async-generator = "*" -importlib-metadata = "*" -mypy-extensions = ">=0.4.2" -packaging = "*" -trio = ">=0.16.0" -typing-extensions = ">=3.7.4" - -[package.extras] -mypy = ["mypy (>=1.0)"] - -[[package]] -name = "trio-websocket" -version = "0.12.2" -description = "WebSocket library for Trio" -optional = false -python-versions = ">=3.8" -groups = ["main"] -files = [ - {file = "trio_websocket-0.12.2-py3-none-any.whl", hash = "sha256:df605665f1db533f4a386c94525870851096a223adcb97f72a07e8b4beba45b6"}, - {file = "trio_websocket-0.12.2.tar.gz", hash = "sha256:22c72c436f3d1e264d0910a3951934798dcc5b00ae56fc4ee079d46c7cf20fae"}, -] - -[package.dependencies] -outcome = ">=1.2.0" -trio = ">=0.11" -wsproto = ">=0.14" - -[[package]] -name = "types-certifi" -version = "2021.10.8.3" -description = "Typing stubs for certifi" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f"}, - {file = "types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a"}, -] - -[[package]] -name = "types-urllib3" -version = "1.26.25.14" -description = "Typing stubs for urllib3" -optional = false -python-versions = "*" -groups = ["main"] -files = [ - {file = "types-urllib3-1.26.25.14.tar.gz", hash = "sha256:229b7f577c951b8c1b92c1bc2b2fdb0b49847bd2af6d1cc2a2e3dd340f3bda8f"}, - {file = "types_urllib3-1.26.25.14-py3-none-any.whl", hash = "sha256:9683bbb7fb72e32bfe9d2be6e04875fbe1b3eeec3cbb4ea231435aa7fd6b4f0e"}, -] - -[[package]] -name = "typing-extensions" -version = "4.15.0" -description = "Backported and Experimental Type Hints for Python 3.9+" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548"}, - {file = "typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466"}, -] - -[[package]] -name = "urllib3" -version = "2.6.3" -description = "HTTP library with thread-safe connection pooling, file post, and more." -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4"}, - {file = "urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed"}, -] - -[package.dependencies] -pysocks = {version = ">=1.5.6,<1.5.7 || >1.5.7,<2.0", optional = true, markers = "extra == \"socks\""} - -[package.extras] -brotli = ["brotli (>=1.2.0) ; platform_python_implementation == \"CPython\"", "brotlicffi (>=1.2.0.0) ; platform_python_implementation != \"CPython\""] -h2 = ["h2 (>=4,<5)"] -socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] -zstd = ["backports-zstd (>=1.0.0) ; python_version < \"3.14\""] - -[[package]] -name = "websocket-client" -version = "1.9.0" -description = "WebSocket client for Python with low level API options" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "websocket_client-1.9.0-py3-none-any.whl", hash = "sha256:af248a825037ef591efbf6ed20cc5faa03d3b47b9e5a2230a529eeee1c1fc3ef"}, - {file = "websocket_client-1.9.0.tar.gz", hash = "sha256:9e813624b6eb619999a97dc7958469217c3176312b3a16a4bd1bc7e08a46ec98"}, -] - -[package.extras] -docs = ["Sphinx (>=6.0)", "myst-parser (>=2.0.0)", "sphinx_rtd_theme (>=1.1.0)"] -optional = ["python-socks", "wsaccel"] -test = ["pytest", "websockets"] - -[[package]] -name = "websockets" -version = "16.0" -description = "An implementation of the WebSocket Protocol (RFC 6455 & 7692)" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "websockets-16.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:04cdd5d2d1dacbad0a7bf36ccbcd3ccd5a30ee188f2560b7a62a30d14107b31a"}, - {file = "websockets-16.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:8ff32bb86522a9e5e31439a58addbb0166f0204d64066fb955265c4e214160f0"}, - {file = "websockets-16.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:583b7c42688636f930688d712885cf1531326ee05effd982028212ccc13e5957"}, - {file = "websockets-16.0-cp310-cp310-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7d837379b647c0c4c2355c2499723f82f1635fd2c26510e1f587d89bc2199e72"}, - {file = "websockets-16.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df57afc692e517a85e65b72e165356ed1df12386ecb879ad5693be08fac65dde"}, - {file = "websockets-16.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:2b9f1e0d69bc60a4a87349d50c09a037a2607918746f07de04df9e43252c77a3"}, - {file = "websockets-16.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:335c23addf3d5e6a8633f9f8eda77efad001671e80b95c491dd0924587ece0b3"}, - {file = "websockets-16.0-cp310-cp310-win32.whl", hash = "sha256:37b31c1623c6605e4c00d466c9d633f9b812ea430c11c8a278774a1fde1acfa9"}, - {file = "websockets-16.0-cp310-cp310-win_amd64.whl", hash = "sha256:8e1dab317b6e77424356e11e99a432b7cb2f3ec8c5ab4dabbcee6add48f72b35"}, - {file = "websockets-16.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:31a52addea25187bde0797a97d6fc3d2f92b6f72a9370792d65a6e84615ac8a8"}, - {file = "websockets-16.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:417b28978cdccab24f46400586d128366313e8a96312e4b9362a4af504f3bbad"}, - {file = "websockets-16.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:af80d74d4edfa3cb9ed973a0a5ba2b2a549371f8a741e0800cb07becdd20f23d"}, - {file = "websockets-16.0-cp311-cp311-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:08d7af67b64d29823fed316505a89b86705f2b7981c07848fb5e3ea3020c1abe"}, - {file = "websockets-16.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7be95cfb0a4dae143eaed2bcba8ac23f4892d8971311f1b06f3c6b78952ee70b"}, - {file = "websockets-16.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d6297ce39ce5c2e6feb13c1a996a2ded3b6832155fcfc920265c76f24c7cceb5"}, - {file = "websockets-16.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:1c1b30e4f497b0b354057f3467f56244c603a79c0d1dafce1d16c283c25f6e64"}, - {file = "websockets-16.0-cp311-cp311-win32.whl", hash = "sha256:5f451484aeb5cafee1ccf789b1b66f535409d038c56966d6101740c1614b86c6"}, - {file = "websockets-16.0-cp311-cp311-win_amd64.whl", hash = "sha256:8d7f0659570eefb578dacde98e24fb60af35350193e4f56e11190787bee77dac"}, - {file = "websockets-16.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:71c989cbf3254fbd5e84d3bff31e4da39c43f884e64f2551d14bb3c186230f00"}, - {file = "websockets-16.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8b6e209ffee39ff1b6d0fa7bfef6de950c60dfb91b8fcead17da4ee539121a79"}, - {file = "websockets-16.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:86890e837d61574c92a97496d590968b23c2ef0aeb8a9bc9421d174cd378ae39"}, - {file = "websockets-16.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:9b5aca38b67492ef518a8ab76851862488a478602229112c4b0d58d63a7a4d5c"}, - {file = "websockets-16.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e0334872c0a37b606418ac52f6ab9cfd17317ac26365f7f65e203e2d0d0d359f"}, - {file = "websockets-16.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a0b31e0b424cc6b5a04b8838bbaec1688834b2383256688cf47eb97412531da1"}, - {file = "websockets-16.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:485c49116d0af10ac698623c513c1cc01c9446c058a4e61e3bf6c19dff7335a2"}, - {file = "websockets-16.0-cp312-cp312-win32.whl", hash = "sha256:eaded469f5e5b7294e2bdca0ab06becb6756ea86894a47806456089298813c89"}, - {file = "websockets-16.0-cp312-cp312-win_amd64.whl", hash = "sha256:5569417dc80977fc8c2d43a86f78e0a5a22fee17565d78621b6bb264a115d4ea"}, - {file = "websockets-16.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:878b336ac47938b474c8f982ac2f7266a540adc3fa4ad74ae96fea9823a02cc9"}, - {file = "websockets-16.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:52a0fec0e6c8d9a784c2c78276a48a2bdf099e4ccc2a4cad53b27718dbfd0230"}, - {file = "websockets-16.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e6578ed5b6981005df1860a56e3617f14a6c307e6a71b4fff8c48fdc50f3ed2c"}, - {file = "websockets-16.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:95724e638f0f9c350bb1c2b0a7ad0e83d9cc0c9259f3ea94e40d7b02a2179ae5"}, - {file = "websockets-16.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c0204dc62a89dc9d50d682412c10b3542d748260d743500a85c13cd1ee4bde82"}, - {file = "websockets-16.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:52ac480f44d32970d66763115edea932f1c5b1312de36df06d6b219f6741eed8"}, - {file = "websockets-16.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6e5a82b677f8f6f59e8dfc34ec06ca6b5b48bc4fcda346acd093694cc2c24d8f"}, - {file = "websockets-16.0-cp313-cp313-win32.whl", hash = "sha256:abf050a199613f64c886ea10f38b47770a65154dc37181bfaff70c160f45315a"}, - {file = "websockets-16.0-cp313-cp313-win_amd64.whl", hash = "sha256:3425ac5cf448801335d6fdc7ae1eb22072055417a96cc6b31b3861f455fbc156"}, - {file = "websockets-16.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8cc451a50f2aee53042ac52d2d053d08bf89bcb31ae799cb4487587661c038a0"}, - {file = "websockets-16.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:daa3b6ff70a9241cf6c7fc9e949d41232d9d7d26fd3522b1ad2b4d62487e9904"}, - {file = "websockets-16.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:fd3cb4adb94a2a6e2b7c0d8d05cb94e6f1c81a0cf9dc2694fb65c7e8d94c42e4"}, - {file = "websockets-16.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:781caf5e8eee67f663126490c2f96f40906594cb86b408a703630f95550a8c3e"}, - {file = "websockets-16.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:caab51a72c51973ca21fa8a18bd8165e1a0183f1ac7066a182ff27107b71e1a4"}, - {file = "websockets-16.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:19c4dc84098e523fd63711e563077d39e90ec6702aff4b5d9e344a60cb3c0cb1"}, - {file = "websockets-16.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a5e18a238a2b2249c9a9235466b90e96ae4795672598a58772dd806edc7ac6d3"}, - {file = "websockets-16.0-cp314-cp314-win32.whl", hash = "sha256:a069d734c4a043182729edd3e9f247c3b2a4035415a9172fd0f1b71658a320a8"}, - {file = "websockets-16.0-cp314-cp314-win_amd64.whl", hash = "sha256:c0ee0e63f23914732c6d7e0cce24915c48f3f1512ec1d079ed01fc629dab269d"}, - {file = "websockets-16.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:a35539cacc3febb22b8f4d4a99cc79b104226a756aa7400adc722e83b0d03244"}, - {file = "websockets-16.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:b784ca5de850f4ce93ec85d3269d24d4c82f22b7212023c974c401d4980ebc5e"}, - {file = "websockets-16.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:569d01a4e7fba956c5ae4fc988f0d4e187900f5497ce46339c996dbf24f17641"}, - {file = "websockets-16.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50f23cdd8343b984957e4077839841146f67a3d31ab0d00e6b824e74c5b2f6e8"}, - {file = "websockets-16.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:152284a83a00c59b759697b7f9e9cddf4e3c7861dd0d964b472b70f78f89e80e"}, - {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bc59589ab64b0022385f429b94697348a6a234e8ce22544e3681b2e9331b5944"}, - {file = "websockets-16.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:32da954ffa2814258030e5a57bc73a3635463238e797c7375dc8091327434206"}, - {file = "websockets-16.0-cp314-cp314t-win32.whl", hash = "sha256:5a4b4cc550cb665dd8a47f868c8d04c8230f857363ad3c9caf7a0c3bf8c61ca6"}, - {file = "websockets-16.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b14dc141ed6d2dde437cddb216004bcac6a1df0935d79656387bd41632ba0bbd"}, - {file = "websockets-16.0-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:349f83cd6c9a415428ee1005cadb5c2c56f4389bc06a9af16103c3bc3dcc8b7d"}, - {file = "websockets-16.0-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:4a1aba3340a8dca8db6eb5a7986157f52eb9e436b74813764241981ca4888f03"}, - {file = "websockets-16.0-pp311-pypy311_pp73-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:f4a32d1bd841d4bcbffdcb3d2ce50c09c3909fbead375ab28d0181af89fd04da"}, - {file = "websockets-16.0-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0298d07ee155e2e9fda5be8a9042200dd2e3bb0b8a38482156576f863a9d457c"}, - {file = "websockets-16.0-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:a653aea902e0324b52f1613332ddf50b00c06fdaf7e92624fbf8c77c78fa5767"}, - {file = "websockets-16.0-py3-none-any.whl", hash = "sha256:1637db62fad1dc833276dded54215f2c7fa46912301a24bd94d45d46a011ceec"}, - {file = "websockets-16.0.tar.gz", hash = "sha256:5f6261a5e56e8d5c42a4497b364ea24d94d9563e8fbd44e78ac40879c60179b5"}, -] - -[[package]] -name = "wheel" -version = "0.46.3" -description = "Command line tool for manipulating wheel files" -optional = false -python-versions = ">=3.9" -groups = ["main"] -files = [ - {file = "wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d"}, - {file = "wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803"}, -] - -[package.dependencies] -packaging = ">=24.0" - -[package.extras] -test = ["pytest (>=6.0.0)", "setuptools (>=77)"] - -[[package]] -name = "wsproto" -version = "1.3.2" -description = "Pure-Python WebSocket protocol implementation" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "wsproto-1.3.2-py3-none-any.whl", hash = "sha256:61eea322cdf56e8cc904bd3ad7573359a242ba65688716b0710a5eb12beab584"}, - {file = "wsproto-1.3.2.tar.gz", hash = "sha256:b86885dcf294e15204919950f666e06ffc6c7c114ca900b060d6e16293528294"}, -] - -[package.dependencies] -h11 = ">=0.16.0,<1" - -[[package]] -name = "zipp" -version = "3.23.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -optional = false -python-versions = ">=3.10" -groups = ["main"] -files = [ - {file = "zipp-3.23.0-py3-none-any.whl", hash = "sha256:071652d6115ed432f5ce1d34c336c0adfd6a884660d1e9712a256d3d3bd4b14e"}, - {file = "zipp-3.23.0.tar.gz", hash = "sha256:a07157588a12518c9d4034df3fbbee09c814741a33ff63c05fa29d26a2404166"}, -] - -[package.extras] -check = ["pytest-checkdocs (>=2.4)", "pytest-ruff (>=0.2.1) ; sys_platform != \"cygwin\""] -cover = ["pytest-cov"] -doc = ["furo", "jaraco.packaging (>=9.3)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-lint"] -enabler = ["pytest-enabler (>=2.2)"] -test = ["big-O", "jaraco.functools", "jaraco.itertools", "jaraco.test", "more_itertools", "pytest (>=6,!=8.1.*)", "pytest-ignore-flaky"] -type = ["pytest-mypy"] - -[metadata] -lock-version = "2.1" -python-versions = "^3.14" -content-hash = "11e7820330aef0a91b8b1b35791aaa335cf1352f21316f2dedaf5439afe474ac" diff --git a/tests/poetry.toml b/tests/poetry.toml deleted file mode 100644 index ab1033b..0000000 --- a/tests/poetry.toml +++ /dev/null @@ -1,2 +0,0 @@ -[virtualenvs] -in-project = true diff --git a/tests/pyproject.toml b/tests/pyproject.toml deleted file mode 100644 index 1907e60..0000000 --- a/tests/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[tool.poetry] -name = "nitter-tests" -version = "0.0.0" -package-mode = false - -[tool.poetry.dependencies] -python = "^3.14" -seleniumbase = "4.46.5" - -[tool.pytest.ini_options] -addopts = "--pls=eager --rcs --reruns=2 --only-rerun=timeout --only-rerun=Timeout --only-rerun=Connection --only-rerun=WebDriverException --timeout_multiplier=5" diff --git a/tests/requirements.txt b/tests/requirements.txt deleted file mode 100644 index e47d1cc..0000000 --- a/tests/requirements.txt +++ /dev/null @@ -1 +0,0 @@ -seleniumbase==4.46.5 diff --git a/tests/test_about_account.py b/tests/test_about_account.py deleted file mode 100644 index 2239d9d..0000000 --- a/tests/test_about_account.py +++ /dev/null @@ -1,76 +0,0 @@ -from base import BaseTestCase, Profile -from parameterized import parameterized - - -class AboutAccount(object): - header = '.about-account-header' - name = '.about-account-name' - body = '.about-account-body' - row = '.about-account-row' - label = '.about-account-label' - value = '.about-account-value' - - -# (username, expected_labels) -# Each label is checked for presence in the page text -about_data = [ - ['jack', ['Date joined', 'Account based in', 'Connected via']], - ['NASA', ['Date joined']], - ['elonmusk', ['Date joined']], -] - -about_verified = [ - ['jack', 'Verified', 'Since '], -] - -about_affiliate = [ - ['jack', 'An affiliate of', 'Square'], - ['elonmusk', 'An affiliate of', 'X'], -] - - -class AboutAccountTest(BaseTestCase): - @parameterized.expand(about_data) - def test_about_page_has_labels(self, username, expected_labels): - """About page shows expected info labels""" - self.open_nitter(f'{username}/about') - self.assert_element_visible(AboutAccount.header) - self.assert_element_visible(AboutAccount.body) - for label in expected_labels: - self.assert_text(label, AboutAccount.body) - - @parameterized.expand(about_verified) - def test_about_verified(self, username, label, value_prefix): - """About page shows verification info for verified accounts""" - self.open_nitter(f'{username}/about') - self.assert_text(label, AboutAccount.body) - self.assert_text(value_prefix, AboutAccount.body) - - @parameterized.expand(about_affiliate) - def test_about_affiliate(self, username, label, affiliate): - """About page shows affiliate info""" - self.open_nitter(f'{username}/about') - self.assert_text(label, AboutAccount.body) - self.assert_text(f'@{affiliate}', AboutAccount.body) - - def test_about_page_title(self): - """Title contains account name""" - self.open_nitter('jack/about') - self.assert_text('jack', AboutAccount.name) - - def test_about_join_date(self): - """About page always shows join date""" - self.open_nitter('jack/about') - self.assert_text('Date joined', AboutAccount.body) - self.assert_text('March 2006', AboutAccount.body) - - def test_about_invalid_user(self): - """About page for non-existent user shows error""" - self.open_nitter('thisprofiledoesntexist/about') - self.assert_text('User "thisprofiledoesntexist" not found') - - def test_joindate_links_to_about(self): - """Join date on profile page links to about page""" - self.open_nitter('jack') - link = self.find_element(Profile.joinDate + ' a') - self.assertIn('/jack/about', link.get_attribute('href')) diff --git a/tests/test_article.py b/tests/test_article.py deleted file mode 100644 index 287b948..0000000 --- a/tests/test_article.py +++ /dev/null @@ -1,327 +0,0 @@ -from base import BaseTestCase -from parameterized import parameterized - - -class ArticleSelectors: - page = '.article-page' - cover = '.article-cover' - body = '.article-body' - title = '.article-title' - author = '.article-author' - fullname = '.article-author .fullname' - username = '.article-author .username' - date = '.article-author .article-date' - avatar = '.article-author img.avatar' - verified = '.article-author .verified-icon' - media = '.article-media' - caption = '.article-media-caption' - divider = '.article-divider' - - -articles = [ - ['2064166507438059759', - '1s,秒杀一切,开源一个 X 文章发布 Skill【重磅升级】', - 'punk2898', 'Punk'], - - ['2064689664213041529', - 'SpaceX Thesis & Valuation Memorandum', - 'Dialectic_Group', 'Dialectic'], - - ['2064691088636424322', - 'Consciousness and AI: The Problem of Inner Experience', - 'CosmicOrFun', 'Cosmic Orphan'], - - ['2064755789391110154', - 'DeFi Markets Update 2026-06-10', - 'SteakhouseFi', 'Steakhouse Financial'], - - ['2064755231901319527', - 'The machine economy has a killswitch and somebody just pulled it.', - '1914ad', 'Justin Bechler HMP-028'], - - ['2062858677149675788', - 'Yakshinis', - 'CosmicOrFun', 'Cosmic Orphan'], -] - -articles_with_media = [ - ['2064166507438059759', 6], - ['2064689664213041529', 11], - ['2064755789391110154', 5], -] - -articles_with_dividers = [ - ['2064166507438059759', 1], - ['2064689664213041529', 6], -] - - -class ArticleBasicTest(BaseTestCase): - @parameterized.expand(articles) - def test_article_loads(self, tweet_id, title, username, fullname): - self.open_nitter(f'i/article/{tweet_id}') - self.assert_element_visible(ArticleSelectors.page) - self.assert_element_visible(ArticleSelectors.body) - self.assert_text(title, ArticleSelectors.title) - - @parameterized.expand(articles) - def test_article_author(self, tweet_id, title, username, fullname): - self.open_nitter(f'i/article/{tweet_id}') - self.assert_element_visible(ArticleSelectors.author) - self.assert_text(f'@{username}', ArticleSelectors.username) - - @parameterized.expand(articles) - def test_article_has_cover(self, tweet_id, title, username, fullname): - self.open_nitter(f'i/article/{tweet_id}') - self.assert_element_visible(ArticleSelectors.cover) - src = self.get_attribute(ArticleSelectors.cover, 'src') - self.assertIn('/pic/', src) - - @parameterized.expand(articles) - def test_article_has_date(self, tweet_id, title, username, fullname): - self.open_nitter(f'i/article/{tweet_id}') - date_text = self.get_text(ArticleSelectors.date) - self.assertTrue(len(date_text) > 3) - - @parameterized.expand(articles) - def test_article_author_avatar(self, tweet_id, title, username, fullname): - self.open_nitter(f'i/article/{tweet_id}') - self.assert_element_visible(ArticleSelectors.avatar) - src = self.get_attribute(ArticleSelectors.avatar, 'src') - self.assertIn('/pic/', src) - self.assertGreater(len(src), len('/pic/')) - - @parameterized.expand(articles) - def test_article_author_verified(self, tweet_id, title, username, fullname): - self.open_nitter(f'i/article/{tweet_id}') - self.assert_element_visible(ArticleSelectors.verified) - - def test_article_author_verified_business(self): - self.open_nitter('i/article/2064755789391110154') - self.assert_element_visible('.article-author .verified-icon.business') - - -class ArticleContentTest(BaseTestCase): - def test_article_has_paragraphs(self): - self.open_nitter('i/article/2064689664213041529') - paragraphs = self.find_elements('.article-body p') - self.assertGreater(len(paragraphs), 10) - - def test_article_has_headers(self): - self.open_nitter('i/article/2064689664213041529') - headers = self.find_elements('.article-body h1, .article-body h2') - self.assertGreater(len(headers), 5) - - def test_article_has_bold_text(self): - self.open_nitter('i/article/2064166507438059759') - bold = self.find_elements('.article-body strong') - self.assertGreater(len(bold), 0) - - def test_article_has_italic_text(self): - self.open_nitter('i/article/2064166507438059759') - italic = self.find_elements('.article-body em') - self.assertGreater(len(italic), 0) - - def test_article_has_blockquotes(self): - self.open_nitter('i/article/2064166507438059759') - self.assert_element_visible('.article-body blockquote') - - def test_article_has_lists(self): - self.open_nitter('i/article/2064166507438059759') - self.assert_element_visible('.article-body ul') - - def test_article_has_emoji_text(self): - self.open_nitter('i/article/2064166507438059759') - body = self.get_text(ArticleSelectors.body) - self.assertTrue(any(ord(c) > 0x1F000 for c in body)) - - def test_article_has_links(self): - self.open_nitter('i/article/2064691088636424322') - links = self.find_elements('.article-body a[href]') - self.assertGreater(len(links), 0) - - def test_article_twitter_links_localized(self): - self.open_nitter('i/article/2064755789391110154') - links = self.find_elements('.article-body a[href^="https://x.com"]') - self.assertEqual(len(links), 0, 'x.com links should be converted to local paths') - - @parameterized.expand(articles_with_media) - def test_article_media_count(self, tweet_id, expected_count): - self.open_nitter(f'i/article/{tweet_id}') - media = self.find_elements(ArticleSelectors.media) - self.assertEqual(len(media), expected_count) - - @parameterized.expand(articles_with_dividers) - def test_article_divider_count(self, tweet_id, expected_count): - self.open_nitter(f'i/article/{tweet_id}') - dividers = self.find_elements(ArticleSelectors.divider) - self.assertEqual(len(dividers), expected_count) - - -class ArticleMediaTest(BaseTestCase): - def test_media_images_proxied(self): - self.open_nitter('i/article/2064689664213041529') - self.assert_element_visible(ArticleSelectors.media) - img = self.find_element(f'{ArticleSelectors.media} img') - src = img.get_attribute('src') - self.assertIn('/pic/', src) - self.assertFalse(src.startswith('https://pbs.twimg.com')) - - def test_cover_image_proxied(self): - self.open_nitter('i/article/2064689664213041529') - self.assert_element_visible(ArticleSelectors.cover) - src = self.get_attribute(ArticleSelectors.cover, 'src') - self.assertIn('/pic/', src) - self.assertFalse(src.startswith('https://pbs.twimg.com')) - - def test_embedded_tweet(self): - self.open_nitter('i/article/2064755789391110154') - self.assert_element_visible('.article-body .timeline-item') - - def test_multiple_embedded_tweets(self): - self.open_nitter('i/article/2064755231901319527') - tweets = self.find_elements('.article-body .timeline-item') - self.assertGreaterEqual(len(tweets), 3) - - def test_media_caption_displayed(self): - self.open_nitter('i/article/2064689664213041529') - self.assert_element_visible(ArticleSelectors.caption) - captions = self.find_elements(ArticleSelectors.caption) - self.assertGreaterEqual(len(captions), 5) - - def test_media_caption_text(self): - self.open_nitter('i/article/2064689664213041529') - self.assert_text_visible('FIGURE 1', ArticleSelectors.caption) - - def test_media_caption_alt_attribute(self): - self.open_nitter('i/article/2064689664213041529') - img = self.find_element(f'{ArticleSelectors.media} img') - alt = img.get_attribute('alt') - self.assertGreater(len(alt), 0) - - def test_no_caption_when_absent(self): - self.open_nitter('i/article/2062858677149675788') - captions = self.find_elements(ArticleSelectors.caption) - self.assertEqual(len(captions), 0) - - -class ArticleMentionTest(BaseTestCase): - def test_mention_linkified(self): - self.open_nitter('i/article/2064755231901319527') - link = self.find_element('.article-body a[href="/ZachXBT"]') - self.assertEqual(link.text, '@ZachXBT') - - def test_multiple_mentions_linkified(self): - self.open_nitter('i/article/2064755231901319527') - links = self.find_elements('.article-body a[href^="/"]') - mention_hrefs = [l.get_attribute('href') for l in links - if l.text.startswith('@')] - usernames = [h.split('/')[-1] for h in mention_hrefs] - self.assertIn('ZachXBT', usernames) - self.assertIn('River', usernames) - - def test_mention_in_different_article(self): - self.open_nitter('i/article/2064689664213041529') - link = self.find_element('.article-body a[href="/FutureJurvetson"]') - self.assertEqual(link.text, '@FutureJurvetson') - - def test_no_spurious_whitespace_in_styled_paragraph(self): - """Styled paragraphs should not have extra whitespace from VNode serialization.""" - self.open_nitter('i/article/2064166507438059759') - source = self.get_page_source() - self.assertNotIn('white-space: pre-wrap', source) - self.assertNotIn('white-space:pre-wrap', source) - - -class ArticleCardTest(BaseTestCase): - @parameterized.expand(articles) - def test_status_page_shows_article_card(self, tweet_id, title, username, fullname): - self.open_nitter(f'{username}/status/{tweet_id}') - self.assert_element_visible('.article-card') - self.assert_text(title, '.article-card .card-title') - - def test_article_card_has_cover_image(self): - self.open_nitter('Dialectic_Group/status/2064689664213041529') - self.assert_element_visible('.article-card .card-image img') - src = self.get_attribute('.article-card .card-image img', 'src') - self.assertIn('/pic/', src) - - def test_article_card_has_badge(self): - self.open_nitter('Dialectic_Group/status/2064689664213041529') - self.assert_element_visible('.article-card-badge') - self.assert_text('Article', '.article-card-badge') - - def test_article_card_has_preview_text(self): - self.open_nitter('CosmicOrFun/status/2064691088636424322') - self.assert_element_visible('.article-card .card-description') - - def test_article_card_links_to_article(self): - self.open_nitter('punk2898/status/2064166507438059759') - href = self.get_attribute('.article-card .card-container', 'href') - self.assertIn('/article/', href) - - def test_article_url_stripped_from_tweet_text(self): - self.open_nitter('punk2898/status/2064166507438059759') - self.assert_element_visible('.article-card') - source = self.get_page_source() - # Main tweet text should not contain article URL - import re - main = re.search(r'id="m".*?tweet-content[^>]*>(.*?)', source, re.DOTALL) - self.assertIsNotNone(main) - self.assertNotIn('/article/', main.group(1)) - - -class ArticleQuotedCardTest(BaseTestCase): - """Article cards inside quoted tweets (1914ad quoting own article).""" - quoted_tweet = '1914ad/status/2064789532071891085' - quoted_article_id = '2063677483548102688' - - def test_quoted_card_visible(self): - self.open_nitter(self.quoted_tweet) - self.assert_element_visible('.quote .article-card') - - def test_quoted_card_has_title(self): - self.open_nitter(self.quoted_tweet) - self.assert_text('David Bailey Already Won', '.quote .article-card .card-title') - - def test_quoted_card_has_badge(self): - self.open_nitter(self.quoted_tweet) - self.assert_element_visible('.quote .article-card-badge') - self.assert_text('Article', '.quote .article-card-badge') - - def test_quoted_card_has_cover_image(self): - self.open_nitter(self.quoted_tweet) - # Scroll to element to trigger lazy loading - self.scroll_to('.quote .article-card .card-image img') - self.assert_element_visible('.quote .article-card .card-image img') - src = self.get_attribute('.quote .article-card .card-image img', 'src') - self.assertIn('/pic/', src) - - def test_quoted_card_has_description(self): - self.open_nitter(self.quoted_tweet) - self.assert_element_visible('.quote .article-card .card-description') - - def test_quoted_card_links_to_article(self): - self.open_nitter(self.quoted_tweet) - href = self.get_attribute('.quote .article-card .card-container', 'href') - self.assertIn(f'/article/{self.quoted_article_id}', href) - - -class ArticleRoutingTest(BaseTestCase): - def test_username_article_route_redirects(self): - self.open_nitter('punk2898/article/2064166507438059759') - self.assert_element_visible(ArticleSelectors.page) - self.assert_text('1s', ArticleSelectors.title) - - def test_status_article_route_redirects(self): - self.open_nitter('punk2898/status/2064166507438059759/article') - self.assert_element_visible(ArticleSelectors.page) - self.assert_text('1s', ArticleSelectors.title) - - def test_invalid_id_returns_404(self): - self.open_nitter('i/article/notanumber') - self.assert_element_not_visible(ArticleSelectors.page) - - def test_nonexistent_article(self): - self.open_nitter('i/article/1') - self.assert_element_visible('.error-panel') diff --git a/tests/test_card.py b/tests/test_card.py index daee099..7fd233c 100644 --- a/tests/test_card.py +++ b/tests/test_card.py @@ -1,10 +1,13 @@ -import os -import unittest from base import BaseTestCase, Card, Conversation from parameterized import parameterized card = [ + ['Thom_Wolf/status/1122466524860702729', + 'pytorch/fairseq', + 'Facebook AI Research Sequence-to-Sequence Toolkit written in Python. - GitHub - pytorch/fairseq: Facebook AI Research Sequence-to-Sequence Toolkit written in Python.', + 'github.com', True], + ['nim_lang/status/1136652293510717440', 'Version 0.20.0 released', 'We are very proud to announce Nim version 0.20. This is a massive release, both literally and figuratively. It contains more than 1,000 commits and it marks our release candidate for version 1.0!', @@ -15,46 +18,70 @@ card = [ 'Basic OBS Studio plugin, written in nim, supporting C++ (C fine too) - obsplugin.nim', 'gist.github.com', True], - ['NASA/status/2061872347477418301', - 'Nancy Grace Roman Space Telescope - NASA Science', - 'The Nancy Grace Roman Space Telescope will settle essential questions in the areas of dark energy, exoplanets, and astrophysics.', - 'science.nasa.gov', True] + ['FluentAI/status/1116417904831029248', + 'Amazon’s Alexa isn’t just AI — thousands of humans are listening', + 'One of the only ways to improve Alexa is to have human beings check it for errors', + 'theverge.com', True] ] no_thumb = [ - ['Thom_Wolf/status/1122466524860702729', - 'GitHub - facebookresearch/XLM: PyTorch original implementation of Cross-lingual Language Model', - 'PyTorch original implementation of Cross-lingual Language Model Pretraining.', - 'github.com'], + ['Bountysource/status/1141879700639215617', + 'Post a bounty on kivy/plyer!', + 'Automation and Screen Reader Support', + 'bountysource.com'], ['brent_p/status/1088857328680488961', - 'GitHub - brentp/hts-nim: nim wrapper for htslib for parsing genomics data files', - '', - 'github.com'], + 'Hts Nim Sugar', + 'hts-nim is a library that allows one to use htslib via the nim programming language. Nim is a garbage-collected language that compiles to C and often has similar performance. I have become very...', + 'brentp.github.io'], ['voidtarget/status/1133028231672582145', 'sinkingsugar/nimqt-example', 'A sample of a Qt app written using mostly nim. Contribute to sinkingsugar/nimqt-example development by creating an account on GitHub.', - 'github.com'] + 'github.com'], + + ['mobile_test/status/490378953744318464', + 'Nantasket Beach', + 'Explore this photo titled Nantasket Beach by Ben Sandofsky (@sandofsky) on 500px', + '500px.com'], + + ['nim_lang/status/1082989146040340480', + 'Nim in 2018: A short recap', + 'Posted in r/programming by u/miran1', + 'reddit.com'] ] playable = [ - ['NASA/status/2047048645845897398', - 'NASA\'s Artemis II News Conference with Moon Astronauts', - 'Live from NASA\'s Johnson Space Center in Houston', + ['nim_lang/status/1118234460904919042', + 'Nim development blog 2019-03', + 'Arne (aka Krux02)* debugging: * improved nim-gdb, $ works, framefilter * alias for --debugger:native: -g* bugs: * forwarding of .pure. * sizeof union* fe...', + 'youtube.com'], + + ['nim_lang/status/1121090879823986688', + 'Nim - First natively compiled language w/ hot code-reloading at...', + '#nim #c++ #ACCUConfNim is a statically typed systems and applications programming language which offers perhaps some of the most powerful metaprogramming cap...', 'youtube.com'] ] +# promo = [ + # ['BangOlufsen/status/1145698701517754368', + # 'Upgrade your journey', '', + # 'www.bang-olufsen.com'], + + # ['BangOlufsen/status/1154934429900406784', + # 'Learn more about Beosound Shape', '', + # 'www.bang-olufsen.com'] +# ] + + class CardTest(BaseTestCase): @parameterized.expand(card) def test_card(self, tweet, title, description, destination, large): - if os.environ.get('GITHUB_ACTIONS') == 'true' and '2061872347477418301' in tweet: - self.skipTest('Card image unreliable from GitHub datacenter IPs') self.open_nitter(tweet) c = Card(Conversation.main + " ") self.assert_text(title, c.title) self.assert_text(destination, c.destination) - self.assertIn('/pic/', self.get_image_url(c.image + ' img')) + self.assertIn('_img', self.get_image_url(c.image + ' img')) if len(description) > 0: self.assert_text(description, c.description) if large: @@ -71,13 +98,23 @@ class CardTest(BaseTestCase): if len(description) > 0: self.assert_text(description, c.description) - @parameterized.expand(playable, skip_on_empty=True) + @parameterized.expand(playable) def test_card_playable(self, tweet, title, description, destination): self.open_nitter(tweet) c = Card(Conversation.main + " ") self.assert_text(title, c.title) self.assert_text(destination, c.destination) - self.assertIn('/pic/', self.get_image_url(c.image + ' img')) + self.assertIn('_img', self.get_image_url(c.image + ' img')) self.assert_element_visible('.card-overlay') if len(description) > 0: self.assert_text(description, c.description) + + # @parameterized.expand(promo) + # def test_card_promo(self, tweet, title, description, destination): + # self.open_nitter(tweet) + # c = Card(Conversation.main + " ") + # self.assert_text(title, c.title) + # self.assert_text(destination, c.destination) + # self.assert_element_visible('.video-overlay') + # if len(description) > 0: + # self.assert_text(description, c.description) diff --git a/tests/test_community.py b/tests/test_community.py deleted file mode 100644 index b266044..0000000 --- a/tests/test_community.py +++ /dev/null @@ -1,211 +0,0 @@ -from base import BaseTestCase -from parameterized import parameterized - - -COMMUNITY_ID = '1493446837214187523' -COMMUNITY_PATH = f'i/communities/{COMMUNITY_ID}' - - -class CommunityTest(BaseTestCase): - def test_top_page_loads(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.community-header') - self.assert_text('Build in Public', '.community-name') - - def test_banner_visible(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.timeline-banner img') - - def test_member_count(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.community-member-count') - self.assert_text('Members', '.community-member-count') - - def test_description_visible(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.community-description') - - def test_tabs_present(self): - self.open_nitter(COMMUNITY_PATH) - tabs = self.find_elements('.tab a') - labels = [t.text for t in tabs] - self.assertEqual(labels, ['Top', 'Latest', 'Media', 'About']) - - def test_top_tab_active(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.tab .active a[href$="/' + COMMUNITY_ID + '"]') - - def test_top_has_tweets(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.timeline-item .tweet-body') - - def test_top_has_pagination(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.show-more') - self.assert_text('Load more', '.show-more') - - def test_latest_has_tweets(self): - self.open_nitter(f'{COMMUNITY_PATH}/latest') - self.assert_element_visible('.timeline-item .tweet-body') - - def test_latest_tab_active(self): - self.open_nitter(f'{COMMUNITY_PATH}/latest') - self.assert_element_visible('.tab .active a[href$="/latest"]') - - def test_media_has_tweets(self): - self.open_nitter(f'{COMMUNITY_PATH}/media') - self.assert_element_visible('.timeline-item .tweet-body') - - def test_media_tab_active(self): - self.open_nitter(f'{COMMUNITY_PATH}/media') - self.assert_element_visible('.tab .active a[href$="/media"]') - - def test_about_page(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_element_visible('.community-about') - self.assert_text('Community Info', '.community-info h2') - - def test_about_rules(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_element_visible('.community-rules') - self.assert_text('Rules', '.community-rules h2') - rules = self.find_elements('.community-rule') - self.assertGreater(len(rules), 0) - - def test_about_creator(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_text('Created', '.community-info') - link = self.find_element('.community-info-item a') - self.assertTrue(link.text.startswith('@')) - self.assertGreater(len(link.text), 1) - - def test_about_tab_active(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_element_visible('.tab .active a[href$="/about"]') - - def test_about_moderators(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_element_visible('.community-moderators') - self.assert_text('Moderators', '.community-moderators h2') - mods = self.find_elements('.community-moderator') - self.assertGreater(len(mods), 0) - - def test_about_moderators_have_avatars(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - avatars = self.find_elements('.community-mod-avatar') - self.assertGreater(len(avatars), 0) - - def test_about_moderators_link_to_profiles(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - links = self.find_elements('.community-mod-username') - self.assertGreater(len(links), 0) - for link in links: - self.assertTrue(link.text.startswith('@')) - self.assertTrue(link.get_attribute('href').startswith('http')) - - def test_about_see_all_link(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - link = self.find_element('.community-mods-link') - self.assertEqual(link.text, 'See all') - self.assertIn('/moderators', link.get_attribute('href')) - - def test_members_page(self): - self.open_nitter(f'{COMMUNITY_PATH}/members') - self.assert_element_visible('.timeline-item') - users = self.find_elements('.timeline-item .username') - self.assertGreater(len(users), 0) - - def test_members_has_member_tabs(self): - self.open_nitter(f'{COMMUNITY_PATH}/members') - tabs = self.find_elements('.tab a') - labels = [t.text for t in tabs] - self.assertEqual(labels, ['All', 'Moderators']) - - def test_members_all_tab_active(self): - self.open_nitter(f'{COMMUNITY_PATH}/members') - self.assert_element_visible('.tab .active a[href$="/members"]') - - def test_members_count_is_link(self): - self.open_nitter(COMMUNITY_PATH) - link = self.find_element('.community-member-count') - self.assertIn('Members', link.text) - self.assertIn('/members', link.get_attribute('href')) - - def test_moderators_page(self): - self.open_nitter(f'{COMMUNITY_PATH}/moderators') - self.assert_element_visible('.timeline-item') - users = self.find_elements('.timeline-item .username') - self.assertGreater(len(users), 0) - - def test_moderators_tab_active(self): - self.open_nitter(f'{COMMUNITY_PATH}/moderators') - self.assert_element_visible('.tab .active a[href$="/moderators"]') - - def test_moderators_has_member_tabs(self): - self.open_nitter(f'{COMMUNITY_PATH}/moderators') - tabs = self.find_elements('.tab a') - labels = [t.text for t in tabs] - self.assertEqual(labels, ['All', 'Moderators']) - - def test_pinned_tweet_label(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.pinned') - self.assert_text('Pinned by Community mods', '.pinned') - - def test_hashtags_visible(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.community-tags') - tags = self.find_elements('.community-tag') - self.assertGreater(len(tags), 0) - - def test_hashtags_are_links(self): - self.open_nitter(COMMUNITY_PATH) - tags = self.find_elements('.community-tag') - for tag in tags: - href = tag.get_attribute('href') - self.assertIn('/hashtag/', href) - self.assertTrue(tag.text.startswith('#')) - - def test_hashtag_page(self): - self.open_nitter(f'{COMMUNITY_PATH}/hashtag/buildinpublic') - self.assert_element_visible('.timeline-item .tweet-body') - - def test_hashtag_shows_header(self): - self.open_nitter(f'{COMMUNITY_PATH}/hashtag/buildinpublic') - self.assert_element_visible('.community-header') - self.assert_text('Build in Public', '.community-name') - - def test_hashtag_shows_tag_title(self): - self.open_nitter(f'{COMMUNITY_PATH}/hashtag/buildinpublic') - self.assert_element_visible('.community-hashtag-header') - self.assert_text('#buildinpublic', '.community-hashtag-title') - - def test_hashtag_no_main_tabs(self): - self.open_nitter(f'{COMMUNITY_PATH}/hashtag/buildinpublic') - tabs = self.find_elements('.tab a') - tab_labels = [t.text for t in tabs] - self.assertNotIn('Top', tab_labels) - self.assertNotIn('About', tab_labels) - - def test_category_visible(self): - self.open_nitter(COMMUNITY_PATH) - self.assert_element_visible('.community-category') - - def test_about_join_policy(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_text('Anyone can join', '.community-info') - - def test_about_visibility_note(self): - self.open_nitter(f'{COMMUNITY_PATH}/about') - self.assert_text('publicly visible', '.community-info') - - def test_404_invalid_id(self): - self.open_nitter('i/communities/999') - self.assert_element_visible('.error-panel') - self.assert_text('not found', '.error-panel') - - @parameterized.expand(['', '/latest', '/media', '/about', - '/members', '/moderators']) - def test_page_no_error(self, suffix): - self.open_nitter(f'{COMMUNITY_PATH}{suffix}') - self.assert_element_not_visible('.error-panel') diff --git a/tests/test_embed.py b/tests/test_embed.py deleted file mode 100644 index cc7a28a..0000000 --- a/tests/test_embed.py +++ /dev/null @@ -1,282 +0,0 @@ -import os -import unittest -import requests -from base import BaseTestCase, Media -from parameterized import parameterized - - -class Embed: - container = '.tweet-embed' - footer = '.embed-footer' - tweet_content = '.tweet-content' - tweet_header = '.tweet-header' - fullname = '.fullname' - username = '.username' - avatar = '.avatar' - stats = '.tweet-stats' - quote = '.quote' - error_panel = '.error-panel' - - -class TweetEmbedTest(BaseTestCase): - """Test tweet embed rendering.""" - tweet = 'elonmusk/status/1141367104702038016' - - def test_embed_container_visible(self): - self.open_nitter(self.tweet + '/embed') - self.assert_element_visible(Embed.container) - - def test_embed_has_footer(self): - self.open_nitter(self.tweet + '/embed') - self.assert_element_visible(Embed.footer) - self.assert_text_visible('Read more on', Embed.footer) - - def test_embed_has_tweet_content(self): - self.open_nitter(self.tweet + '/embed') - self.assert_element_visible(Embed.tweet_content) - - def test_embed_has_avatar(self): - self.open_nitter(self.tweet + '/embed') - self.assert_element_visible(Embed.avatar) - - def test_embed_has_username(self): - self.open_nitter(self.tweet + '/embed') - self.assert_element_visible(Embed.username) - - def test_embed_has_stats(self): - self.open_nitter(self.tweet + '/embed') - self.assert_element_visible(Embed.stats) - - def test_embed_footer_links_to_tweet(self): - self.open_nitter(self.tweet + '/embed') - href = self.get_attribute(Embed.footer, 'href') - self.assertIn('/elonmusk/status/1141367104702038016', href) - - -class TweetEmbedMediaTest(BaseTestCase): - """Test embed rendering with various media types.""" - - def test_embed_with_image(self): - self.open_nitter('mobile_test/status/519364660823207936/embed') - self.assert_element_visible(Embed.container) - self.scroll_to(Media.container) - self.assert_element_visible(Media.image) - - def test_embed_with_gif(self): - self.open_nitter('elonmusk/status/1141367104702038016/embed') - self.assert_element_visible(Embed.container) - self.scroll_to(Media.container) - self.assert_element_visible(Media.gif) - - @unittest.skipIf(os.environ.get('GITHUB_ACTIONS') == 'true', - 'TweetResultByRestId is Cloudflare-blocked from GitHub datacenter IPs') - def test_embed_with_video(self): - self.open_nitter('d0m96/status/1078373829917974528/embed') - self.assert_element_visible(Embed.container) - self.scroll_to(Media.container) - self.assert_element_visible(Media.video) - - def test_embed_with_gallery(self): - self.open_nitter('mobile_test/status/451108446603980803/embed') - self.assert_element_visible(Embed.container) - self.scroll_to(Media.container) - self.assert_element_visible(Media.row) - - -class TweetEmbedQuoteTest(BaseTestCase): - """Test embed rendering with quoted tweets.""" - - def test_embed_with_quote_shows_quote(self): - self.open_nitter('elonmusk/status/1138827760107790336/embed') - self.assert_element_visible(Embed.container) - self.assert_element_visible(Embed.quote) - - def test_embed_quote_has_content(self): - self.open_nitter('elonmusk/status/1138827760107790336/embed') - quote = self.find_element(Embed.quote) - self.assertIsNotNone(quote.text) - - -class EmbedErrorTest(BaseTestCase): - """Test embed error handling.""" - - def test_nonexistent_tweet_shows_error(self): - self.open_nitter('nobody/status/1/embed') - self.assert_element_visible('.tweet-embed.error-embed') - self.assert_text_visible('not found', Embed.error_panel) - - def test_protected_account_embed_shows_error(self): - self.open_nitter('mobile_test_7/status/1/embed') - self.assert_element_visible('.tweet-embed.error-embed') - - def test_invalid_tweet_id_shows_error(self): - self.open_nitter('jack/status/notanumber/embed') - self.assert_element_visible('.tweet-embed.error-embed') - - -class OEmbedApiTest(BaseTestCase): - """Test oEmbed API endpoint.""" - base_url = 'http://localhost:8080' - tweet_url = 'https://twitter.com/elonmusk/status/1141367104702038016' - - def test_oembed_returns_json(self): - resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}') - self.assertEqual(resp.status_code, 200) - self.assertEqual(resp.headers['Content-Type'], 'application/json') - - def test_oembed_has_required_fields(self): - resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}') - data = resp.json() - self.assertEqual(data['type'], 'rich') - self.assertEqual(data['version'], '1.0') - self.assertIn('html', data) - self.assertIn('author_name', data) - self.assertIn('provider_name', data) - - def test_oembed_html_contains_iframe(self): - resp = requests.get(f'{self.base_url}/api/oembed?url={self.tweet_url}') - data = resp.json() - self.assertIn(' resolves the numeric id and redirects to the profile (issue #1433)""" - self.open_nitter(f'i/user/{user_id}') - self.assert_true(self.get_current_url().rstrip('/').endswith(f'/{username}')) - self.assert_exact_text(f'@{username}', Profile.username) - - @parameterized.expand(id_redirects) - def test_intent_user_redirect(self, user_id, username): - """/intent/user?user_id= redirects to the profile (issue #1433)""" - self.open_nitter(f'intent/user?user_id={user_id}') - self.assert_true(self.get_current_url().rstrip('/').endswith(f'/{username}')) - self.assert_exact_text(f'@{username}', Profile.username) diff --git a/tests/test_quote.py b/tests/test_quote.py index 53ad79f..0daec58 100644 --- a/tests/test_quote.py +++ b/tests/test_quote.py @@ -2,13 +2,15 @@ from base import BaseTestCase, Quote, Conversation from parameterized import parameterized text = [ - ['nim_lang/status/1491461266849808397#m', - 'Nim', '@nim_lang', - """What's better than Nim 1.6.0? + ['elonmusk/status/1138136540096319488', + 'Tesla Owners Online', '@Model3Owners', + """As of March 58.4% of new car sales in Norway are electric. -Nim 1.6.2 :) +What are we doing wrong? reuters.com/article/us-norwa…"""], -nim-lang.org/blog/2021/12/17…"""] + ['nim_lang/status/924694255364341760', + 'Hacker News', '@newsycombinator', + 'Why Rust fails hard at scientific computing andre-ratsimbazafy.com/why-r…'] ] image = [ diff --git a/tests/test_reply_sort.py b/tests/test_reply_sort.py deleted file mode 100644 index 1d89e5d..0000000 --- a/tests/test_reply_sort.py +++ /dev/null @@ -1,37 +0,0 @@ -from parameterized import parameterized - -from base import BaseTestCase, Conversation - -sort_modes = [ - ['jack/status/20', 'Relevant'], - ['jack/status/20?sort=relevance', 'Relevant'], - ['jack/status/20?sort=recency', 'Recent'], - ['jack/status/20?sort=likes', 'Liked'], - ['jack/status/20?sort=garbage', 'Relevant'], - ['jack/status/20?sort=%3Cscript%3E', 'Relevant'], -] - - -class ReplySortTest(BaseTestCase): - @parameterized.expand(sort_modes) - def test_active_mode(self, page, expected_active): - self.open_nitter(page) - self.assert_element_visible(Conversation.reply_sort) - active = self.get_text(Conversation.reply_sort_active) - self.assert_equal(active.strip(), expected_active) - - def test_all_three_options_present(self): - self.open_nitter('jack/status/20') - options = self.find_elements('.reply-sort-option') - labels = [o.text.strip() for o in options] - self.assert_equal(labels, ['Relevant', 'Recent', 'Liked']) - - def test_option_links_carry_sort_param(self): - self.open_nitter('jack/status/20') - for slug in ['Relevance', 'Recency', 'Likes']: - self.assert_element(f'.reply-sort-option[href="?sort={slug}#r"]') - - def test_load_more_preserves_sort(self): - self.open_nitter('jack/status/20?sort=Likes') - href = self.get_attribute('.replies .show-more a', 'href') - self.assert_true('sort=Likes' in href, f'sort missing from: {href}') diff --git a/tests/test_search.py b/tests/test_search.py index 0f5456f..80ee36a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -1,129 +1,9 @@ +from base import BaseTestCase from parameterized import parameterized -from base import BaseTestCase, Search -# [url, expected active tab label] -active_tabs = [ - ['search?f=tweets&q=nasa', 'Latest'], - ['search?f=top&q=nasa', 'Top'], - ['search?f=media&q=nasa', 'Media'], - ['search?f=users&q=nasa', 'Users'], - ['search?f=lists&q=test', 'Lists'], - # unknown/hostile values fall back to Latest - ['search?f=garbage&q=nasa', 'Latest'], - ['search?f=%3Cscript%3E&q=nasa', 'Latest'], - # x.com URL compat: f=live/user/list (f=media/top match natively) - ['search?f=live&q=nasa', 'Latest'], - ['search?f=user&q=nasa', 'Users'], - ['search?f=list&q=test', 'Lists'], -] - -results_pages = [ - ['search?f=tweets&q=nasa'], - ['search?f=top&q=nasa'], - ['search?f=media&q=nasa'], -] - - -class SearchProductTest(BaseTestCase): - @parameterized.expand(active_tabs) - def test_active_tab(self, page, expected_active): - self.open_nitter(page) - active = self.get_text(Search.tab_active) - self.assert_equal(active.strip(), expected_active) - - def test_all_tabs_present(self): - self.open_nitter('search?f=tweets&q=nasa') - tabs = self.find_elements(Search.tab_item) - labels = [t.text.strip() for t in tabs] - self.assert_equal(labels, ['Top', 'Latest', 'Media', 'Users', 'Lists']) - - @parameterized.expand(results_pages) - def test_results_render(self, page): - self.open_nitter(page) - self.assert_element('.timeline .timeline-item') - - def test_tab_links_carry_kind(self): - self.open_nitter('search?f=tweets&q=nasa') - self.assert_element('.tab-item a[href="?f=top&q=nasa"]') - self.assert_element('.tab-item a[href="?f=media&q=nasa"]') - self.assert_element('.tab-item a[href="?f=tweets&q=nasa"]') - self.assert_element('.tab-item a[href="?f=users&q=nasa"]') - self.assert_element('.tab-item a[href="?f=lists&q=nasa"]') - - def test_show_more_preserves_kind(self): - self.open_nitter('search?f=media&q=nasa') - href = self.get_attribute('.show-more a', 'href') - self.assert_true('f=media' in href, f'f=media missing from: {href}') - - def test_search_form_preserves_kind(self): - self.open_nitter('search?f=top&q=nasa') - self.assert_element_present('.search-field input[name="f"][value="top"]') - - def test_media_operators_compose(self): - self.open_nitter('search?f=media&q=nasa&e-nativeretweets=on') - self.assert_element('.timeline .timeline-item') - - @parameterized.expand([['DAAC'], ['AB'], ['maxid:'], ['maxid:abc']]) - def test_garbage_cursor_no_crash(self, cursor): - # short/invalid cursors must render the page, not a 500 error - self.open_nitter(f'search?f=media&q=nasa&cursor={cursor}') - self.assert_element(Search.tab_active) - - def test_no_results(self): - self.open_nitter('search?f=media&q=xkqzjwv_no_results_2026') - self.assert_text('No items found', '.timeline-none') - - def test_list_results_render(self): - self.open_nitter('search?f=lists&q=test') - self.assert_element('.timeline-item.list-result') - self.assert_element('.list-result .list-name') - self.assert_element('.list-result .list-members') - - def test_list_card_links_to_list(self): - self.open_nitter('search?f=lists&q=test') - href = self.get_attribute('.list-result .list-name', 'href') - self.assert_true('/i/lists/' in href, f'unexpected list link: {href}') - - def test_list_row_clickable(self): - self.open_nitter('search?f=lists&q=test') - href = self.get_attribute('.list-result a.tweet-link', 'href') - self.assert_true('/i/lists/' in href, f'unexpected row link: {href}') - - def test_list_avatar_links_to_user(self): - self.open_nitter('search?f=lists&q=test') - # the avatar link in a row must point at the user named in that row - row = '.list-result:has(a.facepile-link)' - self.assert_element(f'{row} a.facepile-link > img') - href = self.get_attribute(f'{row} a.facepile-link', 'href') - ctx = self.get_text(f'{row} .list-result-context') - mentioned = ctx.split('@')[-1].strip() - self.assert_true(href.endswith('/' + mentioned), - f'avatar link {href} does not match @{mentioned}') - - def test_list_pagination_preserves_kind(self): - self.open_nitter('search?f=lists&q=test') - href = self.get_attribute('.show-more a', 'href') - self.assert_true('f=lists' in href, f'f=lists missing from: {href}') - - def test_list_garbage_cursor_no_crash(self): - self.open_nitter('search?f=lists&q=test&cursor=DAAC') - self.assert_element(Search.tab_active) - - def test_media_view_tabs_present(self): - self.open_nitter('search?f=media&q=nasa') - tabs = self.find_elements('.media-view-tabs .tab-item') - labels = [t.text.strip() for t in tabs] - self.assert_equal(labels, ['Timeline', 'Grid', 'Gallery']) - - def test_media_view_grid(self): - self.open_nitter('search?f=media&q=nasa&view=grid') - self.assert_element('.timeline.media-grid-view') - - def test_media_view_gallery(self): - self.open_nitter('search?f=media&q=nasa&view=gallery') - self.assert_element('.timeline.media-gallery-view .gallery-masonry') - - def test_media_view_tabs_only_on_media(self): - self.open_nitter('search?f=tweets&q=nasa') - self.assert_element_not_present('.media-view-tabs') +class SearchTest(BaseTestCase): + @parameterized.expand([['@mobile_test'], ['@mobile_test_2']]) + def test_username_search(self, username): + self.search_username(username) + self.assert_text(f'{username}') diff --git a/tests/test_security.py b/tests/test_security.py deleted file mode 100644 index 04ba680..0000000 --- a/tests/test_security.py +++ /dev/null @@ -1,60 +0,0 @@ -import subprocess -from parameterized import parameterized - -BASE_URL = 'http://localhost:8080' - - -def curl_status(url): - """Get HTTP status code using curl to avoid URL normalization by Python libs.""" - result = subprocess.run( - ['curl', '-s', '-o', '/dev/null', '-w', '%{http_code}', url], - capture_output=True, text=True, timeout=30 - ) - return int(result.stdout) - - -class TestMalformedPaths: - """Test that malformed paths don't crash the server. - - URLs like //foo are parsed as having 'foo' as the authority (host), - resulting in an empty path. Empty paths previously crashed jester's - static file handler. Now they return 400. - - URLs like //foo/bar are parsed as authority='foo', path='/bar', - so they route normally (not empty path). - """ - - @parameterized.expand([ - # These parse to empty paths -> 400 - ('//lefty_rae', 400), - ('//test', 400), - ('//anyuser', 400), - ]) - def test_empty_path_returns_400(self, path, expected_status): - """URLs that parse to empty paths should return 400, not crash.""" - status = curl_status(f'{BASE_URL}{path}') - assert status == expected_status, \ - f'Expected {expected_status} for {path}, got {status}' - - @parameterized.expand([ - ('/jack', 200), - ('/about', 200), - ('/', 200), - ]) - def test_normal_paths_work(self, path, expected_status): - """Normal paths should still work.""" - status = curl_status(f'{BASE_URL}{path}') - assert status == expected_status, \ - f'Expected {expected_status} for {path}, got {status}' - - def test_server_survives_malformed_requests(self): - """Server should handle malformed requests without crashing.""" - # These all parse to empty paths - malformed_paths = ['//a', '//b', '//c', '//user', '//test'] - for path in malformed_paths: - status = curl_status(f'{BASE_URL}{path}') - assert status == 400, f'Expected 400 for {path}, got {status}' - - # Verify server is still responding after malformed requests - status = curl_status(f'{BASE_URL}/') - assert status == 200, 'Server should still be alive' diff --git a/tests/test_space.py b/tests/test_space.py deleted file mode 100644 index d2ba672..0000000 --- a/tests/test_space.py +++ /dev/null @@ -1,119 +0,0 @@ -# SPDX-License-Identifier: AGPL-3.0-only -"""Integration tests for Twitter Spaces support.""" -import pytest -from seleniumbase import BaseCase - - -class TestSpacePage(BaseCase): - """Tests for /i/spaces/@id route.""" - - SPACE_ID = "1mxPaaRAwYjKN" - SPACE_URL = f"http://localhost:8080/i/spaces/{SPACE_ID}" - - def test_space_page_loads(self): - """Space page should load with title.""" - self.open(self.SPACE_URL) - self.assert_element(".space-page") - self.assert_element(".space-panel") - self.assert_text_visible("INTEL WILL MOON NEXT WEEK", ".space-title") - - def test_space_host_info(self): - """Space should display host in participants.""" - self.open(self.SPACE_URL) - self.assert_element(".space-participants") - self.assert_text_visible("bubble boi") - self.assert_element(".host-badge") - - def test_space_metadata(self): - """Space should display listener count and state.""" - self.open(self.SPACE_URL) - self.assert_element(".space-meta") - # Should show listener count (number format) - meta_text = self.get_text(".space-meta") - assert any(c.isdigit() for c in meta_text), "Should show listener count" - # Should show ended state - assert "Ended" in meta_text or "Jun" in meta_text, "Should show ended state" - - def test_space_participants(self): - """Space should display host and speakers.""" - self.open(self.SPACE_URL) - self.assert_element(".space-participants") - # Host should have badge - self.assert_element(".host-badge") - self.assert_text_visible("Host", ".host-badge") - # Should show speakers - self.assert_text_visible("CANTELOPEPEEL") - self.assert_text_visible("Based Burner Account") - self.assert_text_visible("anon invests") - - def test_space_participant_avatars(self): - """Participant avatars should load correctly.""" - self.open(self.SPACE_URL) - # Check avatars in participants section - avatars = self.find_elements(".space-participant img") - assert len(avatars) >= 4, "Should have at least 4 participant avatars" - for avatar in avatars: - src = avatar.get_attribute("src") - # Should NOT be double-encoded - assert "%2Fpic%2F" not in src, f"Avatar URL double-encoded: {src}" - # Should have valid path - assert "/pic/" in src, f"Avatar URL missing /pic/: {src}" - - def test_space_player_hls_disabled(self): - """Without HLS, should show enable button with video-overlay style.""" - self.open(self.SPACE_URL) - self.assert_element(".space-player") - self.assert_element(".video-overlay") - # Should show duration in overlay-duration - self.assert_element(".overlay-duration") - # Should have enable button - source = self.get_page_source() - assert "Enable hls playback" in source - - def test_space_player_hls_enabled(self): - """With HLS enabled, should have audio element in DOM.""" - self.open(self.SPACE_URL) - # Set HLS preference via cookie - self.add_cookie({"name": "hlsPlayback", "value": "on"}) - self.refresh() - # Check page source for audio element (hidden until play clicked) - source = self.get_page_source() - assert '