Full Code of iawia002/lux for AI

master dd00f6d258d8 cached
186 files
449.2 KB
156.3k tokens
554 symbols
1 requests
Download .txt
Showing preview only (491K chars total). Download the full file or copy to clipboard to get everything.
Repository: iawia002/lux
Branch: master
Commit: dd00f6d258d8
Files: 186
Total size: 449.2 KB

Directory structure:
gitextract_1wxrt7hr/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── can-not-download-video.md
│   │   └── support-a-new-website.md
│   └── workflows/
│       ├── builder.yml
│       ├── ci.yml
│       ├── goreleaser.yml
│       ├── stream_acfun.yml
│       ├── stream_bcy.yml
│       ├── stream_bilibili.yml
│       ├── stream_bitchute.yml
│       ├── stream_douyin.yml
│       ├── stream_douyu.yml
│       ├── stream_eporner.yml
│       ├── stream_facebook.yml
│       ├── stream_geekbang.yml
│       ├── stream_haokan.yml
│       ├── stream_hupu.yml
│       ├── stream_huya.yml
│       ├── stream_instagram.yml
│       ├── stream_iqiyi.yml
│       ├── stream_ixigua.yml
│       ├── stream_kuaishou.yml
│       ├── stream_mgtv.yml
│       ├── stream_miaopai.yml
│       ├── stream_netease.yml
│       ├── stream_odysee.yml
│       ├── stream_pinterest.yml
│       ├── stream_pixivision.yml
│       ├── stream_pornhub.yml
│       ├── stream_qq.yml
│       ├── stream_reddit.yml
│       ├── stream_rumble.yml
│       ├── stream_streamtape.yml
│       ├── stream_tangdou.yml
│       ├── stream_threads.yml
│       ├── stream_tiktok.yml
│       ├── stream_tumblr.yml
│       ├── stream_twitter.yml
│       ├── stream_udn.yml
│       ├── stream_vimeo.yml
│       ├── stream_vk.yml
│       ├── stream_weibo.yml
│       ├── stream_xiaohongshu.yml
│       ├── stream_ximalaya.yml
│       ├── stream_xinpianchang.yml
│       ├── stream_xvideos.yml
│       ├── stream_yinyuetai.yml
│       ├── stream_youku.yml
│       ├── stream_youtube.yml
│       ├── stream_zhihu.yml
│       └── stream_zingmp3.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── Cask.toml
├── LICENSE
├── README.md
├── app/
│   ├── app.go
│   └── register.go
├── codecov.yml
├── config/
│   └── config.go
├── downloader/
│   ├── downloader.go
│   ├── downloader_test.go
│   ├── types.go
│   └── utils.go
├── extractors/
│   ├── acfun/
│   │   ├── acfun.go
│   │   ├── acfun_test.go
│   │   └── types.go
│   ├── bcy/
│   │   ├── bcy.go
│   │   └── bcy_test.go
│   ├── bilibili/
│   │   ├── bilibili.go
│   │   ├── bilibili_test.go
│   │   └── types.go
│   ├── bitchute/
│   │   ├── bitchute.go
│   │   └── bitchute_test.go
│   ├── douyin/
│   │   ├── douyin.go
│   │   ├── douyin_test.go
│   │   ├── sign.js
│   │   └── types.go
│   ├── douyu/
│   │   ├── douyu.go
│   │   └── douyu_test.go
│   ├── eporner/
│   │   ├── eporner.go
│   │   └── eporner_test.go
│   ├── errors.go
│   ├── extractors.go
│   ├── facebook/
│   │   ├── facebook.go
│   │   └── facebook_test.go
│   ├── geekbang/
│   │   ├── geekbang.go
│   │   └── geekbang_test.go
│   ├── haokan/
│   │   ├── haokan.go
│   │   └── haokan_test.go
│   ├── hupu/
│   │   ├── hupu.go
│   │   └── hupu_test.go
│   ├── huya/
│   │   ├── huya.go
│   │   └── huya_test.go
│   ├── instagram/
│   │   ├── instagram.go
│   │   └── instagram_test.go
│   ├── iqiyi/
│   │   ├── iqiyi.go
│   │   └── iqiyi_test.go
│   ├── ixigua/
│   │   ├── ixigua.go
│   │   ├── ixigua_test.go
│   │   └── types.go
│   ├── kuaishou/
│   │   ├── kuaishou.go
│   │   └── kuaishou_test.go
│   ├── mgtv/
│   │   ├── mgtv.go
│   │   └── mgtv_test.go
│   ├── miaopai/
│   │   ├── miaopai.go
│   │   └── miaopai_test.go
│   ├── netease/
│   │   ├── netease.go
│   │   └── netease_test.go
│   ├── odysee/
│   │   ├── odysee.go
│   │   └── odysee_test.go
│   ├── pinterest/
│   │   ├── pinterest.go
│   │   └── pinterest_test.go
│   ├── pixivision/
│   │   ├── pixivision.go
│   │   └── pixivision_test.go
│   ├── pornhub/
│   │   ├── pornhub.go
│   │   └── pornhub_test.go
│   ├── qq/
│   │   ├── qq.go
│   │   └── qq_test.go
│   ├── reddit/
│   │   ├── reddit.go
│   │   └── reddit_test.go
│   ├── rumble/
│   │   ├── rumble.go
│   │   └── rumble_test.go
│   ├── streamtape/
│   │   ├── streamtape.go
│   │   └── streamtape_test.go
│   ├── tangdou/
│   │   ├── tangdou.go
│   │   └── tangdou_test.go
│   ├── threads/
│   │   ├── threads.go
│   │   └── threads_test.go
│   ├── tiktok/
│   │   ├── tiktok.go
│   │   └── tiktok_test.go
│   ├── tumblr/
│   │   ├── tumblr.go
│   │   └── tumblr_test.go
│   ├── twitter/
│   │   ├── twitter.go
│   │   └── twitter_test.go
│   ├── types.go
│   ├── udn/
│   │   ├── udn.go
│   │   └── udn_test.go
│   ├── universal/
│   │   ├── universal.go
│   │   └── universal_test.go
│   ├── vimeo/
│   │   ├── vimeo.go
│   │   └── vimeo_test.go
│   ├── vk/
│   │   ├── vk.go
│   │   └── vk_test.go
│   ├── weibo/
│   │   ├── weibo.go
│   │   └── weibo_test.go
│   ├── xiaohongshu/
│   │   ├── xiaohongshu.go
│   │   └── xiaohongshu_test.go
│   ├── ximalaya/
│   │   ├── types.go
│   │   ├── ximalaya.go
│   │   └── ximalaya_test.go
│   ├── xinpianchang/
│   │   ├── xinpianchang.go
│   │   └── xinpianchang_test.go
│   ├── xvideos/
│   │   ├── xvideos.go
│   │   └── xvideos_test.go
│   ├── yinyuetai/
│   │   ├── types.go
│   │   ├── yinyuetai.go
│   │   └── yinyuetai_test.go
│   ├── youku/
│   │   ├── youku.go
│   │   └── youku_test.go
│   ├── youtube/
│   │   ├── youtube.go
│   │   └── youtube_test.go
│   ├── zhihu/
│   │   ├── types.go
│   │   ├── zhihu.go
│   │   └── zhihu_test.go
│   └── zingmp3/
│       ├── zingmp3.go
│       └── zingmp3_test.go
├── go.mod
├── go.sum
├── main.go
├── parser/
│   ├── parser.go
│   └── parser_test.go
├── request/
│   ├── request.go
│   └── request_test.go
├── script/
│   ├── generate_github_action_template.js
│   └── github_action_template.yml
├── test/
│   └── utils.go
└── utils/
    ├── download.go
    ├── download_test.go
    ├── ffmpeg.go
    ├── pool.go
    ├── pool_test.go
    ├── utils.go
    └── utils_test.go

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitattributes
================================================
*.go text eol=lf
*.md text eol=lf


================================================
FILE: .github/ISSUE_TEMPLATE/can-not-download-video.md
================================================
---
name: Can not download video
about: If you find that a website download is not working
title: "[download fail]: the website name"
labels: bug
assignees: ''

---

<!-- 请输入网站的名称 -->
<!-- enter the website name -->

**Website name**: name

<!-- 你的操作系统 -->
<!-- your operating system -->

**OS:**: Windows/Linux/macOS

<!-- 视频链接地址 -->
<!-- the video url -->

**Video URL:**: url

<!-- 请输入下载时显示的错误信息 -->
<!-- enter the error message when downloading -->

**Stack overflow**

```
error message here
```

<!-- 如果能提供截图,则对于解决你的问题非常有帮助 -->
<!-- If you can provide screenshots, it will be very helpful to solve your problem. -->

**Screenshots**

none

<!-- 其他信息 -->
<!-- add any other context about the problem here -->

**Additional context**

none


================================================
FILE: .github/ISSUE_TEMPLATE/support-a-new-website.md
================================================
---
name: Support a new website
about: If you want to request support for a new website
title: "[new website require]: the website name"
labels: enhancement
assignees: ''

---

<!-- 请输入网站的名称。例如 抖音 -->
<!-- enter the website name. eg. TikTok -->

- **Website name**: name

<!-- 请输入视频/音频资源地址。 例如 https://video.example.com/v/123456 -->
<!-- enter the video/radio url. eg. https://video.example.com/v/123456 -->

- **Stream link**: url


================================================
FILE: .github/workflows/builder.yml
================================================
name: Builder

on:
  push:
    branches: "*"
    paths:
      - "**/*.go"
      - "go.mod"
      - "go.sum"
      - ".github/workflows/builder.yml"
  pull_request:
    branches: "*"
    paths:
      - "**/*.go"
      - "go.mod"
      - "go.sum"
      - ".github/workflows/builder.yml"
  workflow_dispatch:

env:
  PRODUCT: lux
  CGO_ENABLED: 0
  GO111MODULE: on

jobs:
  build:
    name: Build
    strategy:
      matrix:
        os: [ linux, freebsd, openbsd, dragonfly, windows, darwin ]
        arch: [ amd64, 386 ]
        include:
          - os: linux
            arch: arm
            arm: 5
          - os: linux
            arch: arm
            arm: 6
          - os: linux
            arch: arm
            arm: 7
          - os: linux
            arch: arm64
          - os: linux
            arch: mips
            mips: softfloat
          - os: linux
            arch: mips
            mips: hardfloat
          - os: linux
            arch: mipsle
            mipsle: softfloat
          - os: linux
            arch: mipsle
            mipsle: hardfloat
          - os: linux
            arch: mips64
          - os: linux
            arch: mips64le
          - os: linux
            arch: ppc64
          - os: linux
            arch: ppc64le
          - os: linux
            arch: s390x
          - os: windows
            arch: arm
          - os: android
            arch: arm64
          - os: darwin
            arch: arm64
          - os: freebsd
            arch: arm64
        exclude:
          - os: darwin
            arch: 386
          - os: dragonfly
            arch: 386
      fail-fast: false
    runs-on: ubuntu-latest
    continue-on-error: true
    env:
      GOOS: ${{ matrix.os }}
      GOARCH: ${{ matrix.arch }}
      GOARM: ${{ matrix.arm }}
      GOMIPS: ${{ matrix.mips }}
      GOMIPS64: ${{ matrix.mips64 }}
      GOMIPSLE: ${{ matrix.mipsle }}
    steps:
    - name: Set up Go
      uses: actions/setup-go@v5
      with:
        go-version: 1.24

    - name: Check out code base
      if: github.event_name == 'push'
      uses: actions/checkout@v4
      with:
        fetch-depth: 0

    - name: Check out code base
      if: github.event_name == 'pull_request'
      uses: actions/checkout@v4
      with:
        fetch-depth: 0
        ref: ${{ github.event.pull_request.head.sha }}

    - name: Cache go module
      uses: actions/cache@v4
      with:
        path: ~/go/pkg/mod
        key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }}
        restore-keys: ${{ runner.os }}-go-

    - name: Get dependencies
      run: |
        go get -v -t -d ./...

    - name: Build binary
      id: builder
      run: |
        ARGS="${GOOS}-${GOARCH}"
        if [[ -n "${GOARM}" ]]; then
          ARGS="${ARGS}v${GOARM}"
        elif [[ -n "${GOMIPS}" ]]; then
          ARGS="${ARGS}-${GOMIPS}"
        elif [[ -n "${GOMIPS64}" ]]; then
          ARGS="${ARGS}-${GOMIPS64}"
        elif [[ -n "${GOMIPSLE}" ]]; then
          ARGS="${ARGS}-${GOMIPSLE}"
        fi
        go build -trimpath --ldflags "-s -w -buildid=" -v -o ./bin/${{ env.PRODUCT }}-${ARGS}
        echo "::set-output name=filename::${{ env.PRODUCT }}-${ARGS}"

    - name: Upload binary artifacts
      uses: actions/upload-artifact@v4
      with:
        name: ${{ steps.builder.outputs.filename }}
        path: ./bin/${{ env.PRODUCT }}*
        if-no-files-found: error


================================================
FILE: .github/workflows/ci.yml
================================================
name: ci
on:
  push:
  pull_request:
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"
jobs:
  ci:
    runs-on: ${{ matrix.os }}
    timeout-minutes: 30
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest, macOS-latest]
    name: Go ${{ matrix.go }} in ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}
      - name: Environment
        run: |
          go version
          go env
      - name: Lint
        uses: golangci/golangci-lint-action@v5
        with:
          version: v1.64.7
          only-new-issues: true
      - name: Test
        env:
          GOFLAGS: -mod=mod
        run: go test -race -coverpkg=./... -coverprofile=coverage.txt ./...
      - name: Send coverage
        run: bash <(curl -s https://codecov.io/bash)


================================================
FILE: .github/workflows/goreleaser.yml
================================================
name: goreleaser
on:
  push:
    tags:
      - '*'
permissions:
  contents: write
jobs:
  goreleaser:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: 1.24
      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v5
        with:
          version: latest
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GORELEASER_TOKEN }}


================================================
FILE: .github/workflows/stream_acfun.yml
================================================
name: acfun

on:
  push:
    paths:
      - "extractors/acfun/*.go"
      - ".github/workflows/stream_acfun.yml"
  pull_request:
    paths:
      - "extractors/acfun/*.go"
      - ".github/workflows/stream_acfun.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/acfun


================================================
FILE: .github/workflows/stream_bcy.yml
================================================
name: bcy

on:
  push:
    paths:
      - "extractors/bcy/*.go"
      - ".github/workflows/stream_bcy.yml"
  pull_request:
    paths:
      - "extractors/bcy/*.go"
      - ".github/workflows/stream_bcy.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/bcy


================================================
FILE: .github/workflows/stream_bilibili.yml
================================================
name: bilibili

on:
  push:
    paths:
      - "extractors/bilibili/*.go"
      - ".github/workflows/stream_bilibili.yml"
  pull_request:
    paths:
      - "extractors/bilibili/*.go"
      - ".github/workflows/stream_bilibili.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/bilibili


================================================
FILE: .github/workflows/stream_bitchute.yml
================================================
name: bitchute

on:
  push:
    paths:
      - "extractors/bitchute/*.go"
      - ".github/workflows/stream_bitchute.yml"
  pull_request:
    paths:
      - "extractors/bitchute/*.go"
      - ".github/workflows/stream_bitchute.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/bitchute


================================================
FILE: .github/workflows/stream_douyin.yml
================================================
name: douyin

on:
  push:
    paths:
      - "extractors/douyin/*.go"
      - ".github/workflows/stream_douyin.yml"
  pull_request:
    paths:
      - "extractors/douyin/*.go"
      - ".github/workflows/stream_douyin.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/douyin


================================================
FILE: .github/workflows/stream_douyu.yml
================================================
name: douyu

on:
  push:
    paths:
      - "extractors/douyu/*.go"
      - ".github/workflows/stream_douyu.yml"
  pull_request:
    paths:
      - "extractors/douyu/*.go"
      - ".github/workflows/stream_douyu.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/douyu


================================================
FILE: .github/workflows/stream_eporner.yml
================================================
name: eporner

on:
  push:
    paths:
      - "extractors/eporner/*.go"
      - ".github/workflows/stream_eporner.yml"
  pull_request:
    paths:
      - "extractors/eporner/*.go"
      - ".github/workflows/stream_eporner.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/eporner


================================================
FILE: .github/workflows/stream_facebook.yml
================================================
name: facebook

on:
  push:
    paths:
      - "extractors/facebook/*.go"
      - ".github/workflows/stream_facebook.yml"
  pull_request:
    paths:
      - "extractors/facebook/*.go"
      - ".github/workflows/stream_facebook.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/facebook


================================================
FILE: .github/workflows/stream_geekbang.yml
================================================
name: geekbang

on:
  push:
    paths:
      - "extractors/geekbang/*.go"
      - ".github/workflows/stream_geekbang.yml"
  pull_request:
    paths:
      - "extractors/geekbang/*.go"
      - ".github/workflows/stream_geekbang.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/geekbang


================================================
FILE: .github/workflows/stream_haokan.yml
================================================
name: haokan

on:
  push:
    paths:
      - "extractors/haokan/*.go"
      - ".github/workflows/stream_haokan.yml"
  pull_request:
    paths:
      - "extractors/haokan/*.go"
      - ".github/workflows/stream_haokan.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/haokan


================================================
FILE: .github/workflows/stream_hupu.yml
================================================
name: hupu

on:
  push:
    paths:
      - "extractors/hupu/*.go"
      - ".github/workflows/stream_hupu.yml"
  pull_request:
    paths:
      - "extractors/hupu/*.go"
      - ".github/workflows/stream_hupu.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/hupu


================================================
FILE: .github/workflows/stream_huya.yml
================================================
name: huya

on:
  push:
    paths:
      - "extractors/huya/*.go"
      - ".github/workflows/stream_huya.yml"
  pull_request:
    paths:
      - "extractors/huya/*.go"
      - ".github/workflows/stream_huya.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/huya


================================================
FILE: .github/workflows/stream_instagram.yml
================================================
name: instagram

on:
  push:
    paths:
      - "extractors/instagram/*.go"
      - ".github/workflows/stream_instagram.yml"
  pull_request:
    paths:
      - "extractors/instagram/*.go"
      - ".github/workflows/stream_instagram.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/instagram


================================================
FILE: .github/workflows/stream_iqiyi.yml
================================================
name: iqiyi

on:
  push:
    paths:
      - "extractors/iqiyi/*.go"
      - ".github/workflows/stream_iqiyi.yml"
  pull_request:
    paths:
      - "extractors/iqiyi/*.go"
      - ".github/workflows/stream_iqiyi.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/iqiyi


================================================
FILE: .github/workflows/stream_ixigua.yml
================================================
name: ixigua

on:
  push:
    paths:
      - "extractors/ixigua/*.go"
      - ".github/workflows/stream_ixigua.yml"
  pull_request:
    paths:
      - "extractors/ixigua/*.go"
      - ".github/workflows/stream_ixigua.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/ixigua


================================================
FILE: .github/workflows/stream_kuaishou.yml
================================================
name: kuaishou

on:
  push:
    paths:
      - "extractors/kuaishou/*.go"
      - ".github/workflows/stream_kuaishou.yml"
  pull_request:
    paths:
      - "extractors/kuaishou/*.go"
      - ".github/workflows/stream_kuaishou.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/kuaishou


================================================
FILE: .github/workflows/stream_mgtv.yml
================================================
name: mgtv

on:
  push:
    paths:
      - "extractors/mgtv/*.go"
      - ".github/workflows/stream_mgtv.yml"
  pull_request:
    paths:
      - "extractors/mgtv/*.go"
      - ".github/workflows/stream_mgtv.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/mgtv


================================================
FILE: .github/workflows/stream_miaopai.yml
================================================
name: miaopai

on:
  push:
    paths:
      - "extractors/miaopai/*.go"
      - ".github/workflows/stream_miaopai.yml"
  pull_request:
    paths:
      - "extractors/miaopai/*.go"
      - ".github/workflows/stream_miaopai.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/miaopai


================================================
FILE: .github/workflows/stream_netease.yml
================================================
name: netease

on:
  push:
    paths:
      - "extractors/netease/*.go"
      - ".github/workflows/stream_netease.yml"
  pull_request:
    paths:
      - "extractors/netease/*.go"
      - ".github/workflows/stream_netease.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/netease


================================================
FILE: .github/workflows/stream_odysee.yml
================================================
name: odysee

on:
  push:
    paths:
      - "extractors/odysee/*.go"
      - ".github/workflows/stream_odysee.yml"
  pull_request:
    paths:
      - "extractors/odysee/*.go"
      - ".github/workflows/stream_odysee.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/odysee


================================================
FILE: .github/workflows/stream_pinterest.yml
================================================
name: pinterest

on:
  push:
    paths:
      - "extractors/pinterest/*.go"
      - ".github/workflows/stream_pinterest.yml"
  pull_request:
    paths:
      - "extractors/pinterest/*.go"
      - ".github/workflows/stream_pinterest.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/pinterest


================================================
FILE: .github/workflows/stream_pixivision.yml
================================================
name: pixivision

on:
  push:
    paths:
      - "extractors/pixivision/*.go"
      - ".github/workflows/stream_pixivision.yml"
  pull_request:
    paths:
      - "extractors/pixivision/*.go"
      - ".github/workflows/stream_pixivision.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/pixivision


================================================
FILE: .github/workflows/stream_pornhub.yml
================================================
name: pornhub

on:
  push:
    paths:
      - "extractors/pornhub/*.go"
      - ".github/workflows/stream_pornhub.yml"
  pull_request:
    paths:
      - "extractors/pornhub/*.go"
      - ".github/workflows/stream_pornhub.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/pornhub


================================================
FILE: .github/workflows/stream_qq.yml
================================================
name: qq

on:
  push:
    paths:
      - "extractors/qq/*.go"
      - ".github/workflows/stream_qq.yml"
  pull_request:
    paths:
      - "extractors/qq/*.go"
      - ".github/workflows/stream_qq.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/qq


================================================
FILE: .github/workflows/stream_reddit.yml
================================================
name: reddit

on:
  push:
    paths:
      - "extractors/reddit/*.go"
      - ".github/workflows/stream_reddit.yml"
  pull_request:
    paths:
      - "extractors/reddit/*.go"
      - ".github/workflows/stream_reddit.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/reddit


================================================
FILE: .github/workflows/stream_rumble.yml
================================================
name: rumble

on:
  push:
    paths:
      - "extractors/rumble/*.go"
      - ".github/workflows/stream_rumble.yml"
  pull_request:
    paths:
      - "extractors/rumble/*.go"
      - ".github/workflows/stream_rumble.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/rumble


================================================
FILE: .github/workflows/stream_streamtape.yml
================================================
name: streamtape

on:
  push:
    paths:
      - "extractors/streamtape/*.go"
      - ".github/workflows/stream_streamtape.yml"
  pull_request:
    paths:
      - "extractors/streamtape/*.go"
      - ".github/workflows/stream_streamtape.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/streamtape


================================================
FILE: .github/workflows/stream_tangdou.yml
================================================
name: tangdou

on:
  push:
    paths:
      - "extractors/tangdou/*.go"
      - ".github/workflows/stream_tangdou.yml"
  pull_request:
    paths:
      - "extractors/tangdou/*.go"
      - ".github/workflows/stream_tangdou.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/tangdou


================================================
FILE: .github/workflows/stream_threads.yml
================================================
name: instagram

on:
  push:
    paths:
      - "extractors/threads/*.go"
      - ".github/workflows/stream_threads.yml"
  pull_request:
    paths:
      - "extractors/threads/*.go"
      - ".github/workflows/stream_threads.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/threads


================================================
FILE: .github/workflows/stream_tiktok.yml
================================================
name: tiktok

on:
  push:
    paths:
      - "extractors/tiktok/*.go"
      - ".github/workflows/stream_tiktok.yml"
  pull_request:
    paths:
      - "extractors/tiktok/*.go"
      - ".github/workflows/stream_tiktok.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/tiktok


================================================
FILE: .github/workflows/stream_tumblr.yml
================================================
name: tumblr

on:
  push:
    paths:
      - "extractors/tumblr/*.go"
      - ".github/workflows/stream_tumblr.yml"
  pull_request:
    paths:
      - "extractors/tumblr/*.go"
      - ".github/workflows/stream_tumblr.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/tumblr


================================================
FILE: .github/workflows/stream_twitter.yml
================================================
name: twitter

on:
  push:
    paths:
      - "extractors/twitter/*.go"
      - ".github/workflows/stream_twitter.yml"
  pull_request:
    paths:
      - "extractors/twitter/*.go"
      - ".github/workflows/stream_twitter.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/twitter


================================================
FILE: .github/workflows/stream_udn.yml
================================================
name: udn

on:
  push:
    paths:
      - "extractors/udn/*.go"
      - ".github/workflows/stream_udn.yml"
  pull_request:
    paths:
      - "extractors/udn/*.go"
      - ".github/workflows/stream_udn.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/udn


================================================
FILE: .github/workflows/stream_vimeo.yml
================================================
name: vimeo

on:
  push:
    paths:
      - "extractors/vimeo/*.go"
      - ".github/workflows/stream_vimeo.yml"
  pull_request:
    paths:
      - "extractors/vimeo/*.go"
      - ".github/workflows/stream_vimeo.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/vimeo


================================================
FILE: .github/workflows/stream_vk.yml
================================================
name: vk

on:
  push:
    paths:
      - "extractors/vk/*.go"
      - ".github/workflows/stream_vk.yml"
  pull_request:
    paths:
      - "extractors/vk/*.go"
      - ".github/workflows/stream_vk.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/vk


================================================
FILE: .github/workflows/stream_weibo.yml
================================================
name: weibo

on:
  push:
    paths:
      - "extractors/weibo/*.go"
      - ".github/workflows/stream_weibo.yml"
  pull_request:
    paths:
      - "extractors/weibo/*.go"
      - ".github/workflows/stream_weibo.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/weibo


================================================
FILE: .github/workflows/stream_xiaohongshu.yml
================================================
name: xiaohongshu

on:
  push:
    paths:
      - "extractors/xiaohongshu/*.go"
      - ".github/workflows/stream_xiaohongshu.yml"
  pull_request:
    paths:
      - "extractors/xiaohongshu/*.go"
      - ".github/workflows/stream_xiaohongshu.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/xiaohongshu


================================================
FILE: .github/workflows/stream_ximalaya.yml
================================================
name: ximalaya

on:
  push:
    paths:
      - "extractors/ximalaya/*.go"
      - ".github/workflows/stream_ximalaya.yml"
  pull_request:
    paths:
      - "extractors/ximalaya/*.go"
      - ".github/workflows/stream_ximalaya.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/ximalaya


================================================
FILE: .github/workflows/stream_xinpianchang.yml
================================================
name: xinpianchang

on:
  push:
    paths:
      - "extractors/xinpianchang/*.go"
      - ".github/workflows/stream_xinpianchang.yml"
  pull_request:
    paths:
      - "extractors/xinpianchang/*.go"
      - ".github/workflows/stream_xinpianchang.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/xinpianchang


================================================
FILE: .github/workflows/stream_xvideos.yml
================================================
name: xvideos

on:
  push:
    paths:
      - "extractors/xvideos/*.go"
      - ".github/workflows/stream_xvideos.yml"
  pull_request:
    paths:
      - "extractors/xvideos/*.go"
      - ".github/workflows/stream_xvideos.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/xvideos


================================================
FILE: .github/workflows/stream_yinyuetai.yml
================================================
name: yinyuetai

on:
  push:
    paths:
      - "extractors/yinyuetai/*.go"
      - ".github/workflows/stream_yinyuetai.yml"
  pull_request:
    paths:
      - "extractors/yinyuetai/*.go"
      - ".github/workflows/stream_yinyuetai.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/yinyuetai


================================================
FILE: .github/workflows/stream_youku.yml
================================================
name: youku

on:
  push:
    paths:
      - "extractors/youku/*.go"
      - ".github/workflows/stream_youku.yml"
  pull_request:
    paths:
      - "extractors/youku/*.go"
      - ".github/workflows/stream_youku.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/youku


================================================
FILE: .github/workflows/stream_youtube.yml
================================================
name: youtube

on:
  push:
    paths:
      - "extractors/youtube/*.go"
      - ".github/workflows/stream_youtube.yml"
  pull_request:
    paths:
      - "extractors/youtube/*.go"
      - ".github/workflows/stream_youtube.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/youtube


================================================
FILE: .github/workflows/stream_zhihu.yml
================================================
name: zhihu

on:
  push:
    paths:
      - "extractors/zhihu/*.go"
      - ".github/workflows/stream_zhihu.yml"
  pull_request:
    paths:
      - "extractors/zhihu/*.go"
      - ".github/workflows/stream_zhihu.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/zhihu


================================================
FILE: .github/workflows/stream_zingmp3.yml
================================================
name: zingmp3

on:
  push:
    paths:
      - "extractors/zingmp3/*.go"
      - ".github/workflows/stream_zingmp3.yml"
  pull_request:
    paths:
      - "extractors/zingmp3/*.go"
      - ".github/workflows/stream_zingmp3.yml"
  schedule:
    # run ci weekly
    - cron: "0 0 * * 0"

jobs:
  test:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        go: ["1.24"]
        os: [ubuntu-latest]
    name: ${{ matrix.os }}
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-go@v5
        with:
          go-version: ${{ matrix.go }}

      - name: Test
        run: go test -timeout 5m -race -coverpkg=./... -coverprofile=coverage.txt github.com/iawia002/lux/extractors/zingmp3


================================================
FILE: .gitignore
================================================
# Binaries for programs and plugins
*.exe
*.exe~
*.dll
*.so
*.dylib

# Test binary, build with `go test -c`
*.test

# Output of the go coverage tool, specifically when used with LiteIDE
*.out
coverage.txt

# Python
*.pyc

.vscode
.idea
dist/
*_token
downloader/*.jpg

# Ignore compiled binaries
# - native
annie
# - gox builds
annie_*

# macOS
.DS_Store

*.mp4
*.mkv
*.webm

lux


================================================
FILE: .golangci.yml
================================================
run:
  concurrency: 2
  timeout: 5m
  go: 1.24

linter-settings:
  goconst:
    min-len: 2
    min-occurrences: 2

linters:
  enable:
    - bodyclose
    - errcheck
    - goconst
    - gofmt
    - goimports
    - gosimple
    - govet
    - ineffassign
    - misspell
    - nilerr
    - staticcheck
    - typecheck
    - unconvert
    - unparam
    - unused
    - whitespace

issues:
  exclude-use-default: false
  exclude-rules:
    - path: _test.go
      linters:
        - errcheck


================================================
FILE: .goreleaser.yml
================================================
project_name: lux
env:
  - GO111MODULE=on
  - CGO_ENABLED=0
before:
  hooks:
    - go mod download
builds:
- binary: lux
  ldflags: -s -w -X github.com/iawia002/lux/app.version={{ .RawVersion }}
  goos:
    - windows
    - darwin
    - linux
    - freebsd
    - openbsd
    - netbsd
  goarch:
    - "386"
    - amd64
    - arm
    - arm64
  ignore:
    - goos: freebsd
      goarch: arm
      goarm: 6
    - goos: freebsd
      goarch: arm64
    - goos: openbsd
      goarch: arm
      goarm: 6
archives:
- name_template: >-
    {{ .ProjectName }}_
    {{- .Version }}_
    {{- title .Os }}_
    {{- if eq .Arch "amd64" }}x86_64
    {{- else if eq .Arch "386" }}i386
    {{- else }}{{ .Arch }}{{ end }}
    {{- if .Arm }}v{{ .Arm }}{{ end }}
  format: tar.gz
  format_overrides:
    - goos: windows
      format: zip
  files:
    - none*
  wrap_in_directory: false


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing Guide

* [Style Guide](#style-guide)
* [Build](#build)
* [Features Requested](#features-requested)

## Style Guide
### Code format
Lux uses [gofmt](https://golang.org/cmd/gofmt) to format the code, you must use [gofmt](https://golang.org/cmd/gofmt) to format your code before submitting.

### linter
We recommend using [golint](https://github.com/golang/lint) or [gometalinter](https://github.com/alecthomas/gometalinter) to check your code format.

## Build

Make sure that this folder is in `GOPATH`, then:

```bash
$ go build
```

## Features Requested
There are several [features](https://github.com/iawia002/lux/issues?q=is%3Aissue+is%3Aopen+label%3Afeature-request) requested by the community. If you have any idea, feel free to fork the repo, follow the style guide above, push and merge it after passing the test. Besides, you are welcomed to propose new features through the issue.


================================================
FILE: Cask.toml
================================================
[package]
name = "github.com/iawia002/lux"
bin = "lux"
authors = ["Xinzhao Xu <z2d@jifangcheng.com>"]
keywords = ["go", "golang", "crawler", "scraper", "downloader", "youtube", "video", "download", "tumblr", "bilibili", "qq", "hacktoberfest", "youku", "iqiyi"]
repository = "https://github.com/iawia002/lux"
description = """
👾 Fast and simple video download library and CLI tool written in Go
"""

[darwin]
x86_64 = { url = "https://github.com/iawia002/lux/releases/download/v{version}/lux_{version}_Darwin_x86_64.tar.gz" }
aarch64 = { url = "https://github.com/iawia002/lux/releases/download/v{version}/lux_{version}_Darwin_arm64.tar.gz" }

[windows]
x86_64 = { url = "https://github.com/iawia002/lux/releases/download/v{version}/lux_{version}_Windows_x86_64.zip" }

[linux]
x86_64 = { url = "https://github.com/iawia002/lux/releases/download/v{version}/lux_{version}_Linux_x86_64.tar.gz" }
aarch64 = { url = "https://github.com/iawia002/lux/releases/download/v{version}/lux_{version}_Linux_arm64.tar.gz" }


================================================
FILE: LICENSE
================================================
MIT License

Copyright 2018-present, iawia002

Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


================================================
FILE: README.md
================================================
<h1 align="center">Lux</h1>

<p align="center"><i>Let there be Lux!</i></p>

<div align="center">
  <a href="https://codecov.io/gh/iawia002/lux">
    <img src="https://img.shields.io/codecov/c/github/iawia002/lux.svg?style=flat-square" alt="Codecov">
  </a>
  <a href="https://github.com/iawia002/lux/actions">
    <img src="https://img.shields.io/github/actions/workflow/status/iawia002/lux/ci.yml?style=flat-square" alt="GitHub Workflow Status">
  </a>
  <a href="https://goreportcard.com/report/github.com/iawia002/lux">
    <img src="https://goreportcard.com/badge/github.com/iawia002/lux?style=flat-square" alt="Go Report Card">
  </a>
  <a href="https://github.com/iawia002/lux/releases">
    <img src="https://img.shields.io/github/release/iawia002/lux.svg?style=flat-square" alt="GitHub release">
  </a>
  <a href="https://formulae.brew.sh/formula/lux">
    <img src="https://img.shields.io/homebrew/v/lux.svg?style=flat-square" alt="Homebrew">
  </a>
</div>

👾 Lux is a fast and simple video downloader built with Go.

- [Installation](#installation)
  - [Prerequisites](#prerequisites)
  - [Install via `go install`](#install-via-go-install)
  - [Homebrew (macOS only)](#homebrew-macos-only)
  - [Arch Linux](#arch-linux)
  - [Void Linux](#void-linux)
  - [Scoop on Windows](#scoop-on-windows)
  - [Chocolatey on Windows](#chocolatey-on-windows)
  - [Cask on Windows/macOS/Linux](#cask-on-windowsmacoslinux)
- [Getting Started](#getting-started)
  - [Download a video](#download-a-video)
  - [Download anything else](#download-anything-else)
  - [Download playlist](#download-playlist)
  - [Multiple inputs](#multiple-inputs)
  - [Resume a download](#resume-a-download)
  - [Auto retry](#auto-retry)
  - [Cookies](#cookies)
  - [Proxy](#proxy)
  - [Multi-Thread](#multi-thread)
  - [Short link](#short-link)
    - [bilibili](#bilibili)
  - [Use specified Referrer](#use-specified-referrer)
  - [Specify the output path and name](#specify-the-output-path-and-name)
  - [Debug Mode](#debug-mode)
  - [Reuse extracted data](#reuse-extracted-data)
  - [Options](#options)
    - [Download:](#download)
    - [Network:](#network)
    - [Playlist:](#playlist)
    - [Filesystem:](#filesystem)
    - [Subtitle:](#subtitle)
    - [Youku:](#youku)
    - [aria2:](#aria2)
- [Supported Sites](#supported-sites)
- [Known issues](#known-issues)
  - [优酷](#优酷)
  - [西瓜/头条视频](#西瓜头条视频)
- [Contributing](#contributing)
- [Authors](#authors)
- [Similar projects](#similar-projects)
- [License](#license)

## Installation

### Prerequisites

The following dependencies are required and must be installed separately.

- **[FFmpeg](https://www.ffmpeg.org)**

> **Note**: FFmpeg does not affect the download, only affects the final file merge.

### Install via `go install`

To install Lux, use `go install`, or download the binary file from [Releases](https://github.com/iawia002/lux/releases) page.

```bash
$ go install github.com/iawia002/lux@latest
```

### Homebrew (macOS only)

For macOS users, you can install `lux` via:

```bash
$ brew install lux
```

### Arch Linux

For Arch Users [AUR](https://aur.archlinux.org/packages/lux-dl/) package is available.

### Void Linux

For Void linux users, you can install `lux` via:

```
$ xbps-install -S lux
```

### [Scoop](https://scoop.sh/) on Windows

```sh
$ scoop install lux
```

### [Chocolatey](https://chocolatey.org/) on Windows

```
$ choco install lux
```

### [Cask](https://github.com/axetroy/cask.rs) on Windows/macOS/Linux

```sh
$ cask install github.com/iawia002/lux
```

## Getting Started

Usage:

```
lux [OPTIONS] URL [URL...]
```

### Download a video

```console
$ lux "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

 Site:      YouTube youtube.com
 Title:     Rick Astley - Never Gonna Give You Up (Video)
 Type:      video
 Stream:
     [248]  -------------------
     Quality:         1080p video/webm; codecs="vp9"
     Size:            63.93 MiB (67038963 Bytes)
     # download with: lux -f 248 ...

 41.88 MiB / 63.93 MiB [=================>-------------]  65.51% 4.22 MiB/s 00m05s
```

The `-i` option displays all available quality of video without downloading.

```console
$ lux -i "https://www.youtube.com/watch?v=dQw4w9WgXcQ"

 Site:      YouTube youtube.com
 Title:     Rick Astley - Never Gonna Give You Up (Video)
 Type:      video
 Streams:   # All available quality
     [248]  -------------------
     Quality:         1080p video/webm; codecs="vp9"
     Size:            49.29 MiB (51687554 Bytes)
     # download with: lux -f 248 ...

     [137]  -------------------
     Quality:         1080p video/mp4; codecs="avc1.640028"
     Size:            43.45 MiB (45564306 Bytes)
     # download with: lux -f 137 ...

     [398]  -------------------
     Quality:         720p video/mp4; codecs="av01.0.05M.08"
     Size:            37.12 MiB (38926432 Bytes)
     # download with: lux -f 398 ...

     [136]  -------------------
     Quality:         720p video/mp4; codecs="avc1.4d401f"
     Size:            31.34 MiB (32867324 Bytes)
     # download with: lux -f 136 ...

     [247]  -------------------
     Quality:         720p video/webm; codecs="vp9"
     Size:            31.03 MiB (32536181 Bytes)
     # download with: lux -f 247 ...
```

Use `lux -f stream "URL"` to download a specific stream listed in the output of `-i` option.

### Download anything else

If Lux is provided the URL of a specific resource, then it will be downloaded directly:

```console
$ lux "https://img9.bcyimg.com/drawer/15294/post/1799t/1f5a87801a0711e898b12b640777720f.jpg"

lux doesn't support this URL right now, but it will try to download it directly

 Site:      Universal
 Title:     1f5a87801a0711e898b12b640777720f
 Type:      image/jpeg
 Stream:
     [default]  -------------------
     Size:            1.00 MiB (1051042 Bytes)
     # download with: lux -f default "URL"

 1.00 MiB / 1.00 MiB [===================================] 100.00% 1.21 MiB/s 0s
```

### Download playlist

The `-p` option downloads an entire playlist instead of a single video.

```console
$ lux -i -p "https://www.bilibili.com/bangumi/play/ep198061"

 Site:      哔哩哔哩 bilibili.com
 Title:     Doctor X 第四季:第一集
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            845.66 MiB (886738354 Bytes)
     # download with: lux -f default "URL"


 Site:      哔哩哔哩 bilibili.com
 Title:     Doctor X 第四季:第二集
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            930.71 MiB (975919195 Bytes)
     # download with: lux -f default "URL"

......
```

You can use the `-start`, `-end` or `-items` option to specify the download range of the list:

```
-start
    	Playlist video to start at (default 1)
-end
    	Playlist video to end at
-items
    	Playlist video items to download. Separated by commas like: 1,5,6,8-10
```

For bilibili playlists only:

```
-eto
  File name of each bilibili episode doesn't include the playlist title
```

### Multiple inputs

You can also download multiple URLs at once:

```console
$ lux -i "https://www.bilibili.com/video/av21877586" "https://www.bilibili.com/video/av21990740"

 Site:      哔哩哔哩 bilibili.com
 Title:     【莓机会了】甜到虐哭的13集单集MAD「我现在什么都不想干,更不想看14集」
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            51.88 MiB (54403767 Bytes)
     # download with: lux -f default "URL"


 Site:      哔哩哔哩 bilibili.com
 Title:     【莓救了】甜到虐哭!!!国家队单集MAD-当熟悉的bgm响起,眼泪从脸颊滑下
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            77.63 MiB (81404093 Bytes)
     # download with: lux -f default "URL"
```

These URLs will be downloaded one by one.

You can also use the `-F` option to read URLs from file:

```console
$ lux -F ~/Desktop/u.txt

 Site:      微博 weibo.com
 Title:     在Google,我们设计什么? via@阑夕
 Type:      video
 Stream:
     [default]  -------------------
     Size:            19.19 MiB (20118196 Bytes)
     # download with: lux -f default "URL"

 19.19 MiB / 19.19 MiB [=================================] 100.00% 9.69 MiB/s 1s

......
```

You can use the `-start`, `-end` or `-items` option to specify the download range of the list:

```
-start
    	File line to start at (default 1)
-end
    	File line to end at
-items
    	File lines to download. Separated by commas like: 1,5,6,8-10
```

### Resume a download

<kbd>Ctrl</kbd>+<kbd>C</kbd> interrupts a download.

A temporary `.download` file is kept in the output directory. If `lux` is ran with the same arguments, then the download progress will resume from the last session.

### Auto retry

lux will auto retry when the download failed, you can specify the retry times by `-retry` option (default is 100).

### Cookies

Cookies can be provided to `lux` with the `-c` option if they are required for accessing the video.

Cookies can be the following format or [Netscape Cookie](https://curl.haxx.se/rfc/cookie_spec.html) format:

```console
name=value; name2=value2; ...
```

Cookies can be a string or a text file, supply cookies in one of the two following ways.

As a string:

```console
$ lux -c "name=value; name2=value2" "https://www.bilibili.com/video/av20203945"
```

As a text file:

```console
$ lux -c cookies.txt "https://www.bilibili.com/video/av20203945"
```

### Proxy

You can set the HTTP/SOCKS5 proxy using environment variables:

```console
$ HTTP_PROXY="http://127.0.0.1:1087/" lux -i "https://www.youtube.com/watch?v=Gnbch2osEeo"
```

```console
$ HTTP_PROXY="socks5://127.0.0.1:1080/" lux -i "https://www.youtube.com/watch?v=Gnbch2osEeo"
```

### Multi-Thread

Use `--multi-thread` or `-m` multiple threads to download single video.

Use `--thread` or `-n` option to set the number of download threads(default is 10).

> Note: If the video has multi fragment, the number of actual download threads will increase.
>
> For example:
> * If `-n` is set to 10, and the video has 2 fragments, then 20 threads will actually be used.
> * If the video has 20 fragments, only 10 fragments are downloaded in the same time, the actual threads count is 100.

> **Special Tips:** Use too many threads in **mgtv** download will cause HTTP 403 error, we recommend setting the number of threads to **1**.

### Short link

#### bilibili

You can just use `av` or `ep` number to download bilibili's video:

```console
$ lux -i ep198381 av21877586

 Site:      哔哩哔哩 bilibili.com
 Title:     狐妖小红娘:第79话 南国公主的吃货本色
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            485.23 MiB (508798478 Bytes)
     # download with: lux -f default "URL"


 Site:      哔哩哔哩 bilibili.com
 Title:     【莓机会了】甜到虐哭的13集单集MAD「我现在什么都不想干,更不想看14集」
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            51.88 MiB (54403767 Bytes)
     # download with: lux -f default "URL"
```

### Use specified Referrer

A Referrer can be used for the request with the `-r` option:

```console
$ lux -r "https://www.bilibili.com/video/av20383055/" "http://cn-scnc1-dx.acgvideo.com/"
```

### Specify the output path and name

The `-o` option sets the path, and `-O` option sets the name of the downloaded file:

```console
$ lux -o ../ -O "hello" "https://example.com"
```

### Debug Mode

The `-d` option outputs network request messages:

```console
$ lux -i -d "http://www.bilibili.com/video/av20088587"

URL:         http://www.bilibili.com/video/av20088587
Method:      GET
Headers:     http.Header{
    "Referer":         {"http://www.bilibili.com/video/av20088587"},
    "Accept":          {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
    "Accept-Charset":  {"UTF-8,*;q=0.5"},
    "Accept-Encoding": {"gzip,deflate,sdch"},
    "Accept-Language": {"en-US,en;q=0.8"},
    "User-Agent":      {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36"},
}
Status Code: 200

URL:         https://interface.bilibili.com/v2/playurl?appkey=84956560bc028eb7&cid=32782944&otype=json&qn=116&quality=116&type=&sign=fb2e3f261fec398652f96d358517e535
Method:      GET
Headers:     http.Header{
    "Accept-Charset":  {"UTF-8,*;q=0.5"},
    "Accept-Encoding": {"gzip,deflate,sdch"},
    "Accept-Language": {"en-US,en;q=0.8"},
    "User-Agent":      {"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_3) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/65.0.3325.146 Safari/537.36"},
    "Referer":         {"https://interface.bilibili.com/v2/playurl?appkey=84956560bc028eb7&cid=32782944&otype=json&qn=116&quality=116&type=&sign=fb2e3f261fec398652f96d358517e535"},
    "Accept":          {"text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8"},
}
Status Code: 200

 Site:      哔哩哔哩 bilibili.com
 Title:     燃油动力的遥控奥迪R8跑赛道
 Type:      video
 Streams:   # All available quality
     [default]  -------------------
     Quality:         高清 1080P
     Size:            64.38 MiB (67504795 Bytes)
     # download with: lux -f default "URL"
```

### Reuse extracted data

The `-j` option will print the extracted data in JSON format.

```console
$ lux -j "https://www.bilibili.com/video/av20203945"

{
    "site": "哔哩哔哩 bilibili.com",
    "title": "【2018拜年祭单品】相遇day by day",
    "type": "video",
    "streams": {
        "15": {
            "urls": [
                {
                    "url": "...",
                    "size": 18355205,
                    "ext": "flv"
                }
            ],
            "quality": "流畅 360P",
            "size": 18355205
        },
        "32": {
            "urls": [
                {
                    "url": "...",
                    "size": 40058632,
                    "ext": "flv"
                }
            ],
            "quality": "清晰 480P",
            "size": 40058632
        },
        "64": {
            "urls": [
                {
                    "url": "...",
                    "size": 82691087,
                    "ext": "flv"
                }
            ],
            "quality": "高清 720P",
            "size": 82691087
        },
        "80": {
            "urls": [
                {
                    "url": "...",
                    "size": 121735559,
                    "ext": "flv"
                }
            ],
            "quality": "高清 1080P",
            "size": 121735559
        }
    }
}
```

### Options

```
  -i	Information only
  -F string
    	URLs file path
  -d	Debug mode
  -j	Print extracted data
  -s	Minimum outputs
  -v	Show version
```

#### Download:

```
  -f string
    	Select specific stream to download
  -p	Download playlist
  -n int
    	The number of download thread (only works for multiple-parts video) (default 10)
  -c string
    	Cookie
  -r string
    	Use specified Referrer
  -cs int
    	HTTP chunk size for downloading (in MB) (default 1)
```

#### Network:

```
  -retry int
    	How many times to retry when the download failed (default 10)
```

#### Playlist:

```
  -start int
    	Playlist video to start at (default 1)
  -end int
    	Playlist video to end at
  -items string
    	Playlist video items to download. Separated by commas like: 1,5,6,8-10
```

#### Filesystem:

```
  -o string
    	Specify the output path
  -O string
    	Specify the output file name
```

#### Subtitle:

```
  -C	Download subtitles
  -C -items en,zh
    	Download specific languages (YouTube only)
  -C -items en,zh -embed 
    	Embed subtitles into the video (YouTube only)
```

#### Youku:

```
  -ccode string
    	Youku ccode (default "0502")
  -ckey string
    	Youku ckey (default "7B19C0AB12633B22E7FE81271162026020570708D6CC189E4924503C49D243A0DE6CD84A766832C2C99898FC5ED31F3709BB3CDD82C96492E721BDD381735026")
  -password string
    	Youku password
```

#### aria2:

> Note: If you use aria2 to download, you need to merge the multi-part videos yourself.

```
  -aria2
    	Use Aria2 RPC to download
  -aria2addr string
    	Aria2 Address (default "localhost:6800")
  -aria2method string
    	Aria2 Method (default "http")
  -aria2token string
    	Aria2 RPC Token
```

## Supported Sites

| Site             | URL                                                                       | 🎬 Videos | 🌁 Images | 🔊 Audio | 📚 Playlist | 🍪 VIP adaptation | Build Status                                                                                                                                                                      |
| ---------------- | ------------------------------------------------------------------------- | -------- | -------- | ------- | ---------- | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 抖音             | <https://www.douyin.com>                                                  | ✓        | ✓        |         |            |                  | [![douyin](https://github.com/iawia002/lux/actions/workflows/stream_douyin.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_douyin.yml)                   |
| 哔哩哔哩         | <https://www.bilibili.com>                                                | ✓        |          |         | ✓          | ✓                | [![bilibili](https://github.com/iawia002/lux/actions/workflows/stream_bilibili.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_bilibili.yml)             |
| 半次元           | <https://bcy.net>                                                         |          | ✓        |         |            |                  | [![bcy](https://github.com/iawia002/lux/actions/workflows/stream_bcy.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_bcy.yml)                            |
| pixivision       | <https://www.pixivision.net>                                              |          | ✓        |         |            |                  | [![pixivision](https://github.com/iawia002/lux/actions/workflows/stream_pixivision.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_pixivision.yml)       |
| 优酷             | <https://www.youku.com>                                                   | ✓        |          |         |            | ✓                | [![youku](https://github.com/iawia002/lux/actions/workflows/stream_youku.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_youku.yml)                      |
| YouTube          | <https://www.youtube.com>                                                 | ✓        |          |         | ✓          |                  | [![youtube](https://github.com/iawia002/lux/actions/workflows/stream_youtube.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_youtube.yml)                |
| 西瓜视频(头条) | <https://m.toutiao.com>, <https://v.ixigua.com>, <https://www.ixigua.com> | ✓        |          |         |            |                  | [![ixigua](https://github.com/iawia002/lux/actions/workflows/stream_ixigua.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_ixigua.yml)                   |
| 爱奇艺           | <https://www.iqiyi.com>                                                   | ✓        |          |         |            |                  | [![iqiyi](https://github.com/iawia002/lux/actions/workflows/stream_iqiyi.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_iqiyi.yml)                      |
| 新片场           | <https://www.xinpianchang.com>                                            | ✓        |          |         |            |                  | [![xinpianchang](https://github.com/iawia002/lux/actions/workflows/stream_xinpianchang.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_xinpianchang.yml) |
| 芒果 TV          | <https://www.mgtv.com>                                                    | ✓        |          |         |            |                  | [![mgtv](https://github.com/iawia002/lux/actions/workflows/stream_mgtv.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_mgtv.yml)                         |
| 糖豆广场舞       | <https://www.tangdou.com>                                                 | ✓        |          |         |            |                  | [![tangdou](https://github.com/iawia002/lux/actions/workflows/stream_tangdou.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_tangdou.yml)                |
| Tumblr           | <https://www.tumblr.com>                                                  | ✓        | ✓        |         |            |                  | [![tumblr](https://github.com/iawia002/lux/actions/workflows/stream_tumblr.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_tumblr.yml)                   |
| Vimeo            | <https://vimeo.com>                                                       | ✓        |          |         |            |                  | [![vimeo](https://github.com/iawia002/lux/actions/workflows/stream_vimeo.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_vimeo.yml)                      |
| Facebook         | <https://facebook.com>                                                    | ✓        |          |         |            |                  | [![facebook](https://github.com/iawia002/lux/actions/workflows/stream_facebook.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_facebook.yml)             |
| 斗鱼视频         | <https://v.douyu.com>                                                     | ✓        |          |         |            |                  | [![douyu](https://github.com/iawia002/lux/actions/workflows/stream_douyu.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_douyu.yml)                      |
| 秒拍             | <https://www.miaopai.com>                                                 | ✓        |          |         |            |                  | [![miaopai](https://github.com/iawia002/lux/actions/workflows/stream_miaopai.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_miaopai.yml)                |
| 微博             | <https://weibo.com>                                                       | ✓        |          |         |            |                  | [![weibo](https://github.com/iawia002/lux/actions/workflows/stream_weibo.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_weibo.yml)                      |
| Instagram        | <https://www.instagram.com>                                               | ✓        | ✓        |         |            |                  | [![instagram](https://github.com/iawia002/lux/actions/workflows/stream_instagram.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_instagram.yml)          |
| Threads        | <https://www.threads.net>                                               | ✓        | ✓        |         |            |                  | [![threads](https://github.com/iawia002/lux/actions/workflows/stream_threads.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_threads.yml)          |
| Twitter          | <https://twitter.com>                                                     | ✓        |          |         |            |                  | [![twitter](https://github.com/iawia002/lux/actions/workflows/stream_twitter.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_twitter.yml)                |
| 腾讯视频         | <https://v.qq.com>                                                        | ✓        |          |         |            |                  | [![qq](https://github.com/iawia002/lux/actions/workflows/stream_qq.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_qq.yml)                               |
| 网易云音乐       | <https://music.163.com>                                                   | ✓        |          |         |            |                  | [![netease](https://github.com/iawia002/lux/actions/workflows/stream_netease.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_netease.yml)                |
| 音悦台           | <https://yinyuetai.com>                                                   | ✓        |          |         |            |                  | [![yinyuetai](https://github.com/iawia002/lux/actions/workflows/stream_yinyuetai.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_yinyuetai.yml)          |
| 极客时间         | <https://time.geekbang.org>                                               | ✓        |          |         |            |                  | [![geekbang](https://github.com/iawia002/lux/actions/workflows/stream_geekbang.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_geekbang.yml)             |
| Pornhub          | <https://pornhub.com>                                                     | ✓        |          |         |            |                  | [![pornhub](https://github.com/iawia002/lux/actions/workflows/stream_pornhub.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_pornhub.yml)                |
| XVIDEOS          | <https://xvideos.com>                                                     | ✓        |          |         |            |                  | [![xvideos](https://github.com/iawia002/lux/actions/workflows/stream_xvideos.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_xvideos.yml)                |
| 聯合新聞網       | <https://udn.com>                                                         | ✓        |          |         |            |                  | [![udn](https://github.com/iawia002/lux/actions/workflows/stream_udn.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_udn.yml)                            |
| TikTok           | <https://www.tiktok.com>                                                  | ✓        |          |         |            |                  | [![tiktok](https://github.com/iawia002/lux/actions/workflows/stream_tiktok.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_tiktok.yml)                   |
| Pinterest        | <https://www.pinterest.com>                                               | ✓        |          |         |            |                  | [![pinterest](https://github.com/iawia002/lux/actions/workflows/stream_pinterest.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_pinterest.yml)          |
| 好看视频         | <https://haokan.baidu.com>                                                | ✓        |          |         |            |                  | [![haokan](https://github.com/iawia002/lux/actions/workflows/stream_haokan.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_haokan.yml)                   |
| AcFun            | <https://www.acfun.cn>                                                    | ✓        |          |         | ✓          |                  | [![acfun](https://github.com/iawia002/lux/actions/workflows/stream_acfun.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_acfun.yml)                      |
| Eporner          | <https://eporner.com>                                                     | ✓        |          |         |            |                  | [![eporner](https://github.com/iawia002/lux/actions/workflows/stream_eporner.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_eporner.yml)                |
| StreamTape       | <https://streamtape.com>                                                  | ✓        |          |         |            |                  | [![streamtape](https://github.com/iawia002/lux/actions/workflows/stream_streamtape.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_streamtape.yml)       |
| 虎扑             | <https://hupu.com>                                                        | ✓        |          |         |            |                  | [![hupu](https://github.com/iawia002/lux/actions/workflows/stream_hupu.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_hupu.yml)                         |
| 虎牙视频         | <https://v.huya.com>                                                      | ✓        |          |         |            |                  | [![huya](https://github.com/iawia002/lux/actions/workflows/stream_huya.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_huya.yml)                         |
| 喜马拉雅         | <https://www.ximalaya.com>                                                |          |          | ✓       |            |                  | [![ximalaya](https://github.com/iawia002/lux/actions/workflows/stream_ximalaya.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_ximalaya.yml)             |
| 快手             | <https://www.kuaishou.com>                                                | ✓        |          |         |            |                  | [![kuaishou](https://github.com/iawia002/lux/actions/workflows/stream_kuaishou.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_kuaishou.yml)             |
| Reddit           | <https://www.reddit.com>                                                  | ✓        | ✓        |         |            |                  | [![reddit](https://github.com/iawia002/lux/actions/workflows/stream_reddit.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_reddit.yml)                   |
| VKontakte        | <https://vk.com>                                                          | ✓        |          |         |            |                  | [![vk](https://github.com/iawia002/lux/actions/workflows/stream_vk.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_vk.yml/)                              |
| 知乎             | <https://zhihu.com>                                                       | ✓        |          |         |            |                  | [![zhihu](https://github.com/iawia002/lux/actions/workflows/stream_zhihu.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_zhihu.yml/)                     |
| Rumble           | <https://rumble.com>                                                      | ✓        |          |         |            |                  | [![rumble](https://github.com/iawia002/lux/actions/workflows/stream_rumble.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_rumble.yml/)                  |
| 小红书           | <https://xiaohongshu.com>                                                 | ✓        |          |         |            |                  | [![xiaohongshu](https://github.com/iawia002/lux/actions/workflows/stream_xiaohongshu.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_xiaohongshu.yml/)   |
| Zing MP3         | <https://zingmp3.vn>                                                      | ✓        |          | ✓       |            |                  | [![zingmp3](https://github.com/iawia002/lux/actions/workflows/stream_zingmp3.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_zingmp3.yml/)               |
| Bitchute         | <https://www.bitchute.com>                                                | ✓        |          |         |            |                  | [![bitchute](https://github.com/iawia002/lux/actions/workflows/stream_bitchute.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_bitchute.yml/)            |
| Odysee         | <https://odysee.com>                                                | ✓        |          | ✓       |            |                  | [![odysee](https://github.com/iawia002/lux/actions/workflows/stream_odysee.yml/badge.svg)](https://github.com/iawia002/lux/actions/workflows/stream_odysee.yml/)            |


## Known issues

### 优酷

优酷的 `ccode` 经常变化导致 lux 不可用,如果你知道有新的可用的 `ccode`,可以直接使用 `lux -ccode ...` 而不用等待 lux 更新(当然,也欢迎你给我们提一个 Pull request 来更新默认的 `ccode`)

最好是每次下载都附带登录过的 Cookie 以避免部分 `ccode` 的问题

### 西瓜/头条视频
西瓜/头条视频必须带 Cookie 才能下载成功,西瓜和头条可共用西瓜视频的 Cookie,Cookie 的有效期可能较短,下载失败就更新 Cookie 尝试:

```
$ lux -c "msToken=yoEh0-qLUq4obZ8Sfxsem_CxCo9R3NM6ViTrWaRcM1...; ttwid=1%7C..." "https://m.toutiao.com/is/iYbTfJ79/"
```

## Contributing

Lux is an open source project and built on the top of open-source projects. Check out the [Contributing Guide](./CONTRIBUTING.md) to get started.

## Authors

Code with ❤️ by [iawia002](https://github.com/iawia002) and lovely [contributors](https://github.com/iawia002/lux/graphs/contributors)

## Similar projects

- [youtube](https://github.com/kkdai/youtube)
- [youtube-dl](https://github.com/rg3/youtube-dl)
- [you-get](https://github.com/soimort/you-get)
- [ytdl](https://github.com/rylio/ytdl)

## License

MIT

Copyright (c) 2018-present, iawia002


================================================
FILE: app/app.go
================================================
package app

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"sort"
	"strings"

	"github.com/fatih/color"
	"github.com/urfave/cli/v2"

	"github.com/iawia002/lux/downloader"
	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/request"
	"github.com/iawia002/lux/utils"
)

// Name is the name of this app.
const Name = "lux"

// This value will be injected into the corresponding git tag value at build time using `-ldflags`.
var version = "v0.0.0"

func init() {
	cli.VersionPrinter = func(c *cli.Context) {
		blue := color.New(color.FgBlue)
		cyan := color.New(color.FgCyan)
		fmt.Fprintf(
			color.Output,
			"\n%s: version %s, A fast and simple video downloader.\n\n",
			cyan.Sprintf(Name),
			blue.Sprintf(c.App.Version),
		)
	}
}

// New returns the App instance.
func New() *cli.App {
	app := &cli.App{
		Name:    Name,
		Usage:   "A fast and simple video downloader.",
		Version: version,
		Flags: []cli.Flag{
			&cli.BoolFlag{
				Name:    "debug",
				Aliases: []string{"d"},
				Usage:   "Debug mode",
			},
			&cli.BoolFlag{
				Name:    "silent",
				Aliases: []string{"s"},
				Usage:   "Minimum outputs",
			},
			&cli.BoolFlag{
				Name:    "info",
				Aliases: []string{"i"},
				Usage:   "Information only",
			},
			&cli.BoolFlag{
				Name:    "json",
				Aliases: []string{"j"},
				Usage:   "Print extracted JSON data",
			},

			&cli.StringFlag{
				Name:    "cookie",
				Aliases: []string{"c"},
				Usage:   "Cookie",
			},
			&cli.BoolFlag{
				Name:    "playlist",
				Aliases: []string{"p"},
				Usage:   "Download playlist",
			},
			&cli.StringFlag{
				Name:    "user-agent",
				Aliases: []string{"u"},
				Usage:   "Use specified User-Agent",
			},
			&cli.StringFlag{
				Name:    "refer",
				Aliases: []string{"r"},
				Usage:   "Use specified Referrer",
			},
			&cli.StringFlag{
				Name:    "stream-format",
				Aliases: []string{"f"},
				Usage:   "Select specific stream to download",
			},
			&cli.BoolFlag{
				Name:    "audio-only",
				Aliases: []string{"ao"},
				Usage:   "Download audio only at best quality",
			},
			&cli.StringFlag{
				Name:    "file",
				Aliases: []string{"F"},
				Usage:   "URLs file path",
			},
			&cli.StringFlag{
				Name:    "output-path",
				Aliases: []string{"o"},
				Usage:   "Specify the output path",
			},
			&cli.StringFlag{
				Name:    "output-name",
				Aliases: []string{"O"},
				Usage:   "Specify the output file name",
			},
			&cli.UintFlag{
				Name:  "file-name-length",
				Value: 255,
				Usage: "The maximum length of a file name, 0 means unlimited",
			},
			&cli.BoolFlag{
				Name:    "caption",
				Aliases: []string{"C"},
				Usage:   "Download captions",
			},
			&cli.BoolFlag{
				Name:    "embed-subtitle",
				Aliases: []string{"embed"},
				Usage:   "Embed subtitles into the video (requires ffmpeg)",
			},

			&cli.UintFlag{
				Name:  "start",
				Value: 1,
				Usage: "Define the starting item of a playlist or a file input",
			},
			&cli.UintFlag{
				Name:  "end",
				Value: 0,
				Usage: "Define the ending item of a playlist or a file input",
			},
			&cli.StringFlag{
				Name:  "items",
				Usage: "Define wanted items from a file or playlist. Separated by commas like: 1,5,6,8-10",
			},

			&cli.BoolFlag{
				Name:    "multi-thread",
				Aliases: []string{"m"},
				Usage:   "Multiple threads to download single video",
			},
			&cli.UintFlag{
				Name:  "retry",
				Value: 10,
				Usage: "How many times to retry when the download failed",
			},
			&cli.UintFlag{
				Name:    "chunk-size",
				Aliases: []string{"cs"},
				Value:   1,
				Usage:   "HTTP chunk size for downloading (in MB)",
			},
			&cli.UintFlag{
				Name:    "thread",
				Aliases: []string{"n"},
				Value:   10,
				Usage:   "The number of download thread (only works for multiple-parts video)",
			},

			// Aria2
			&cli.BoolFlag{
				Name:  "aria2",
				Usage: "Use Aria2 RPC to download",
			},
			&cli.StringFlag{
				Name:  "aria2-token",
				Usage: "Aria2 RPC Token",
			},
			&cli.StringFlag{
				Name:  "aria2-addr",
				Value: "localhost:6800",
				Usage: "Aria2 Address",
			},
			&cli.StringFlag{
				Name:  "aria2-method",
				Value: "http",
				Usage: "Aria2 Method",
			},

			// youku
			&cli.StringFlag{
				Name:    "youku-ccode",
				Aliases: []string{"ccode"},
				Value:   "0502",
				Usage:   "Youku ccode",
			},
			&cli.StringFlag{
				Name:    "youku-ckey",
				Aliases: []string{"ckey"},
				Value:   "7B19C0AB12633B22E7FE81271162026020570708D6CC189E4924503C49D243A0DE6CD84A766832C2C99898FC5ED31F3709BB3CDD82C96492E721BDD381735026",
				Usage:   "Youku ckey",
			},
			&cli.StringFlag{
				Name:    "youku-password",
				Aliases: []string{"password"},
				Usage:   "Youku password",
			},

			&cli.BoolFlag{
				Name:    "episode-title-only",
				Aliases: []string{"eto"},
				Usage:   "File name of each bilibili episode doesn't include the playlist title",
			},
		},
		Action: func(c *cli.Context) error {
			args := c.Args().Slice()

			if c.Bool("debug") {
				cli.VersionPrinter(c)
			}

			if file := c.String("file"); file != "" {
				f, err := os.Open(file)
				if err != nil {
					return err
				}
				defer f.Close() // nolint

				fileItems := utils.ParseInputFile(f, c.String("items"), int(c.Uint("start")), int(c.Uint("end")))
				args = append(args, fileItems...)
			}

			if len(args) < 1 {
				return errors.New("too few arguments")
			}

			cookie := c.String("cookie")
			if cookie != "" {
				// If cookie is a file path, convert it to a string to ensure cookie is always string
				if _, fileErr := os.Stat(cookie); fileErr == nil {
					// Cookie is a file
					data, err := os.ReadFile(cookie)
					if err != nil {
						return err
					}
					cookie = strings.TrimSpace(string(data))
				}
			}

			request.SetOptions(request.Options{
				RetryTimes: int(c.Uint("retry")),
				Cookie:     cookie,
				UserAgent:  c.String("user-agent"),
				Refer:      c.String("refer"),
				Debug:      c.Bool("debug"),
				Silent:     c.Bool("silent"),
			})

			var isErr bool
			for _, videoURL := range args {
				if err := download(c, videoURL); err != nil {
					fmt.Fprintf(
						color.Output,
						"Downloading %s error:\n",
						color.CyanString("%s", videoURL),
					)
					fmt.Printf("%+v\n", err)
					isErr = true
				}
			}
			if isErr {
				return cli.Exit("", 1)
			}
			return nil
		},
		EnableBashCompletion: true,
	}

	sort.Sort(cli.FlagsByName(app.Flags))
	return app
}

func download(c *cli.Context, videoURL string) error {
	data, err := extractors.Extract(videoURL, extractors.Options{
		Playlist:         c.Bool("playlist"),
		Items:            c.String("items"),
		ItemStart:        int(c.Uint("start")),
		ItemEnd:          int(c.Uint("end")),
		ThreadNumber:     int(c.Uint("thread")),
		EpisodeTitleOnly: c.Bool("episode-title-only"),
		Cookie:           c.String("cookie"),
		YoukuCcode:       c.String("youku-ccode"),
		YoukuCkey:        c.String("youku-ckey"),
		YoukuPassword:    c.String("youku-password"),
	})
	if err != nil {
		// if this error occurs, it means that an error occurred before actually starting to extract data
		// (there is an error in the preparation step), and the data list is empty.
		return err
	}

	if c.Bool("json") {
		e := json.NewEncoder(os.Stdout)
		e.SetIndent("", "\t")
		e.SetEscapeHTML(false)
		if err := e.Encode(data); err != nil {
			return err
		}

		return nil
	}

	defaultDownloader := downloader.New(downloader.Options{
		Silent:         c.Bool("silent"),
		InfoOnly:       c.Bool("info"),
		Stream:         c.String("stream-format"),
		AudioOnly:      c.Bool("audio-only"),
		Refer:          c.String("refer"),
		OutputPath:     c.String("output-path"),
		OutputName:     c.String("output-name"),
		FileNameLength: int(c.Uint("file-name-length")),
		Caption:        c.Bool("caption"),
		EmbedSubtitle:  c.Bool("embed-subtitle"),
		MultiThread:    c.Bool("multi-thread"),
		ThreadNumber:   int(c.Uint("thread")),
		RetryTimes:     int(c.Uint("retry")),
		ChunkSizeMB:    int(c.Uint("chunk-size")),
		UseAria2RPC:    c.Bool("aria2"),
		Aria2Token:     c.String("aria2-token"),
		Aria2Method:    c.String("aria2-method"),
		Aria2Addr:      c.String("aria2-addr"),
	})
	errors := make([]error, 0)
	for _, item := range data {
		if item.Err != nil {
			// if this error occurs, the preparation step is normal, but the data extraction is wrong.
			// the data is an empty struct.
			errors = append(errors, item.Err)
			continue
		}
		if err = defaultDownloader.Download(item); err != nil {
			errors = append(errors, err)
		}
	}
	if len(errors) != 0 {
		return errors[0]
	}
	return nil
}


================================================
FILE: app/register.go
================================================
package app

import (
	_ "github.com/iawia002/lux/extractors/acfun"
	_ "github.com/iawia002/lux/extractors/bcy"
	_ "github.com/iawia002/lux/extractors/bilibili"
	_ "github.com/iawia002/lux/extractors/bitchute"
	_ "github.com/iawia002/lux/extractors/douyin"
	_ "github.com/iawia002/lux/extractors/douyu"
	_ "github.com/iawia002/lux/extractors/eporner"
	_ "github.com/iawia002/lux/extractors/facebook"
	_ "github.com/iawia002/lux/extractors/geekbang"
	_ "github.com/iawia002/lux/extractors/haokan"
	_ "github.com/iawia002/lux/extractors/hupu"
	_ "github.com/iawia002/lux/extractors/huya"
	_ "github.com/iawia002/lux/extractors/instagram"
	_ "github.com/iawia002/lux/extractors/iqiyi"
	_ "github.com/iawia002/lux/extractors/ixigua"
	_ "github.com/iawia002/lux/extractors/kuaishou"
	_ "github.com/iawia002/lux/extractors/mgtv"
	_ "github.com/iawia002/lux/extractors/miaopai"
	_ "github.com/iawia002/lux/extractors/netease"
	_ "github.com/iawia002/lux/extractors/odysee"
	_ "github.com/iawia002/lux/extractors/pinterest"
	_ "github.com/iawia002/lux/extractors/pixivision"
	_ "github.com/iawia002/lux/extractors/pornhub"
	_ "github.com/iawia002/lux/extractors/qq"
	_ "github.com/iawia002/lux/extractors/reddit"
	_ "github.com/iawia002/lux/extractors/rumble"
	_ "github.com/iawia002/lux/extractors/streamtape"
	_ "github.com/iawia002/lux/extractors/tangdou"
	_ "github.com/iawia002/lux/extractors/threads"
	_ "github.com/iawia002/lux/extractors/tiktok"
	_ "github.com/iawia002/lux/extractors/tumblr"
	_ "github.com/iawia002/lux/extractors/twitter"
	_ "github.com/iawia002/lux/extractors/udn"
	_ "github.com/iawia002/lux/extractors/universal"
	_ "github.com/iawia002/lux/extractors/vimeo"
	_ "github.com/iawia002/lux/extractors/vk"
	_ "github.com/iawia002/lux/extractors/weibo"
	_ "github.com/iawia002/lux/extractors/xiaohongshu"
	_ "github.com/iawia002/lux/extractors/ximalaya"
	_ "github.com/iawia002/lux/extractors/xinpianchang"
	_ "github.com/iawia002/lux/extractors/xvideos"
	_ "github.com/iawia002/lux/extractors/yinyuetai"
	_ "github.com/iawia002/lux/extractors/youku"
	_ "github.com/iawia002/lux/extractors/youtube"
	_ "github.com/iawia002/lux/extractors/zhihu"
	_ "github.com/iawia002/lux/extractors/zingmp3"
)


================================================
FILE: codecov.yml
================================================
codecov:
  token: e0f2d44f-c6a7-469a-a688-37c72c0f18f9


================================================
FILE: config/config.go
================================================
package config

// FakeHeaders fake http headers
var FakeHeaders = map[string]string{
	"Accept":          "text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8",
	"Accept-Charset":  "UTF-8,*;q=0.5",
	"Accept-Encoding": "gzip,deflate,sdch",
	"Accept-Language": "en-US,en;q=0.8",
	"User-Agent":      "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_13_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/69.0.3497.81 Safari/537.36",
}


================================================
FILE: downloader/downloader.go
================================================
package downloader

import (
	"bytes"
	"encoding/binary"
	"encoding/json"
	"fmt"
	"io"
	"net/http"
	"os"
	"path"
	"path/filepath"
	"regexp"
	"sort"
	"strings"
	"sync"
	"time"

	"github.com/cheggaaa/pb/v3"
	"github.com/pkg/errors"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/request"
	"github.com/iawia002/lux/utils"
)

// Options defines options used in downloading.
type Options struct {
	InfoOnly       bool
	Silent         bool
	Stream         string
	AudioOnly      bool
	Refer          string
	OutputPath     string
	OutputName     string
	FileNameLength int
	Caption        bool
	EmbedSubtitle  bool

	MultiThread  bool
	ThreadNumber int
	RetryTimes   int
	ChunkSizeMB  int
	// Aria2
	UseAria2RPC bool
	Aria2Token  string
	Aria2Method string
	Aria2Addr   string
}

// Downloader is the default downloader.
type Downloader struct {
	Bar    *pb.ProgressBar
	option Options
}

const (
	DOWNLOAD_FILE_EXT = ".download"
)

func progressBar(size int64) *pb.ProgressBar {
	tmpl := `{{counters .}} {{bar . "[" "=" ">" "-" "]"}} {{speed .}} {{percent . | green}} {{rtime .}}`
	return pb.New64(size).
		Set(pb.Bytes, true).
		SetMaxWidth(1000).
		SetTemplate(pb.ProgressBarTemplate(tmpl))
}

// New returns a new Downloader implementation.
func New(option Options) *Downloader {
	downloader := &Downloader{
		option: option,
	}
	return downloader
}

// caption downloads danmaku, subtitles, etc
func (downloader *Downloader) caption(url, fileName, ext string, transform func([]byte) ([]byte, error)) error {
	refer := downloader.option.Refer
	if refer == "" {
		refer = url
	}
	body, err := request.GetByte(url, refer, nil)
	if err != nil {
		return err
	}

	if transform != nil {
		body, err = transform(body)
		if err != nil {
			return err
		}
	}

	filePath, err := utils.FilePath(fileName, ext, downloader.option.FileNameLength, downloader.option.OutputPath, true)
	if err != nil {
		return err
	}
	file, fileError := os.Create(filePath)
	if fileError != nil {
		return fileError
	}
	defer file.Close() // nolint

	if _, err = file.Write(body); err != nil {
		return err
	}
	return nil
}

func (downloader *Downloader) writeFile(url string, file *os.File, headers map[string]string) (int64, error) {
	res, err := request.Request(http.MethodGet, url, nil, headers)
	if err != nil {
		return 0, err
	}
	defer res.Body.Close() // nolint

	barWriter := downloader.Bar.NewProxyWriter(file)
	// Note that io.Copy reads 32kb(maximum) from input and writes them to output, then repeats.
	// So don't worry about memory.
	written, copyErr := io.Copy(barWriter, res.Body)
	if copyErr != nil && copyErr != io.EOF {
		return written, errors.Errorf("file copy error: %s", copyErr)
	}
	return written, nil
}

func (downloader *Downloader) save(part *extractors.Part, refer, fileName string) error {
	filePath, err := utils.FilePath(fileName, part.Ext, downloader.option.FileNameLength, downloader.option.OutputPath, false)
	if err != nil {
		return err
	}
	fileSize, exists, err := utils.FileSize(filePath)
	if err != nil {
		return err
	}
	// Skip segment file
	// TODO: Live video URLs will not return the size
	if exists && fileSize == part.Size {
		downloader.Bar.Add64(fileSize)
		return nil
	}

	tempFilePath := filePath + DOWNLOAD_FILE_EXT
	tempFileSize, _, err := utils.FileSize(tempFilePath)
	if err != nil {
		return err
	}
	headers := map[string]string{
		"Referer": refer,
	}
	var (
		file      *os.File
		fileError error
	)
	if tempFileSize > 0 {
		// range start from 0, 0-1023 means the first 1024 bytes of the file
		headers["Range"] = fmt.Sprintf("bytes=%d-", tempFileSize)
		file, fileError = os.OpenFile(tempFilePath, os.O_APPEND|os.O_WRONLY, 0644)
		downloader.Bar.Add64(tempFileSize)
	} else {
		file, fileError = os.Create(tempFilePath)
	}
	if fileError != nil {
		return fileError
	}

	// close and rename temp file at the end of this function
	defer func() {
		// must close the file before rename or it will cause
		// `The process cannot access the file because it is being used by another process.` error.
		file.Close() // nolint
		if err == nil {
			os.Rename(tempFilePath, filePath) // nolint
		}
	}()

	if downloader.option.ChunkSizeMB > 0 {
		var start, end, chunkSize int64
		chunkSize = int64(downloader.option.ChunkSizeMB) * 1024 * 1024
		remainingSize := part.Size
		if tempFileSize > 0 {
			start = tempFileSize
			remainingSize -= tempFileSize
		}
		chunk := remainingSize / chunkSize
		if remainingSize%chunkSize != 0 {
			chunk++
		}
		var i int64 = 1
		for ; i <= chunk; i++ {
			end = start + chunkSize - 1
			headers["Range"] = fmt.Sprintf("bytes=%d-%d", start, end)
			temp := start
			for i := 0; ; i++ {
				written, err := downloader.writeFile(part.URL, file, headers)
				if err == nil {
					break
				} else if i+1 >= downloader.option.RetryTimes {
					return err
				}
				temp += written
				headers["Range"] = fmt.Sprintf("bytes=%d-%d", temp, end)
				time.Sleep(1 * time.Second)
			}
			start = end + 1
		}
	} else {
		temp := tempFileSize
		for i := 0; ; i++ {
			written, err := downloader.writeFile(part.URL, file, headers)
			if err == nil {
				break
			} else if i+1 >= downloader.option.RetryTimes {
				return err
			}
			temp += written
			headers["Range"] = fmt.Sprintf("bytes=%d-", temp)
			time.Sleep(1 * time.Second)
		}
	}

	return nil
}

func (downloader *Downloader) multiThreadSave(dataPart *extractors.Part, refer, fileName string) error {
	filePath, err := utils.FilePath(fileName, dataPart.Ext, downloader.option.FileNameLength, downloader.option.OutputPath, false)
	if err != nil {
		return err
	}
	fileSize, exists, err := utils.FileSize(filePath)
	if err != nil {
		return err
	}

	// Skip segment file
	// TODO: Live video URLs will not return the size
	if exists && fileSize == dataPart.Size {
		downloader.Bar.Add64(fileSize)
		return nil
	}
	tmpFilePath := filePath + DOWNLOAD_FILE_EXT
	tmpFileSize, tmpExists, err := utils.FileSize(tmpFilePath)
	if err != nil {
		return err
	}
	if tmpExists {
		if tmpFileSize == dataPart.Size {
			downloader.Bar.Add64(dataPart.Size)
			return os.Rename(tmpFilePath, filePath)
		}

		if err = os.Remove(tmpFilePath); err != nil {
			return err
		}
	}

	// Scan all parts
	parts, err := readDirAllFilePart(filePath, fileName, dataPart.Ext)
	if err != nil {
		return err
	}

	var unfinishedPart []*FilePartMeta
	savedSize := int64(0)
	if len(parts) > 0 {
		lastEnd := int64(-1)
		for i, part := range parts {
			// If some parts are lost, re-insert one part.
			if part.Start-lastEnd != 1 {
				newPart := &FilePartMeta{
					Index: part.Index - 0.000001,
					Start: lastEnd + 1,
					End:   part.Start - 1,
					Cur:   lastEnd + 1,
				}
				tmp := append([]*FilePartMeta{}, parts[:i]...)
				tmp = append(tmp, newPart)
				parts = append(tmp, parts[i:]...)
				unfinishedPart = append(unfinishedPart, newPart)
			}
			// When the part has been downloaded in whole, part.Cur is equal to part.End + 1
			if part.Cur <= part.End+1 {
				savedSize += part.Cur - part.Start
				if part.Cur < part.End+1 {
					unfinishedPart = append(unfinishedPart, part)
				}
			} else {
				// The size of this part has been saved greater than the part size, delete it transparently and re-download.
				err = os.Remove(filePartPath(filePath, part))
				if err != nil {
					return err
				}
				part.Cur = part.Start
				unfinishedPart = append(unfinishedPart, part)
			}
			lastEnd = part.End
		}
		if lastEnd != dataPart.Size-1 {
			newPart := &FilePartMeta{
				Index: parts[len(parts)-1].Index + 1,
				Start: lastEnd + 1,
				End:   dataPart.Size - 1,
				Cur:   lastEnd + 1,
			}
			parts = append(parts, newPart)
			unfinishedPart = append(unfinishedPart, newPart)
		}
	} else {
		var start, end, partSize int64
		var i float32
		partSize = dataPart.Size / int64(downloader.option.ThreadNumber)
		i = 0
		for start < dataPart.Size {
			end = start + partSize - 1
			if end > dataPart.Size {
				end = dataPart.Size - 1
			} else if int(i+1) == downloader.option.ThreadNumber && end < dataPart.Size {
				end = dataPart.Size - 1
			}
			part := &FilePartMeta{
				Index: i,
				Start: start,
				End:   end,
				Cur:   start,
			}
			parts = append(parts, part)
			unfinishedPart = append(unfinishedPart, part)
			start = end + 1
			i++
		}
	}
	if savedSize > 0 {
		downloader.Bar.Add64(savedSize)
		if savedSize == dataPart.Size {
			return mergeMultiPart(filePath, parts)
		}
	}

	wgp := utils.NewWaitGroupPool(downloader.option.ThreadNumber)
	var errs []error
	var mu sync.Mutex
	for _, part := range unfinishedPart {
		wgp.Add()
		go func(part *FilePartMeta) {
			file, err := os.OpenFile(filePartPath(filePath, part), os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
			if err != nil {
				mu.Lock()
				errs = append(errs, err)
				mu.Unlock()
				return
			}
			defer func() {
				file.Close() // nolint
				wgp.Done()
			}()

			var end, chunkSize int64
			headers := map[string]string{
				"Referer": refer,
			}
			if downloader.option.ChunkSizeMB <= 0 {
				chunkSize = part.End - part.Start + 1
			} else {
				chunkSize = int64(downloader.option.ChunkSizeMB) * 1024 * 1024
			}
			remainingSize := part.End - part.Cur + 1
			if part.Cur == part.Start {
				// Only write part to new file.
				err = writeFilePartMeta(file, part)
				if err != nil {
					mu.Lock()
					errs = append(errs, err)
					mu.Unlock()
					return
				}
			}
			for remainingSize > 0 {
				end = computeEnd(part.Cur, chunkSize, part.End)
				headers["Range"] = fmt.Sprintf("bytes=%d-%d", part.Cur, end)
				temp := part.Cur
				for i := 0; ; i++ {
					written, err := downloader.writeFile(dataPart.URL, file, headers)
					if err == nil {
						remainingSize -= chunkSize
						break
					} else if i+1 >= downloader.option.RetryTimes {
						mu.Lock()
						errs = append(errs, err)
						mu.Unlock()
						return
					}
					temp += written
					headers["Range"] = fmt.Sprintf("bytes=%d-%d", temp, end)
				}
				part.Cur = end + 1
			}
		}(part)
	}
	wgp.Wait()
	if len(errs) > 0 {
		return errs[0]
	}
	return mergeMultiPart(filePath, parts)
}

func filePartPath(filepath string, part *FilePartMeta) string {
	return fmt.Sprintf("%s.part%f", filepath, part.Index)
}

func computeEnd(s, chunkSize, max int64) int64 {
	var end int64
	end = s + chunkSize - 1
	if end > max {
		end = max
	}
	return end
}

func readDirAllFilePart(filePath, filename, extname string) ([]*FilePartMeta, error) {
	dirPath := filepath.Dir(filePath)
	dir, err := os.Open(dirPath)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer dir.Close() // nolint
	fns, err := dir.Readdir(0)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	var metas []*FilePartMeta
	reg := regexp.MustCompile(fmt.Sprintf("%s.%s.part.+", regexp.QuoteMeta(filename), extname))
	for _, fn := range fns {
		if reg.MatchString(fn.Name()) {
			meta, err := parseFilePartMeta(path.Join(dirPath, fn.Name()), fn.Size())
			if err != nil {
				return nil, errors.WithStack(err)
			}
			metas = append(metas, meta)
		}
	}
	sort.SliceStable(metas, func(i, j int) bool {
		return metas[i].Index < metas[j].Index
	})
	return metas, nil
}

func parseFilePartMeta(filepath string, fileSize int64) (*FilePartMeta, error) {
	meta := new(FilePartMeta)
	size := binary.Size(*meta)
	file, err := os.OpenFile(filepath, os.O_RDWR, 0666)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer file.Close() // nolint
	var buf [512]byte
	readSize, err := file.ReadAt(buf[0:size], 0)
	if err != nil && err != io.EOF {
		return nil, errors.WithStack(err)
	}
	if readSize < size {
		return nil, errors.Errorf("the file has been broken, please delete all part files and re-download")
	}
	err = binary.Read(bytes.NewBuffer(buf[:size]), binary.LittleEndian, meta)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	savedSize := fileSize - int64(binary.Size(meta))
	meta.Cur = meta.Start + savedSize
	return meta, nil
}

func writeFilePartMeta(file *os.File, meta *FilePartMeta) error {
	return binary.Write(file, binary.LittleEndian, meta)
}

func mergeMultiPart(filepath string, parts []*FilePartMeta) error {
	tempFilePath := filepath + DOWNLOAD_FILE_EXT
	tempFile, err := os.OpenFile(tempFilePath, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0666)
	if err != nil {
		return err
	}
	var partFiles []*os.File
	defer func() {
		for _, f := range partFiles {
			f.Close()           // nolint
			os.Remove(f.Name()) // nolint
		}
	}()
	for _, part := range parts {
		file, err := os.Open(filePartPath(filepath, part))
		if err != nil {
			return err
		}
		partFiles = append(partFiles, file)
		_, err = file.Seek(int64(binary.Size(part)), 0)
		if err != nil {
			return err
		}
		_, err = io.Copy(tempFile, file)
		if err != nil {
			return err
		}
	}
	tempFile.Close() // nolint
	err = os.Rename(tempFilePath, filepath)
	return err
}

func (downloader *Downloader) aria2(title string, stream *extractors.Stream) error {
	rpcData := Aria2RPCData{
		JSONRPC: "2.0",
		ID:      "lux", // can be modified
		Method:  "aria2.addUri",
	}
	rpcData.Params[0] = "token:" + downloader.option.Aria2Token
	var urls []string
	for _, p := range stream.Parts {
		urls = append(urls, p.URL)
	}
	var inputs Aria2Input
	inputs.Header = append(inputs.Header, "Referer: "+downloader.option.Refer)
	for i := range urls {
		rpcData.Params[1] = urls[i : i+1]
		inputs.Out = fmt.Sprintf("%s[%d].%s", title, i, stream.Parts[0].Ext)
		rpcData.Params[2] = &inputs
		jsonData, err := json.Marshal(rpcData)
		if err != nil {
			return err
		}
		reqURL := fmt.Sprintf("%s://%s/jsonrpc", downloader.option.Aria2Method, downloader.option.Aria2Addr)
		req, err := http.NewRequest(http.MethodPost, reqURL, bytes.NewBuffer(jsonData))
		if err != nil {
			return err
		}
		req.Header.Set("Content-Type", "application/json")

		var client = http.Client{Timeout: 30 * time.Second}
		res, err := client.Do(req)
		if err != nil {
			return err
		}
		// The http Client and Transport guarantee that Body is always
		// non-nil, even on responses without a body or responses with
		// a zero-length body.
		res.Body.Close() // nolint
	}
	return nil
}

// Download download urls
func (downloader *Downloader) Download(data *extractors.Data) error {
	if len(data.Streams) == 0 {
		return errors.Errorf("no streams in title %s", data.Title)
	}

	sortedStreams := genSortedStreams(data.Streams)
	if downloader.option.InfoOnly {
		printInfo(data, sortedStreams)
		return nil
	}

	title := downloader.option.OutputName
	if title == "" {
		title = data.Title
	}
	title = utils.FileName(title, "", downloader.option.FileNameLength)

	streamName := downloader.option.Stream
	if streamName == "" {
		streamName = sortedStreams[0].ID
	}
	stream, ok := data.Streams[streamName]
	if !ok {
		return errors.Errorf("no stream named %s", streamName)
	}

	if downloader.option.AudioOnly {
		var isFound bool
		reg, err := regexp.Compile("audio+")
		if err != nil {
			return err
		}

		for _, s := range sortedStreams {
			// Looking for the best quality
			if reg.MatchString(s.Quality) {
				isFound = true
				stream = data.Streams[s.ID]
				break
			}
			for _, part := range s.Parts {
				if part.Ext == "m4a" {
					isFound = true
					stream = data.Streams[s.ID]
					break
				}
			}
		}
		if !isFound {
			return errors.Errorf("No audio stream found")
		}
	}

	if !downloader.option.Silent {
		printStreamInfo(data, stream)
	}

	// download caption
	var subtitlePaths []string
	var subtitleLangs []string
	var subtitleFilesToDelete []string
	if downloader.option.Caption && data.Captions != nil {
		fmt.Println("\nDownloading captions...")
		for k, v := range data.Captions {
			if v != nil {
				fmt.Printf("Downloading %s ...\n", k)
				if err := downloader.caption(v.URL, title, v.Ext, v.Transform); err != nil {
					// nolint
				} else if downloader.option.EmbedSubtitle {
					subtitlePath, _ := utils.FilePath(title, v.Ext, downloader.option.FileNameLength, downloader.option.OutputPath, true)
					subtitleFilesToDelete = append(subtitleFilesToDelete, subtitlePath)
					if strings.HasSuffix(v.Ext, "xml") {
						if srtPath, err := utils.ConvertXMLFileToSRT(subtitlePath); err == nil {
							subtitlePath = srtPath
							subtitleFilesToDelete = append(subtitleFilesToDelete, srtPath)
						}
					}
					subtitlePaths = append(subtitlePaths, subtitlePath)
					subtitleLangs = append(subtitleLangs, k)
				}
			}
		}
	}

	// Use aria2 rpc to download
	if downloader.option.UseAria2RPC {
		return downloader.aria2(title, stream)
	}

	// Skip the complete file that has been merged
	mergedFilePath, err := utils.FilePath(title, stream.Ext, downloader.option.FileNameLength, downloader.option.OutputPath, false)
	if err != nil {
		return err
	}
	_, mergedFileExists, err := utils.FileSize(mergedFilePath)
	if err != nil {
		return err
	}
	// After the merge, the file size has changed, so we do not check whether the size matches
	if mergedFileExists {
		fmt.Printf("%s: file already exists, skipping\n", mergedFilePath)
		return nil
	}

	downloader.Bar = progressBar(stream.Size)
	if !downloader.option.Silent {
		downloader.Bar.Start()
	}
	if len(stream.Parts) == 1 {
		// only one fragment
		var err error
		if downloader.option.MultiThread {
			err = downloader.multiThreadSave(stream.Parts[0], data.URL, title)
		} else {
			err = downloader.save(stream.Parts[0], data.URL, title)
		}

		if err != nil {
			return err
		}
		downloader.Bar.Finish()

		if downloader.option.EmbedSubtitle && len(subtitlePaths) > 0 {
			if !downloader.option.Silent {
				fmt.Println("Embedding subtitles...")
			}
			if err := utils.EmbedSubtitles(mergedFilePath, subtitlePaths, subtitleLangs); err != nil {
				return err
			}
			for _, path := range subtitleFilesToDelete {
				os.Remove(path)
			}
		}
		return nil
	}

	wgp := utils.NewWaitGroupPool(downloader.option.ThreadNumber)
	// multiple fragments
	errs := make([]error, 0)
	lock := sync.Mutex{}
	parts := make([]string, len(stream.Parts))
	for index, part := range stream.Parts {
		if len(errs) > 0 {
			break
		}

		if downloader.option.AudioOnly && (part.Ext != "m4a") {
			continue
		}

		partFileName := fmt.Sprintf("%s[%d]", title, index)
		partFilePath, err := utils.FilePath(partFileName, part.Ext, downloader.option.FileNameLength, downloader.option.OutputPath, false)
		if err != nil {
			return err
		}
		parts[index] = partFilePath

		wgp.Add()
		go func(part *extractors.Part, fileName string) {
			defer wgp.Done()
			var err error
			if downloader.option.MultiThread {
				err = downloader.multiThreadSave(part, data.URL, fileName)
			} else {
				err = downloader.save(part, data.URL, fileName)
			}
			if err != nil {
				lock.Lock()
				errs = append(errs, err)
				lock.Unlock()
			}
		}(part, partFileName)
	}
	wgp.Wait()
	if len(errs) > 0 {
		return errs[0]
	}
	downloader.Bar.Finish()

	if data.Type != extractors.DataTypeVideo || downloader.option.AudioOnly {
		return nil
	}

	if !downloader.option.Silent {
		fmt.Printf("Merging video parts into %s\n", mergedFilePath)
	}
	if stream.Ext != "mp4" || stream.NeedMux {
		if err := utils.MergeFilesWithSameExtension(parts, mergedFilePath); err != nil {
			return err
		}
	} else {
		if err := utils.MergeToMP4(parts, mergedFilePath, title); err != nil {
			return err
		}
	}

	if downloader.option.EmbedSubtitle && len(subtitlePaths) > 0 {
		if !downloader.option.Silent {
			fmt.Println("Embedding subtitles...")
		}
		if err := utils.EmbedSubtitles(mergedFilePath, subtitlePaths, subtitleLangs); err != nil {
			return err
		}
		for _, path := range subtitleFilesToDelete {
			os.Remove(path)
		}
	}

	return nil
}


================================================
FILE: downloader/downloader_test.go
================================================
package downloader

import (
	"testing"

	"github.com/iawia002/lux/extractors"
)

func TestDownload(t *testing.T) {
	testCases := []struct {
		name string
		data *extractors.Data
	}{
		{
			name: "normal test",
			data: &extractors.Data{
				Site:  "douyin",
				Title: "test",
				Type:  extractors.DataTypeVideo,
				URL:   "https://www.douyin.com",
				Streams: map[string]*extractors.Stream{
					"default": {
						ID: "default",
						Parts: []*extractors.Part{
							{
								URL:  "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f9a0000bc117isuatl67cees890&line=0",
								Size: 4927877,
								Ext:  "mp4",
							},
						},
					},
				},
			},
		},
		{
			name: "multi-stream test",
			data: &extractors.Data{
				Site:  "douyin",
				Title: "test2",
				Type:  extractors.DataTypeVideo,
				URL:   "https://www.douyin.com",
				Streams: map[string]*extractors.Stream{
					"miaopai": {
						ID: "miaopai",
						Parts: []*extractors.Part{
							{
								URL:  "https://txycdn.miaopai.com/stream/KwR26jUGh2ySnVjYbQiFmomNjP14LtMU3vi6sQ__.mp4?ssig=6594aa01a78e78f50c65c164d186ba9e&time_stamp=1537070910786",
								Size: 4011590,
								Ext:  "mp4",
							},
						},
						Size: 4011590,
					},
					"douyin": {
						ID: "douyin",
						Parts: []*extractors.Part{
							{
								URL:  "https://aweme.snssdk.com/aweme/v1/playwm/?video_id=v0200f9a0000bc117isuatl67cees890&line=0",
								Size: 4927877,
								Ext:  "mp4",
							},
						},
						Size: 4927877,
					},
				},
			},
		},
		{
			name: "image test",
			data: &extractors.Data{
				Site:  "bcy",
				Title: "bcy image test",
				Type:  extractors.DataTypeImage,
				URL:   "https://www.bcyimg.com",
				Streams: map[string]*extractors.Stream{
					"default": {
						ID: "default",
						Parts: []*extractors.Part{
							{
								URL:  "http://img5.bcyimg.com/coser/143767/post/c0j7x/0d713eb41a614053ac6a3b146914f6bc.jpg/w650",
								Size: 56107,
								Ext:  "jpg",
							},
							{
								URL:  "http://img9.bcyimg.com/coser/143767/post/c0j7x/d17e9b8587794d939a1363c5f715014b.jpg/w650",
								Size: 142100,
								Ext:  "jpg",
							},
						},
					},
				},
			},
		},
	}
	for _, testCase := range testCases {
		err := New(Options{}).Download(testCase.data)
		if err != nil {
			t.Error(err)
		}
	}
}


================================================
FILE: downloader/types.go
================================================
package downloader

// Aria2RPCData defines the data structure of json RPC 2.0 info for Aria2
type Aria2RPCData struct {
	// More info about RPC interface please refer to
	// https://aria2.github.io/manual/en/html/aria2c.html#rpc-interface
	JSONRPC string `json:"jsonrpc"`
	ID      string `json:"id"`
	// For a simple download, only implemented `addUri`
	Method string `json:"method"`
	// secret, uris, options
	Params [3]interface{} `json:"params"`
}

// Aria2Input is options for `aria2.addUri`
// https://aria2.github.io/manual/en/html/aria2c.html#id3
type Aria2Input struct {
	// The file name of the downloaded file
	Out string `json:"out"`
	// For a simple download, only add headers
	Header []string `json:"header"`
}

// FilePartMeta defines the data structure of file meta info.
type FilePartMeta struct {
	Index float32
	Start int64
	End   int64
	Cur   int64
}


================================================
FILE: downloader/utils.go
================================================
package downloader

import (
	"fmt"
	"sort"

	"github.com/fatih/color"

	"github.com/iawia002/lux/extractors"
)

var (
	blue = color.New(color.FgBlue)
	cyan = color.New(color.FgCyan)
)

func genSortedStreams(streams map[string]*extractors.Stream) []*extractors.Stream {
	sortedStreams := make([]*extractors.Stream, 0, len(streams))
	for _, data := range streams {
		sortedStreams = append(sortedStreams, data)
	}
	if len(sortedStreams) > 1 {
		sort.SliceStable(
			sortedStreams, func(i, j int) bool { return sortedStreams[i].Size > sortedStreams[j].Size },
		)
	}
	return sortedStreams
}

func printHeader(data *extractors.Data) {
	fmt.Println()
	cyan.Printf(" Site:      ") // nolint
	fmt.Println(data.Site)
	cyan.Printf(" Title:     ") // nolint
	fmt.Println(data.Title)
	cyan.Printf(" Type:      ") // nolint
	fmt.Println(data.Type)
}

func printStream(stream *extractors.Stream) {
	blue.Println(fmt.Sprintf("     [%s]  -------------------", stream.ID)) // nolint
	if stream.Quality != "" {
		cyan.Printf("     Quality:         ") // nolint
		fmt.Println(stream.Quality)
	}
	cyan.Printf("     Size:            ") // nolint
	fmt.Printf("%.2f MiB (%d Bytes)\n", float64(stream.Size)/(1024*1024), stream.Size)
	cyan.Printf("     # download with: ") // nolint
	fmt.Printf("lux -f %s ...\n\n", stream.ID)
}

func printInfo(data *extractors.Data, sortedStreams []*extractors.Stream) {
	printHeader(data)
	if len(data.Captions) > 0 {
		cyan.Printf(" Captions:  ") // nolint
		languages := make([]string, 0, len(data.Captions))
		for lang := range data.Captions {
			languages = append(languages, lang)
		}
		sort.Strings(languages)
		captionList := ""
		for _, lang := range languages {
			caption := data.Captions[lang]
			if caption == nil {
				continue
			}
			captionList += fmt.Sprintf("%s ", lang)
		}
		fmt.Println(captionList)
	}
	cyan.Printf(" Streams:   ") // nolint
	fmt.Println("# All available quality")
	for _, stream := range sortedStreams {
		printStream(stream)
	}
}

func printStreamInfo(data *extractors.Data, stream *extractors.Stream) {
	printHeader(data)

	cyan.Printf(" Stream:   ") // nolint
	fmt.Println()
	printStream(stream)
}


================================================
FILE: extractors/acfun/acfun.go
================================================
package acfun

import (
	"fmt"
	"net/url"
	"regexp"

	jsoniter "github.com/json-iterator/go"
	"github.com/pkg/errors"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/parser"
	"github.com/iawia002/lux/request"
	"github.com/iawia002/lux/utils"
)

func init() {
	extractors.Register("acfun", New())
}

const (
	bangumiDataPattern = "window.pageInfo = window.bangumiData = (.*);"
	bangumiListPattern = "window.bangumiList = (.*);"

	bangumiHTMLURL = "https://www.acfun.cn/bangumi/aa%d_36188_%d"

	referer = "https://www.acfun.cn"
)

type extractor struct{}

// New returns a new acfun bangumi extractor
func New() extractors.Extractor {
	return &extractor{}
}

// Extract ...
func (e *extractor) Extract(URL string, option extractors.Options) ([]*extractors.Data, error) {
	html, err := request.GetByte(URL, referer, nil)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	epDatas := make([]*episodeData, 0)

	if option.Playlist {
		list, err := resolvingEpisodes(html)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		items := utils.NeedDownloadList(option.Items, option.ItemStart, option.ItemEnd, len(list.Episodes))

		for _, item := range items {
			epDatas = append(epDatas, list.Episodes[item-1])
		}
	} else {
		bgData, _, err := resolvingData(html)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		epDatas = append(epDatas, &bgData.episodeData)
	}

	datas := make([]*extractors.Data, 0)

	wgp := utils.NewWaitGroupPool(option.ThreadNumber)
	for _, epData := range epDatas {
		t := epData
		wgp.Add()
		go func() {
			defer wgp.Done()
			datas = append(datas, extractBangumi(concatURL(t)))
		}()
	}
	wgp.Wait()
	return datas, nil
}

func concatURL(epData *episodeData) string {
	return fmt.Sprintf(bangumiHTMLURL, epData.BangumiID, epData.ItemID)
}

func extractBangumi(URL string) *extractors.Data {
	var err error
	html, err := request.GetByte(URL, referer, nil)
	if err != nil {
		return extractors.EmptyData(URL, err)
	}

	_, vInfo, err := resolvingData(html)
	if err != nil {
		return extractors.EmptyData(URL, err)
	}

	streams := make(map[string]*extractors.Stream)

	for _, stm := range vInfo.AdaptationSet[0].Streams {
		m3u8URL, err := url.Parse(stm.URL)
		if err != nil {
			return extractors.EmptyData(URL, err)
		}

		urls, err := utils.M3u8URLs(m3u8URL.String())
		if err != nil {
			_, err = url.Parse(stm.URL)
			if err != nil {
				return extractors.EmptyData(URL, err)
			}

			urls, err = utils.M3u8URLs(stm.BackURL)
			if err != nil {
				return extractors.EmptyData(URL, err)
			}
		}

		// There is no size information in the m3u8 file and the calculation will take too much time, just ignore it.
		parts := make([]*extractors.Part, 0)
		for _, u := range urls {
			parts = append(parts, &extractors.Part{
				URL: u,
				Ext: "ts",
			})
		}
		streams[stm.QualityLabel] = &extractors.Stream{
			ID:      stm.QualityType,
			Parts:   parts,
			Quality: stm.QualityType,
			NeedMux: false,
		}
	}

	doc, err := parser.GetDoc(string(html))
	if err != nil {
		return extractors.EmptyData(URL, err)
	}
	data := &extractors.Data{
		Site:    "AcFun acfun.cn",
		Title:   parser.Title(doc),
		Type:    extractors.DataTypeVideo,
		Streams: streams,
		URL:     URL,
	}
	return data
}

func resolvingData(html []byte) (*bangumiData, *videoInfo, error) {
	bgData := &bangumiData{}
	vInfo := &videoInfo{}

	pattern, _ := regexp.Compile(bangumiDataPattern)

	groups := pattern.FindSubmatch(html)
	err := jsoniter.Unmarshal(groups[1], bgData)
	if err != nil {
		return nil, nil, errors.WithStack(err)
	}

	err = jsoniter.UnmarshalFromString(bgData.CurrentVideoInfo.KsPlayJSON, vInfo)
	if err != nil {
		return nil, nil, errors.WithStack(err)
	}
	return bgData, vInfo, nil
}

func resolvingEpisodes(html []byte) (*episodeList, error) {
	list := &episodeList{}
	pattern, _ := regexp.Compile(bangumiListPattern)

	groups := pattern.FindSubmatch(html)
	err := jsoniter.Unmarshal(groups[1], list)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	return list, nil
}


================================================
FILE: extractors/acfun/acfun_test.go
================================================
package acfun

import (
	"testing"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/test"
)

func TestDownload(t *testing.T) {
	tests := []struct {
		name string
		args test.Args
	}{
		{
			name: "normal test",
			args: test.Args{
				URL:   "https://www.acfun.cn/bangumi/aa6000686_36188_1704167",
				Title: "瑞克和莫蒂 第四季 :第2话 注释版",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			data, err := New().Extract(tt.args.URL, extractors.Options{})
			test.CheckError(t, err)
			test.Check(t, tt.args, data[0])
		})
	}
}


================================================
FILE: extractors/acfun/types.go
================================================
package acfun

type episodeData struct {
	ItemID      int64  `json:"itemId"`
	EpisodeName string `json:"episodeName"`
	BangumiID   int64  `json:"bangumiId"`
	VideoID     int64  `json:"videoId"`
}

type bangumiData struct {
	episodeData
	BangumiTitle     string `json:"bangumiTitle"`
	CurrentVideoInfo struct {
		KsPlayJSON string `json:"ksPlayJson"`
	} `json:"currentVideoInfo"`
}

type videoInfo struct {
	AdaptationSet []struct {
		Streams streams `json:"representation"`
	} `json:"adaptationSet"`
}

type streams []stream

type episodeList struct {
	Episodes []*episodeData `json:"items"`
}

type stream struct {
	ID           int64  `json:"id"`
	BackURL      string `json:"backUrl"`
	Codecs       string `json:"codecs"`
	URL          string `json:"url"`
	BitRate      int64  `json:"avgBitrate"`
	QualityType  string `json:"qualityType"`
	QualityLabel string `json:"qualityLabel"`
}


================================================
FILE: extractors/bcy/bcy.go
================================================
package bcy

import (
	"encoding/json"
	"strings"

	"github.com/pkg/errors"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/parser"
	"github.com/iawia002/lux/request"
	"github.com/iawia002/lux/utils"
)

func init() {
	extractors.Register("bcy", New())
}

type bcyData struct {
	Detail struct {
		PostData struct {
			Multi []struct {
				OriginalPath string `json:"original_path"`
			} `json:"multi"`
		} `json:"post_data"`
	} `json:"detail"`
}

type extractor struct{}

// New returns a bcy extractor.
func New() extractors.Extractor {
	return &extractor{}
}

// Extract is the main function to extract the data.
func (e *extractor) Extract(url string, option extractors.Options) ([]*extractors.Data, error) {
	html, err := request.Get(url, url, nil)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	// parse json data
	rep := strings.NewReplacer(`\"`, `"`, `\\`, `\`)
	realURLs := utils.MatchOneOf(html, `JSON.parse\("(.+?)"\);`)
	if realURLs == nil || len(realURLs) < 2 {
		return nil, errors.WithStack(extractors.ErrURLParseFailed)
	}
	jsonString := rep.Replace(realURLs[1])

	var data bcyData
	if err = json.Unmarshal([]byte(jsonString), &data); err != nil {
		return nil, errors.WithStack(err)
	}

	doc, err := parser.GetDoc(html)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	title := strings.Replace(parser.Title(doc), " - 半次元 banciyuan - ACG爱好者社区", "", -1)

	parts := make([]*extractors.Part, 0, len(data.Detail.PostData.Multi))
	var totalSize int64
	for _, img := range data.Detail.PostData.Multi {
		size, err := request.Size(img.OriginalPath, url)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		totalSize += size
		_, ext, err := utils.GetNameAndExt(img.OriginalPath)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		parts = append(parts, &extractors.Part{
			URL:  img.OriginalPath,
			Size: size,
			Ext:  ext,
		})
	}
	streams := map[string]*extractors.Stream{
		"default": {
			Parts: parts,
			Size:  totalSize,
		},
	}
	return []*extractors.Data{
		{
			Site:    "半次元 bcy.net",
			Title:   title,
			Type:    extractors.DataTypeImage,
			Streams: streams,
			URL:     url,
		},
	}, nil
}


================================================
FILE: extractors/bcy/bcy_test.go
================================================
package bcy

import (
	"testing"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/test"
)

func TestDownload(t *testing.T) {
	tests := []struct {
		name string
		args test.Args
	}{
		{
			name: "normal test",
			args: test.Args{
				URL:   "https://bcy.net/item/detail/6558738153367142664",
				Title: "cos正片 命运石之门 牧濑红莉栖 克里斯蒂娜… - 半次元 - ACG爱好者社区",
				Size:  13035763,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			data, err := New().Extract(tt.args.URL, extractors.Options{})
			test.CheckError(t, err)
			test.Check(t, tt.args, data[0])
		})
	}
}


================================================
FILE: extractors/bilibili/bilibili.go
================================================
package bilibili

import (
	"encoding/json"
	"fmt"
	"slices"
	"sort"
	"strconv"
	"strings"
	"time"

	"github.com/pkg/errors"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/parser"
	"github.com/iawia002/lux/request"
	"github.com/iawia002/lux/utils"
)

func init() {
	bilibiliExtractor := New()
	extractors.Register("bilibili", bilibiliExtractor)
	extractors.Register("b23", bilibiliExtractor)
}

const (
	bilibiliAPI        = "https://api.bilibili.com/x/player/playurl?"
	bilibiliBangumiAPI = "https://api.bilibili.com/pgc/player/web/playurl?"
	bilibiliTokenAPI   = "https://api.bilibili.com/x/player/playurl/token?"
)

const referer = "https://www.bilibili.com"

var utoken string

func genAPI(aid, cid, quality int, bvid string, bangumi bool, cookie string) (string, error) {
	var (
		err        error
		baseAPIURL string
		params     string
	)
	if cookie != "" && utoken == "" {
		utoken, err = request.Get(
			fmt.Sprintf("%said=%d&cid=%d", bilibiliTokenAPI, aid, cid),
			referer,
			nil,
		)
		if err != nil {
			return "", err
		}
		var t token
		err = json.Unmarshal([]byte(utoken), &t)
		if err != nil {
			return "", err
		}
		if t.Code != 0 {
			return "", errors.Errorf("cookie error: %s", t.Message)
		}
		utoken = t.Data.Token
	}
	var api string
	if bangumi {
		// The parameters need to be sorted by name
		// qn=0 flag makes the CDN address different every time
		// quality=120(4k) is the highest quality so far
		params = fmt.Sprintf(
			"cid=%d&bvid=%s&qn=%d&type=&otype=json&fourk=1&fnver=0&fnval=16",
			cid, bvid, quality,
		)
		baseAPIURL = bilibiliBangumiAPI
	} else {
		params = fmt.Sprintf(
			"avid=%d&cid=%d&bvid=%s&qn=%d&type=&otype=json&fourk=1&fnver=0&fnval=2000",
			aid, cid, bvid, quality,
		)
		baseAPIURL = bilibiliAPI
	}
	api = baseAPIURL + params
	// bangumi utoken also need to put in params to sign, but the ordinary video doesn't need
	if !bangumi && utoken != "" {
		api = fmt.Sprintf("%s&utoken=%s", api, utoken)
	}
	return api, nil
}

type bilibiliOptions struct {
	url      string
	html     string
	bangumi  bool
	aid      int
	cid      int
	bvid     string
	page     int
	subtitle string
}

func extractBangumi(url, html string, extractOption extractors.Options) ([]*extractors.Data, error) {
	dataString := utils.MatchOneOf(html, `const playurlSSRData = ({[\s\S]+})`)[1]
	epArrayString := utils.MatchOneOf(dataString, `"episode_info"\s*:\s*(.+?)\s*,\s*"season_info"`)[1]
	fullVideoIdString := utils.MatchOneOf(dataString, `"videoId"\s*:\s*"(ep|ss)(\d+)"`)
	epSsString := fullVideoIdString[1] // "ep" or "ss"
	videoIdString := fullVideoIdString[2]

	var epArray EpVideoInfo
	err := json.Unmarshal([]byte(epArrayString), &epArray)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	var data bangumiData

	videoId, err := strconv.ParseInt(videoIdString, 10, 0)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if epArray.EpID == int(videoId) || (epSsString == "ss" && epArray.Title == "第1话") {
		data.EpInfo = epArray
	}
	data.EpList = append(data.EpList, epArray)

	sort.Slice(data.EpList, func(i, j int) bool {
		return data.EpList[i].EpID < data.EpList[j].EpID
	})

	if !extractOption.Playlist {
		aid := data.EpInfo.Aid
		cid := data.EpInfo.Cid
		bvid := data.EpInfo.Bvid
		titleFormat := data.EpInfo.Title
		longTitle := data.EpInfo.LongTitle
		if aid <= 0 || cid <= 0 || bvid == "" {
			aid = data.EpList[0].Aid
			cid = data.EpList[0].Cid
			bvid = data.EpList[0].Bvid
			titleFormat = data.EpList[0].Title
			longTitle = data.EpList[0].LongTitle
		}
		options := bilibiliOptions{
			url:     url,
			html:    html,
			bangumi: true,
			aid:     aid,
			cid:     cid,
			bvid:    bvid,

			subtitle: fmt.Sprintf("%s %s", titleFormat, longTitle),
		}
		return []*extractors.Data{bilibiliDownload(options, extractOption)}, nil
	}

	// handle bangumi playlist
	needDownloadItems := utils.NeedDownloadList(extractOption.Items, extractOption.ItemStart, extractOption.ItemEnd, len(data.EpList))
	extractedData := make([]*extractors.Data, len(needDownloadItems))
	wgp := utils.NewWaitGroupPool(extractOption.ThreadNumber)
	dataIndex := 0
	for index, u := range data.EpList {
		if !slices.Contains(needDownloadItems, index+1) {
			continue
		}
		wgp.Add()
		id := u.EpID
		if id == 0 {
			id = u.EpID
		}
		// html content can't be reused here
		options := bilibiliOptions{
			url:     fmt.Sprintf("https://www.bilibili.com/bangumi/play/ep%d", id),
			bangumi: true,
			aid:     u.Aid,
			cid:     u.Cid,
			bvid:    u.Bvid,

			subtitle: fmt.Sprintf("%s %s", u.Title, u.LongTitle),
		}
		go func(index int, options bilibiliOptions, extractedData []*extractors.Data) {
			defer wgp.Done()
			extractedData[index] = bilibiliDownload(options, extractOption)
		}(dataIndex, options, extractedData)
		dataIndex++
	}
	wgp.Wait()
	return extractedData, nil
}

func getMultiPageData(html string) (*multiPage, error) {
	var data multiPage
	multiPageDataString := utils.MatchOneOf(
		html, `window.__INITIAL_STATE__=(.+?);\(function`,
	)
	if multiPageDataString == nil {
		return &data, errors.New("this page has no playlist")
	}
	err := json.Unmarshal([]byte(multiPageDataString[1]), &data)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	return &data, nil
}

func extractFestival(url, html string, extractOption extractors.Options) ([]*extractors.Data, error) {
	matches := utils.MatchAll(html, "<\\s*script[^>]*>\\s*window\\.__INITIAL_STATE__=([\\s\\S]*?);\\s?\\(function[\\s\\S]*?<\\/\\s*script\\s*>")
	if len(matches) < 1 {
		return nil, errors.WithStack(extractors.ErrURLParseFailed)
	}
	if len(matches[0]) < 2 {
		return nil, errors.New("could not find video in page")
	}

	var festivalData festival
	err := json.Unmarshal([]byte(matches[0][1]), &festivalData)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	options := bilibiliOptions{
		url:  url,
		html: html,
		aid:  festivalData.VideoInfo.Aid,
		bvid: festivalData.VideoInfo.BVid,
		cid:  festivalData.VideoInfo.Cid,
		page: 0,
	}

	return []*extractors.Data{bilibiliDownload(options, extractOption)}, nil
}

func extractNormalVideo(url, html string, extractOption extractors.Options) ([]*extractors.Data, error) {
	pageData, err := getMultiPageData(html)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	if !extractOption.Playlist {
		// handle URL that has a playlist, mainly for unified titles
		// <h1> tag does not include subtitles
		// bangumi doesn't need this
		pageString := utils.MatchOneOf(url, `\?p=(\d+)`)
		var p int
		if pageString == nil {
			// https://www.bilibili.com/video/av20827366/
			p = 1
		} else {
			// https://www.bilibili.com/video/av20827366/?p=2
			p, _ = strconv.Atoi(pageString[1])
		}

		if len(pageData.VideoData.Pages) < p || p < 1 {
			return nil, errors.WithStack(extractors.ErrURLParseFailed)
		}

		page := pageData.VideoData.Pages[p-1]
		options := bilibiliOptions{
			url:  url,
			html: html,
			aid:  pageData.Aid,
			bvid: pageData.BVid,
			cid:  page.Cid,
			page: p,
		}
		// "part":"" or "part":"Untitled"
		if page.Part == "Untitled" || len(pageData.VideoData.Pages) == 1 {
			options.subtitle = ""
		} else {
			options.subtitle = page.Part
		}
		return []*extractors.Data{bilibiliDownload(options, extractOption)}, nil
	}

	// handle normal video playlist
	if len(pageData.Sections) == 0 {
		// https://www.bilibili.com/video/av20827366/?p=* each video in playlist has different p=?
		return multiPageDownload(url, html, extractOption, pageData)
	}
	// handle another kind of playlist
	// https://www.bilibili.com/video/av*** each video in playlist has different av/bv id
	return multiEpisodeDownload(url, html, extractOption, pageData)
}

// handle multi episode download
func multiEpisodeDownload(url, html string, extractOption extractors.Options, pageData *multiPage) ([]*extractors.Data, error) {
	needDownloadItems := utils.NeedDownloadList(extractOption.Items, extractOption.ItemStart, extractOption.ItemEnd, len(pageData.Sections[0].Episodes))
	extractedData := make([]*extractors.Data, len(needDownloadItems))
	wgp := utils.NewWaitGroupPool(extractOption.ThreadNumber)
	dataIndex := 0
	for index, u := range pageData.Sections[0].Episodes {
		if !slices.Contains(needDownloadItems, index+1) {
			continue
		}
		wgp.Add()
		options := bilibiliOptions{
			url:      url,
			html:     html,
			aid:      u.Aid,
			bvid:     u.BVid,
			cid:      u.Cid,
			subtitle: fmt.Sprintf("%s P%d", u.Title, index+1),
		}
		go func(index int, options bilibiliOptions, extractedData []*extractors.Data) {
			defer wgp.Done()
			extractedData[index] = bilibiliDownload(options, extractOption)
		}(dataIndex, options, extractedData)
		dataIndex++
	}
	wgp.Wait()
	return extractedData, nil
}

// handle multi page download
func multiPageDownload(url, html string, extractOption extractors.Options, pageData *multiPage) ([]*extractors.Data, error) {
	needDownloadItems := utils.NeedDownloadList(extractOption.Items, extractOption.ItemStart, extractOption.ItemEnd, len(pageData.VideoData.Pages))
	extractedData := make([]*extractors.Data, len(needDownloadItems))
	wgp := utils.NewWaitGroupPool(extractOption.ThreadNumber)
	dataIndex := 0
	for index, u := range pageData.VideoData.Pages {
		if !slices.Contains(needDownloadItems, index+1) {
			continue
		}
		wgp.Add()
		options := bilibiliOptions{
			url:      url,
			html:     html,
			aid:      pageData.Aid,
			bvid:     pageData.BVid,
			cid:      u.Cid,
			subtitle: u.Part,
			page:     u.Page,
		}
		go func(index int, options bilibiliOptions, extractedData []*extractors.Data) {
			defer wgp.Done()
			extractedData[index] = bilibiliDownload(options, extractOption)
		}(dataIndex, options, extractedData)
		dataIndex++
	}
	wgp.Wait()
	return extractedData, nil
}

type extractor struct{}

// New returns a bilibili extractor.
func New() extractors.Extractor {
	return &extractor{}
}

// Extract is the main function to extract the data.
func (e *extractor) Extract(url string, option extractors.Options) ([]*extractors.Data, error) {
	var err error
	html, err := request.Get(url, referer, nil)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	// set thread number to 1 manually to avoid http 412 error
	option.ThreadNumber = 1

	if strings.Contains(url, "bangumi") {
		// handle bangumi
		return extractBangumi(url, html, option)
	} else if strings.Contains(url, "festival") {
		return extractFestival(url, html, option)
	} else {
		// handle normal video
		return extractNormalVideo(url, html, option)
	}
}

// bilibiliDownload is the download function for a single URL
func bilibiliDownload(options bilibiliOptions, extractOption extractors.Options) *extractors.Data {
	var (
		err  error
		html string
	)
	if options.html != "" {
		// reuse html string, but this can't be reused in case of playlist
		html = options.html
	} else {
		html, err = request.Get(options.url, referer, nil)
		if err != nil {
			return extractors.EmptyData(options.url, err)
		}
	}

	// Get "accept_quality" and "accept_description"
	// "accept_description":["超高清 8K","超清 4K","高清 1080P+","高清 1080P","高清 720P","清晰 480P","流畅 360P"],
	// "accept_quality":[127,120,112,80,48,32,16],
	api, err := genAPI(options.aid, options.cid, 127, options.bvid, options.bangumi, extractOption.Cookie)
	if err != nil {
		return extractors.EmptyData(options.url, err)
	}
	jsonString, err := request.Get(api, referer, nil)
	if err != nil {
		return extractors.EmptyData(options.url, err)
	}

	var data dash
	err = json.Unmarshal([]byte(jsonString), &data)
	if err != nil {
		return extractors.EmptyData(options.url, err)
	}
	var dashData dashInfo
	if data.Data.Description == nil {
		dashData = data.Result
	} else {
		dashData = data.Data
	}

	var audioPart *extractors.Part
	if dashData.Streams.Audio != nil {
		// Get audio part
		var audioID int
		audios := map[int]string{}
		bandwidth := 0
		for _, stream := range dashData.Streams.Audio {
			if stream.Bandwidth > bandwidth {
				audioID = stream.ID
				bandwidth = stream.Bandwidth
			}
			audios[stream.ID] = stream.BaseURL
		}
		s, err := request.Size(audios[audioID], referer)
		if err != nil {
			return extractors.EmptyData(options.url, err)
		}
		audioPart = &extractors.Part{
			URL:  audios[audioID],
			Size: s,
			Ext:  "m4a",
		}
	}

	streams := make(map[string]*extractors.Stream, len(dashData.Quality))
	for _, stream := range dashData.Streams.Video {
		s, err := request.Size(stream.BaseURL, referer)
		if err != nil {
			return extractors.EmptyData(options.url, err)
		}
		parts := make([]*extractors.Part, 0, 2)
		parts = append(parts, &extractors.Part{
			URL:  stream.BaseURL,
			Size: s,
			Ext:  getExtFromMimeType(stream.MimeType),
		})
		if audioPart != nil {
			parts = append(parts, audioPart)
		}
		var size int64
		for _, part := range parts {
			size += part.Size
		}
		id := fmt.Sprintf("%d-%d", stream.ID, stream.Codecid)
		streams[id] = &extractors.Stream{
			Parts:   parts,
			Size:    size,
			Quality: fmt.Sprintf("%s %s", qualityString[stream.ID], stream.Codecs),
		}
		if audioPart != nil {
			streams[id].NeedMux = true
		}
	}

	for _, durl := range dashData.DURLs {
		var ext string
		switch dashData.DURLFormat {
		case "flv", "flv480":
			ext = "flv"
		case "mp4", "hdmp4": // nolint
			ext = "mp4"
		}

		parts := make([]*extractors.Part, 0, 1)
		parts = append(parts, &extractors.Part{
			URL:  durl.URL,
			Size: durl.Size,
			Ext:  ext,
		})

		streams[strconv.Itoa(dashData.CurQuality)] = &extractors.Stream{
			Parts:   parts,
			Size:    durl.Size,
			Quality: qualityString[dashData.CurQuality],
		}
	}

	// get the title
	doc, err := parser.GetDoc(html)
	if err != nil {
		return extractors.EmptyData(options.url, err)
	}
	title := parser.Title(doc)
	if options.subtitle != "" {
		pageString := ""
		if options.page > 0 {
			pageString = fmt.Sprintf("P%d ", options.page)
		}
		if extractOption.EpisodeTitleOnly {
			title = fmt.Sprintf("%s%s", pageString, options.subtitle)
		} else {
			title = fmt.Sprintf("%s %s%s", title, pageString, options.subtitle)
		}
	}

	return &extractors.Data{
		Site:    "哔哩哔哩 bilibili.com",
		Title:   title,
		Type:    extractors.DataTypeVideo,
		Streams: streams,
		Captions: map[string]*extractors.CaptionPart{
			"danmaku": {
				Part: extractors.Part{
					URL: fmt.Sprintf("https://comment.bilibili.com/%d.xml", options.cid),
					Ext: "xml",
				},
			},
			"subtitle": getSubTitleCaptionPart(options.aid, options.cid),
		},
		URL: options.url,
	}
}

func getExtFromMimeType(mimeType string) string {
	exts := strings.Split(mimeType, "/")
	if len(exts) == 2 {
		return exts[1]
	}
	return "mp4"
}

func getSubTitleCaptionPart(aid int, cid int) *extractors.CaptionPart {
	jsonString, err := request.Get(
		fmt.Sprintf("http://api.bilibili.com/x/player/wbi/v2?aid=%d&cid=%d", aid, cid), referer, nil,
	)
	if err != nil {
		return nil
	}
	stu := bilibiliWebInterface{}
	err = json.Unmarshal([]byte(jsonString), &stu)
	if err != nil || len(stu.Data.SubtitleInfo.SubtitleList) == 0 {
		return nil
	}
	return &extractors.CaptionPart{
		Part: extractors.Part{
			URL: fmt.Sprintf("https:%s", stu.Data.SubtitleInfo.SubtitleList[0].SubtitleUrl),
			Ext: "srt",
		},
		Transform: subtitleTransform,
	}
}

func subtitleTransform(body []byte) ([]byte, error) {
	bytes := ""
	captionData := bilibiliSubtitleFormat{}
	err := json.Unmarshal(body, &captionData)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	for i := 0; i < len(captionData.Body); i++ {
		bytes += fmt.Sprintf("%d\n%s --> %s\n%s\n\n",
			i,
			time.Unix(0, int64(captionData.Body[i].From*1000)*int64(time.Millisecond)).UTC().Format("15:04:05.000"),
			time.Unix(0, int64(captionData.Body[i].To*1000)*int64(time.Millisecond)).UTC().Format("15:04:05.000"),
			captionData.Body[i].Content,
		)
	}
	return []byte(bytes), nil
}


================================================
FILE: extractors/bilibili/bilibili_test.go
================================================
package bilibili

import (
	"testing"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/test"
)

func TestBilibili(t *testing.T) {
	tests := []struct {
		name     string
		args     test.Args
		playlist bool
	}{
		{
			name: "normal test 1",
			args: test.Args{
				URL:   "https://www.bilibili.com/video/av20203945/",
				Title: "【2018拜年祭单品】相遇day by day",
			},
			playlist: false,
		},
		{
			name: "normal test 2",
			args: test.Args{
				URL:   "https://www.bilibili.com/video/av41301960",
				Title: "【英雄联盟】2019赛季CG 《觉醒》",
			},
			playlist: false,
		},
		{
			name: "bangumi test",
			args: test.Args{
				URL:   "https://www.bilibili.com/bangumi/play/ep167000",
				Title: "狐妖小红娘 第70话 苏苏智商上线",
			},
		},
		{
			name: "bangumi playlist test",
			args: test.Args{
				URL:   "https://www.bilibili.com/bangumi/play/ss5050",
				Title: "一人之下:第1话 异人刀兵起,道炁携阴阳",
			},
			playlist: true,
		},
		{
			name: "playlist test",
			args: test.Args{
				URL:   "https://www.bilibili.com/video/av16907446/",
				Title: "\"不要相信歌词,他们为了押韵什么都干得出来\"",
			},
			playlist: true,
		},
		{
			name: "8k test",
			args: test.Args{
				URL:   "https://www.bilibili.com/video/BV1qM4y1w716",
				Title: "【8K演示片】B站首发!你的设备还顶得住吗?",
			},
		},
		{
			name: "b23 test",
			args: test.Args{
				URL:   "https://b23.tv/Fc9i7QF",
				Title: "【十年榜】2000-2009年最强华语金曲TOP100 P1 100爱转角-罗志祥",
			},
		},
		{
			name: "festival test",
			args: test.Args{
				URL:   "https://www.bilibili.com/festival/lty10th?bvid=BV1dZ4y1Y7bt",
				Title: "洛天依十周年官方演唱会",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			var (
				data []*extractors.Data
				err  error
			)
			if tt.playlist {
				// for playlist, we don't check the data
				_, err = New().Extract(tt.args.URL, extractors.Options{
					Playlist:     true,
					ThreadNumber: 9,
				})
				test.CheckError(t, err)
			} else {
				data, err = New().Extract(tt.args.URL, extractors.Options{})
				test.CheckError(t, err)
				test.Check(t, tt.args, data[0])
			}
		})
	}
}


================================================
FILE: extractors/bilibili/types.go
================================================
package bilibili

// {"code":0,"message":"0","ttl":1,"data":{"token":"aaa"}}
// {"code":-101,"message":"账号未登录","ttl":1}
type tokenData struct {
	Token string `json:"token"`
}

type token struct {
	Code    int       `json:"code"`
	Message string    `json:"message"`
	Data    tokenData `json:"data"`
}

type Interaction struct {
	Interaction bool `json:"interaction"`
}

type EpVideoInfo struct {
	Aid                           int         `json:"aid"`
	Bvid                          string      `json:"bvid"`
	Cid                           int         `json:"cid"`
	DeliveryBusinessFragmentVideo bool        `json:"delivery_business_fragment_video"`
	DeliveryFragmentVideo         bool        `json:"delivery_fragment_video"`
	EpID                          int         `json:"ep_id"`
	EpStatus                      int         `json:"ep_status"`
	Interaction                   Interaction `json:"interaction"`
	LongTitle                     string      `json:"long_title"`
	Title                         string      `json:"title"`
}

type bangumiData struct {
	EpInfo EpVideoInfo   `json:"epInfo"`
	EpList []EpVideoInfo `json:"epList"`
}

type videoPagesData struct {
	Cid  int    `json:"cid"`
	Part string `json:"part"`
	Page int    `json:"page"`
}

type multiPageVideoData struct {
	Title string           `json:"title"`
	Pages []videoPagesData `json:"pages"`
}

type episode struct {
	Aid   int    `json:"aid"`
	Cid   int    `json:"cid"`
	Title string `json:"title"`
	BVid  string `json:"bvid"`
}

type multiEpisodeData struct {
	Seasionid int       `json:"season_id"`
	Episodes  []episode `json:"episodes"`
}

type multiPage struct {
	Aid       int                `json:"aid"`
	BVid      string             `json:"bvid"`
	Sections  []multiEpisodeData `json:"sections"`
	VideoData multiPageVideoData `json:"videoData"`
}

type dashStream struct {
	ID        int    `json:"id"`
	BaseURL   string `json:"baseUrl"`
	Bandwidth int    `json:"bandwidth"`
	MimeType  string `json:"mimeType"`
	Codecid   int    `json:"codecid"`
	Codecs    string `json:"codecs"`
}

type dashStreams struct {
	Video []dashStream `json:"video"`
	Audio []dashStream `json:"audio"`
}

type dashInfo struct {
	CurQuality  int         `json:"quality"`
	Description []string    `json:"accept_description"`
	Quality     []int       `json:"accept_quality"`
	Streams     dashStreams `json:"dash"`
	DURLFormat  string      `json:"format"`
	DURLs       []dURL      `json:"durl"`
}

type dURL struct {
	URL  string `json:"url"`
	Size int64  `json:"size"`
}

type dash struct {
	Code    int      `json:"code"`
	Message string   `json:"message"`
	Data    dashInfo `json:"data"`
	Result  dashInfo `json:"result"`
}

var qualityString = map[int]string{
	127: "超高清 8K",
	120: "超清 4K",
	116: "高清 1080P60",
	74:  "高清 720P60",
	112: "高清 1080P+",
	80:  "高清 1080P",
	64:  "高清 720P",
	48:  "高清 720P",
	32:  "清晰 480P",
	16:  "流畅 360P",
	15:  "流畅 360P",
}

type subtitleData struct {
	From     float32 `json:"from"`
	To       float32 `json:"to"`
	Location int     `json:"location"`
	Content  string  `json:"content"`
}

type bilibiliSubtitleFormat struct {
	FontSize        float32        `json:"font_size"`
	FontColor       string         `json:"font_color"`
	BackgroundAlpha float32        `json:"background_alpha"`
	BackgroundColor string         `json:"background_color"`
	Stroke          string         `json:"Stroke"`
	Body            []subtitleData `json:"body"`
}

type subtitleProperty struct {
	ID          int64  `json:"id"`
	Lan         string `json:"lan"`
	LanDoc      string `json:"lan_doc"`
	SubtitleUrl string `json:"subtitle_url"`
}

type subtitleInfo struct {
	AllowSubmit  bool               `json:"allow_submit"`
	SubtitleList []subtitleProperty `json:"subtitles"`
}

type bilibiliWebInterfaceData struct {
	Bvid         string       `json:"bvid"`
	SubtitleInfo subtitleInfo `json:"subtitle"`
}

type bilibiliWebInterface struct {
	Code int                      `json:"code"`
	Data bilibiliWebInterfaceData `json:"data"`
}

type festival struct {
	VideoSections []struct {
		Id    int64  `json:"id"`
		Title string `json:"title"`
		Type  int    `json:"type"`
	} `json:"videoSections"`
	Episodes  []episode `json:"episodes"`
	VideoInfo struct {
		Aid   int    `json:"aid"`
		BVid  string `json:"bvid"`
		Cid   int    `json:"cid"`
		Title string `json:"title"`
		Desc  string `json:"desc"`
		Pages []struct {
			Cid       int    `json:"cid"`
			Duration  int    `json:"duration"`
			Page      int    `json:"page"`
			Part      string `json:"part"`
			Dimension struct {
				Width  int `json:"width"`
				Height int `json:"height"`
				Rotate int `json:"rotate"`
			} `json:"dimension"`
		} `json:"pages"`
	} `json:"videoInfo"`
}


================================================
FILE: extractors/bitchute/bitchute.go
================================================
package bitchute

import (
	"compress/flate"
	"compress/gzip"
	"fmt"
	"io"
	"net/http"
	"regexp"
	"strings"

	"github.com/pkg/errors"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/request"
)

func init() {
	extractors.Register("bitchute", New())
}

type extractor struct{}

// New returns a bitchute extractor.
func New() extractors.Extractor {
	return &extractor{}
}

// Extract is the main function to extract the data.
func (e *extractor) Extract(u string, option extractors.Options) ([]*extractors.Data, error) {
	regVideoID := regexp.MustCompile(`/video/([^?/]+)`)
	matchVideoID := regVideoID.FindStringSubmatch(u)
	if len(matchVideoID) < 2 {
		return nil, errors.New("Invalid video URL: Missing video ID parameter")
	}
	embedURL := fmt.Sprintf("https://www.bitchute.com/api/beta9/embed/?videoID=%s", matchVideoID[1])

	res, err := request.Request(http.MethodGet, embedURL, nil, nil)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	defer res.Body.Close() // nolint

	var reader io.ReadCloser
	switch res.Header.Get("Content-Encoding") {
	case "gzip":
		reader, _ = gzip.NewReader(res.Body)
	case "deflate":
		reader = flate.NewReader(res.Body)
	default:
		reader = res.Body
	}
	defer reader.Close() // nolint

	b, err := io.ReadAll(reader)
	if err != nil {
		return nil, errors.WithStack(err)
	}

	// There is also an API that provides meta data
	// POST https://api.bitchute.com/api/beta9/video {"video_id": <video_id>}
	html := string(b)
	regMediaURL := regexp.MustCompile(`media_url\s*=\s*['|"](https:\/\/[^.]+\.bitchute\.com\/.*\.mp4)`)
	matchMediaURL := regMediaURL.FindStringSubmatch(html)
	if len(matchMediaURL) < 2 {
		return nil, errors.New("Could not extract media URL from page")
	}
	mediaURL := matchMediaURL[1]

	regVideoName := regexp.MustCompile(`(?m)video_name\s*=\s*["|']\\?"?(.*)["|'];?$`)
	matchVideoName := regVideoName.FindStringSubmatch(html)
	if len(matchVideoName) < 2 {
		return nil, errors.New("Could not extract media name from page")
	}
	videoName := strings.ReplaceAll(matchVideoName[1], `\"`, "")

	streams := make(map[string]*extractors.Stream, 1)
	size, err := request.Size(mediaURL, u)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	streams["Default"] = &extractors.Stream{
		Parts: []*extractors.Part{
			{
				URL:  mediaURL,
				Size: size,
				Ext:  "mp4",
			},
		},
		Size:    size,
		Quality: "Default",
	}

	return []*extractors.Data{
		{
			Site:    "Bitchute bitchute.com",
			Title:   videoName,
			Type:    extractors.DataTypeVideo,
			Streams: streams,
			URL:     u,
		},
	}, nil
}


================================================
FILE: extractors/bitchute/bitchute_test.go
================================================
package bitchute

import (
	"testing"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/test"
)

func TestDownload(t *testing.T) {
	tests := []struct {
		name string
		args test.Args
	}{
		{
			name: "video test 1",
			args: test.Args{
				URL:   "https://www.bitchute.com/video/C17naZ6WlWPo",
				Title: "Everybody Dance Now",
				Size:  1794720,
			},
		},
		{
			name: "video test 2",
			args: test.Args{
				URL:   "https://www.bitchute.com/video/HFgoUz6HrvQd",
				Title: "Bear Level 1",
				Size:  971698,
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			data, err := New().Extract(tt.args.URL, extractors.Options{})
			test.CheckError(t, err)
			test.Check(t, tt.args, data[0])
		})
	}
}


================================================
FILE: extractors/douyin/douyin.go
================================================
package douyin

import (
	"crypto/rand"
	_ "embed"
	"encoding/json"
	"fmt"
	"net/http"
	netURL "net/url"
	"regexp"
	"strings"

	"github.com/dop251/goja"
	"github.com/pkg/errors"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/request"
	"github.com/iawia002/lux/utils"
)

func init() {
	e := New()
	extractors.Register("douyin", e)
	extractors.Register("iesdouyin", e)
}

//go:embed sign.js
var script string

type extractor struct{}

// New returns a douyin extractor.
func New() extractors.Extractor {
	return &extractor{}
}

// Extract is the main function to extract the data.
func (e *extractor) Extract(url string, option extractors.Options) ([]*extractors.Data, error) {
	if strings.Contains(url, "v.douyin.com") {
		req, err := http.NewRequest("GET", url, nil)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		c := http.Client{
			CheckRedirect: func(req *http.Request, via []*http.Request) error {
				return http.ErrUseLastResponse
			},
		}
		resp, err := c.Do(req)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		defer resp.Body.Close() // nolint
		url = resp.Header.Get("location")
	}

	itemIds := utils.MatchOneOf(url, `/video/(\d+)`)
	if len(itemIds) == 0 {
		return nil, errors.New("unable to get video ID")
	}
	if itemIds == nil || len(itemIds) < 2 {
		return nil, errors.WithStack(extractors.ErrURLParseFailed)
	}
	itemId := itemIds[len(itemIds)-1]

	// dynamic generate cookie
	cookie, err := createCookie()
	if err != nil {
		return nil, errors.WithStack(err)
	}

	api := "https://www.douyin.com/aweme/v1/web/aweme/detail/?aweme_id=" + itemId
	// parse api query params string
	query, err := netURL.Parse(api)
	if err != nil {
		return nil, errors.WithStack(extractors.ErrURLQueryParamsParseFailed)
	}
	// define request headers and sign agent
	headers := map[string]string{}
	headers["Cookie"] = cookie
	headers["Referer"] = "https://www.douyin.com/"
	headers["User-Agent"] = "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Mobile Safari/537.36 Edg/87.0.664.66"

	// init JavaScripts runtime
	vm := goja.New()
	// load sign scripts
	_, _ = vm.RunString(script)
	// sign
	sign, err := vm.RunString(fmt.Sprintf("sign('%s', '%s')", query.RawQuery, headers["User-Agent"]))
	if err != nil {
		return nil, errors.WithStack(err)
	}
	api = fmt.Sprintf("%s&X-Bogus=%s", api, sign)

	jsonData, err := request.Get(api, url, headers)
	if err != nil {
		return nil, errors.WithStack(err)
	}
	var douyin douyinData
	if err = json.Unmarshal([]byte(jsonData), &douyin); err != nil {
		return nil, errors.WithStack(err)
	}

	urlData := make([]*extractors.Part, 0)
	var douyinType extractors.DataType
	var totalSize int64
	// AwemeType: 0:video 68:image
	if douyin.AwemeDetail.AwemeType == 68 {
		douyinType = extractors.DataTypeImage
		for _, img := range douyin.AwemeDetail.Images {
			realURL := img.URLList[len(img.URLList)-1]
			size, err := request.Size(realURL, url)
			if err != nil {
				return nil, errors.WithStack(err)
			}
			totalSize += size
			_, ext, err := utils.GetNameAndExt(realURL)
			if err != nil {
				return nil, errors.WithStack(err)
			}
			urlData = append(urlData, &extractors.Part{
				URL:  realURL,
				Size: size,
				Ext:  ext,
			})
		}
	} else {
		douyinType = extractors.DataTypeVideo
		realURL := douyin.AwemeDetail.Video.PlayAddr.URLList[0]
		totalSize, err = request.Size(realURL, url)
		if err != nil {
			return nil, errors.WithStack(err)
		}
		urlData = append(urlData, &extractors.Part{
			URL:  realURL,
			Size: totalSize,
			Ext:  "mp4",
		})
	}
	streams := map[string]*extractors.Stream{
		"default": {
			Parts: urlData,
			Size:  totalSize,
		},
	}

	return []*extractors.Data{
		{
			Site:    "抖音 douyin.com",
			Title:   douyin.AwemeDetail.Desc,
			Type:    douyinType,
			Streams: streams,
			URL:     url,
		},
	}, nil
}

func createCookie() (string, error) {
	v1, err := msToken(107)
	if err != nil {
		return "", err
	}
	v2, err := ttwid()
	if err != nil {
		return "", err
	}
	v3 := "324fb4ea4a89c0c05827e18a1ed9cf9bf8a17f7705fcc793fec935b637867e2a5a9b8168c885554d029919117a18ba69"
	v4 := "eyJiZC10aWNrZXQtZ3VhcmQtdmVyc2lvbiI6MiwiYmQtdGlja2V0LWd1YXJkLWNsaWVudC1jc3IiOiItLS0tLUJFR0lOIENFUlRJRklDQVRFIFJFUVVFU1QtLS0tLVxyXG5NSUlCRFRDQnRRSUJBREFuTVFzd0NRWURWUVFHRXdKRFRqRVlNQllHQTFVRUF3d1BZbVJmZEdsamEyVjBYMmQxXHJcbllYSmtNRmt3RXdZSEtvWkl6ajBDQVFZSUtvWkl6ajBEQVFjRFFnQUVKUDZzbjNLRlFBNUROSEcyK2F4bXAwNG5cclxud1hBSTZDU1IyZW1sVUE5QTZ4aGQzbVlPUlI4NVRLZ2tXd1FJSmp3Nyszdnc0Z2NNRG5iOTRoS3MvSjFJc3FBc1xyXG5NQ29HQ1NxR1NJYjNEUUVKRGpFZE1Cc3dHUVlEVlIwUkJCSXdFSUlPZDNkM0xtUnZkWGxwYmk1amIyMHdDZ1lJXHJcbktvWkl6ajBFQXdJRFJ3QXdSQUlnVmJkWTI0c0RYS0c0S2h3WlBmOHpxVDRBU0ROamNUb2FFRi9MQnd2QS8xSUNcclxuSURiVmZCUk1PQVB5cWJkcytld1QwSDZqdDg1czZZTVNVZEo5Z2dmOWlmeTBcclxuLS0tLS1FTkQgQ0VSVElGSUNBVEUgUkVRVUVTVC0tLS0tXHJcbiJ9"
	cookie := fmt.Sprintf("msToken=%s;ttwid=%s;odin_tt=%s;bd_ticket_guard_client_data=%s;", v1, v2, v3, v4)
	return cookie, nil
}

func msToken(length int) (string, error) {
	const characters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
	randomBytes := make([]byte, length)
	if _, err := rand.Read(randomBytes); err != nil {
		return "", err
	}
	token := make([]byte, length)
	for i, b := range randomBytes {
		token[i] = characters[int(b)%len(characters)]
	}
	return string(token), nil
}

func ttwid() (string, error) {
	body := map[string]interface{}{
		"aid":           1768,
		"union":         true,
		"needFid":       false,
		"region":        "cn",
		"cbUrlProtocol": "https",
		"service":       "www.ixigua.com",
		"migrate_info":  map[string]string{"ticket": "", "source": "node"},
	}
	bytes, err := json.Marshal(body)
	if err != nil {
		return "", err
	}
	payload := strings.NewReader(string(bytes))
	resp, err := request.Request(http.MethodPost, "https://ttwid.bytedance.com/ttwid/union/register/", payload, nil)
	if err != nil {
		return "", err
	}
	defer resp.Body.Close() // nolint
	cookie := resp.Header.Get("Set-Cookie")
	re := regexp.MustCompile(`ttwid=([^;]+)`)
	if match := re.FindStringSubmatch(cookie); match != nil {
		return match[1], nil
	}
	return "", errors.New("douyin ttwid request failed")
}


================================================
FILE: extractors/douyin/douyin_test.go
================================================
package douyin

import (
	"testing"

	"github.com/iawia002/lux/extractors"
	"github.com/iawia002/lux/test"
)

func TestDownload(t *testing.T) {
	tests := []struct {
		name string
		args test.Args
	}{
		{
			name: "normal test",
			args: test.Args{
				URL:   "https://www.douyin.com/video/6967223681286278436?previous_page=main_page&tab_name=home",
				Title: "是爱情,让父子相认#陈翔六点半  #关于爱情",
			},
		},
		{
			name: "image test",
			args: test.Args{
				URL:   "https://v.douyin.com/LvCYKvV",
				Title: "黑发限定#开春必备",
			},
		},
	}
	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			data, err := New().Extract(tt.args.URL, extractors.Options{})
			test.CheckError(t, err)
			test.Check(t, tt.args, data[0])
		})
	}
}


================================================
FILE: extractors/douyin/sign.js
================================================
var window = null;

function _0x5cd844(e) {
    var b = {
        exports: {}
    };
    return e(b, b.exports), b.exports
}

jsvmp = function(e, b, a) {
    function f(e, b, a) {
        return (f = function() {
            if ("undefined" == typeof Reflect || !Reflect.construct || Reflect.construct.sham) return !1;
            if ("function" == typeof Proxy) return !0;
            try {
                return Date.prototype.toString.call(Reflect.construct(Date, [], function() {})), !0
            } catch (e) {
                return !1
            }
        }() ? Reflect.construct : function(e, b, a) {
            var f = [null];
            f.push.apply(f, b);
            var c = new(Function.bind.apply(e, f));
            return a && function(e, b) {
                (Object.setPrototypeOf || function(e, b) {
                    return e.__proto__ = b, e
                })(e, b)
            }(c, a.prototype), c
        }).apply(null, arguments)
    }

    function c(e) {
        return function(e) {
            if (Array.isArray(e)) {
                for (var b = 0, a = new Array(e.length); b < e.length; b++) a[b] = e[b];
                return a
            }
        }(e) || function(e) {
            if (Symbol.iterator in Object(e) || "[object Arguments]" === Object.prototype.toString.call(e)) return Array.from(e)
        }(e) || function() {
            throw new TypeError("Invalid attempt to spread non-iterable instance")
        }()
    }
    for (var r = [], t = 0, d = [], i = 0, n = function(e, b) {
        var a = e[b++],
            f = e[b],
            c = parseInt("" + a + f, 16);
        if (c >> 7 == 0) return [1, c];
        if (c >> 6 == 2) {
            var r = parseInt("" + e[++b] + e[++b], 16);
            return c &= 63, [2, r = (c <<= 8) + r]
        }
        if (c >> 6 == 3) {
            var t = parseInt("" + e[++b] + e[++b], 16),
                d = parseInt("" + e[++b] + e[++b], 16);
            return c &= 63, [3, d = (c <<= 16) + (t <<= 8) + d]
        }
    }, s = function(e, b) {
        var a = parseInt("" + e[b] + e[b + 1], 16);
        return a > 127 ? -256 + a : a
    }, o = function(e, b) {
        var a = parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16);
        return a > 32767 ? -65536 + a : a
    }, l = function(e, b) {
        var a = parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3] + e[b + 4] + e[b + 5] + e[b + 6] + e[b + 7], 16);
        return a > 2147483647 ? 0 + a : a
    }, _ = function(e, b) {
        return parseInt("" + e[b] + e[b + 1], 16)
    }, x = function(e, b) {
        return parseInt("" + e[b] + e[b + 1] + e[b + 2] + e[b + 3], 16)
    }, u = u || this || window, h = (e.length, 0), p = "", y = h; y < h + 16; y++) {
        var v = "" + e[y++] + e[y];
        v = parseInt(v, 16), p += String.fromCharCode(v)
    }
    if ("HNOJ@?RC" != p) throw new Error("error magic number " + p);
    parseInt("" + e[h += 16] + e[h + 1], 16), h += 8, t = 0;
    for (var g = 0; g < 4; g++) {
        var w = h + 2 * g,
            A = parseInt("" + e[w++] + e[w], 16);
        t += (3 & A) << 2 * g
    }
    h += 16;
    var C = parseInt("" + e[h += 8] + e[h + 1] + e[h + 2] + e[h + 3] + e[h + 4] + e[h + 5] + e[h + 6] + e[h + 7], 16),
        m = C,
        S = h += 8,
        z = x(e, h += C);
    z[1], h += 4, r = {
        p: [],
        q: []
    };
    for (var B = 0; B < z; B++) {
        for (var R = n(e, h), q = h += 2 * R[0], I = r.p.length, k = 0; k < R[1]; k++) {
            var j = n(e, q);
            r.p.push(j[1]), q += 2 * j[0]
        }
        h = q, r.q.push([I, r.p.length])
    }
    var O = {
            5: 1,
            6: 1,
            70: 1,
            22: 1,
            23: 1,
            37: 1,
            73: 1
        },
        U = {
            72: 1
        },
        D = {
            74: 1
        },
        N = {
            11: 1,
            12: 1,
            24: 1,
            26: 1,
            27: 1,
            31: 1
        },
        J = {
            10: 1
        },
        L = {
            2: 1,
            29: 1,
            30: 1,
            20: 1
        },
        T = [],
        E = [];

    function M(e, b, a) {
        for (var f = b; f < b + a;) {
            var c = _(e, f);
            T[f] = c, f += 2, U[c] ? (E[f] = s(e, f), f += 2) : O[c] ? (E[f] = o(e, f), f += 4) : D[c] ? (E[f] = l(e, f), f += 8) : N[c] ? (E[f] = _(e, f), f += 2) : J[c] ? (E[f] = x(e, f), f += 4) : L[c] && (E[f] = x(e, f), f += 4)
        }
    }
    return F(e, S, m / 2, [], b, a);

    function P(e, b, a, n, h, p, y, v) {
        null == p && (p = this);
        var g, w, A, C, m = [],
            S = 0;
        y && (w = y);
        var z, B, R = b,
            q = R + 2 * a;
        if (!v)
            for (; R < q;) {
                var I = parseInt("" + e[R] + e[R + 1], 16);
                R += 2;
                var j = 3 & (z = 13 * I % 241);
                if (z >>= 2, j < 1)
                    if (j = 3 & z, z >>= 2, j < 1) {
                        if ((j = z) < 1) return [1, m[S--]];
                        j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = o(e, R), (g = function b() {
                            var a = arguments;
                            return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0)
                        }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2)
                    } else if (j < 2)(j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A));
                    else if (j < 3) {
                        if ((j = z) < 9) {
                            for (w = m[S--], B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
                            R += 4, m[S--][j] = w
                        } else if (j < 13) throw m[S--]
                    } else(j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0);
                else if (j < 2)
                    if (j = 3 & z, z >>= 2, j < 1)
                        if ((j = z) < 5) {
                            B = o(e, R);
                            try {
                                if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w
                            } catch (b) {
                                if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w
                            } finally {
                                if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w;
                                d[i] = 0, i--
                            }
                            R += 2 * B - 2
                        } else j < 7 ? (B = _(e, R), R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)))) : j < 9 && (w = m[S--], m[S] = m[S] & w);
                    else if (j < 2)
                        if ((j = z) > 12) m[++S] = s(e, R), R += 2;
                        else if (j > 10) w = m[S--], m[S] = m[S] << w;
                        else if (j > 8) {
                            for (B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
                            R += 4, m[S] = m[S][j]
                        } else j > 6 && (A = m[S--], w = delete m[S--][A]);
                    else if (j < 3)(j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w);
                    else if ((j = z) > 12) m[++S] = p;
                    else if (j > 5) w = m[S--], m[S] = m[S] !== w;
                    else if (j > 3) w = m[S--], m[S] = m[S] / w;
                    else if (j > 1) {
                        if ((B = o(e, R)) < 0) {
                            v = 1, M(e, b, 2 * a), R += 2 * B - 2;
                            break
                        }
                        R += 2 * B - 2
                    } else j > -1 && (m[S] = !m[S]);
                else if (j < 3)
                    if (j = 3 & z, z >>= 2, j < 1)(j = z) > 13 ? (m[++S] = o(e, R), R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = _(e, R), R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = x(e, R), R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w);
                    else if (j < 2)(j = z) > 12 ? (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A) : j > 3 ? (w = m[S--], m[S] = m[S] == w) : j > 1 ? (w = m[S--], m[S] = m[S] + w) : j > -1 && (m[++S] = u);
                    else if (j < 3) {
                        if ((j = z) > 13) m[++S] = !1;
                        else if (j > 6) w = m[S--], m[S] = m[S] instanceof w;
                        else if (j > 4) w = m[S--], m[S] = m[S] % w;
                        else if (j > 2)
                            if (m[S--]) R += 4;
                            else {
                                if ((B = o(e, R)) < 0) {
                                    v = 1, M(e, b, 2 * a), R += 2 * B - 2;
                                    break
                                }
                                R += 2 * B - 2
                            }
                        else if (j > 0) {
                            for (B = x(e, R), w = "", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]);
                            m[++S] = w, R += 4
                        }
                    } else(j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = _(e, R), R += 2, m[++S] = h["$" + B]) : j > 3 && (B = o(e, R), d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2);
                else if (j = 3 & z, z >>= 2, j > 2)(j = z) > 13 ? (m[++S] = l(e, R), R += 8) : j > 11 ? (w = m[S--], m[S] = m[S] >>> w) : j > 9 ? m[++S] = !0 : j > 7 ? (B = _(e, R), R += 2, m[S] = m[S][B]) : j > 0 && (w = m[S--], m[S] = m[S] < w);
                else if (j > 1)(j = z) > 10 ? (B = o(e, R), d[++i] = [
                    [R + 4, B - 3], 0, 0
                ], R += 2 * B - 2) : j > 8 ? (w = m[S--], m[S] = m[S] ^ w) : j > 6 && (w = m[S--]);
                else if (j > 0) {
                    if ((j = z) > 7) w = m[S--], m[S] = m[S] in w;
                    else if (j > 5) m[S] = ++m[S];
                    else if (j > 3) B = _(e, R), R += 2, w = h[B], m[++S] = w;
                    else if (j > 1) {
                        var O = 0,
                            U = m[S].length,
                            D = m[S];
                        m[++S] = function() {
                            var e = O < U;
                            if (e) {
                                var b = D[O++];
                                m[++S] = b
                            }
                            m[++S] = e
                        }
                    }
                } else if ((j = z) > 13) w = m[S], m[S] = m[S - 1], m[S - 1] = w;
                else if (j > 4) w = m[S--], m[S] = m[S] === w;
                else if (j > 2) w = m[S--], m[S] = m[S] - w;
                else if (j > 0) {
                    for (B = x(e, R), j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
                    j = +j, R += 4, m[++S] = j
                }
            }
        if (v)
            for (; R < q;)
                if (I = T[R], R += 2, j = 3 & (z = 13 * I % 241), z >>= 2, j > 2)
                    if (j = 3 & z, z >>= 2, j > 2)(j = z) < 2 ? (w = m[S--], m[S] = m[S] < w) : j < 9 ? (B = E[R], R += 2, m[S] = m[S][B]) : j < 11 ? m[++S] = !0 : j < 13 ? (w = m[S--], m[S] = m[S] >>> w) : j < 15 && (m[++S] = E[R], R += 8);
                    else if (j > 1)(j = z) < 6 || (j < 8 ? w = m[S--] : j < 10 ? (w = m[S--], m[S] = m[S] ^ w) : j < 12 && (B = E[R], d[++i] = [
                        [R + 4, B - 3], 0, 0
                    ], R += 2 * B - 2));
                    else if (j > 0)(j = z) > 7 ? (w = m[S--], m[S] = m[S] in w) : j > 5 ? m[S] = ++m[S] : j > 3 ? (B = E[R], R += 2, w = h[B], m[++S] = w) : j > 1 && (O = 0, U = m[S].length, D = m[S], m[++S] = function() {
                        var e = O < U;
                        if (e) {
                            var b = D[O++];
                            m[++S] = b
                        }
                        m[++S] = e
                    });
                    else if ((j = z) < 2) {
                        for (B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
                        j = +j, R += 4, m[++S] = j
                    } else j < 4 ? (w = m[S--], m[S] = m[S] - w) : j < 6 ? (w = m[S--], m[S] = m[S] === w) : j < 15 && (w = m[S], m[S] = m[S - 1], m[S - 1] = w);
                else if (j > 1)
                    if (j = 3 & z, z >>= 2, j < 1)(j = z) > 13 ? (m[++S] = E[R], R += 4) : j > 11 ? (w = m[S--], m[S] = m[S] >> w) : j > 9 ? (B = E[R], R += 2, w = m[S--], h[B] = w) : j > 7 ? (B = E[R], R += 4, A = S + 1, m[S -= B - 1] = B ? m.slice(S, A) : []) : j > 0 && (w = m[S--], m[S] = m[S] > w);
                    else if (j < 2)(j = z) < 1 ? m[++S] = u : j < 3 ? (w = m[S--], m[S] = m[S] + w) : j < 5 ? (w = m[S--], m[S] = m[S] == w) : j < 14 && (w = m[S - 1], A = m[S], m[++S] = w, m[++S] = A);
                    else if (j < 3) {
                        if ((j = z) > 13) m[++S] = !1;
                        else if (j > 6) w = m[S--], m[S] = m[S] instanceof w;
                        else if (j > 4) w = m[S--], m[S] = m[S] % w;
                        else if (j > 2) m[S--] ? R += 4 : R += 2 * (B = E[R]) - 2;
                        else if (j > 0) {
                            for (B = E[R], w = "", k = r.q[B][0]; k < r.q[B][1]; k++) w += String.fromCharCode(t ^ r.p[k]);
                            m[++S] = w, R += 4
                        }
                    } else(j = z) > 7 ? (w = m[S--], m[S] = m[S] | w) : j > 5 ? (B = E[R], R += 2, m[++S] = h["$" + B]) : j > 3 && (B = E[R], d[i][0] && !d[i][2] ? d[i][1] = [R + 4, B - 3] : d[i++] = [0, [R + 4, B - 3], 0], R += 2 * B - 2);
                else if (j > 0)
                    if (j = 3 & z, z >>= 2, j < 1) {
                        if ((j = z) > 9);
                        else if (j > 7) w = m[S--], m[S] = m[S] & w;
                        else if (j > 5) B = E[R], R += 2, m[S -= B] = 0 === B ? new m[S] : f(m[S], c(m.slice(S + 1, S + B + 1)));
                        else if (j > 3) {
                            B = E[R];
                            try {
                                if (d[i][2] = 1, 1 == (w = P(e, R + 4, B - 3, [], h, p, null, 0))[0]) return w
                            } catch (b) {
                                if (d[i] && d[i][1] && 1 == (w = P(e, d[i][1][0], d[i][1][1], [], h, p, b, 0))[0]) return w
                            } finally {
                                if (d[i] && d[i][0] && 1 == (w = P(e, d[i][0][0], d[i][0][1], [], h, p, null, 0))[0]) return w;
                                d[i] = 0, i--
                            }
                            R += 2 * B - 2
                        }
                    } else if (j < 2)
                        if ((j = z) < 8) A = m[S--], w = delete m[S--][A];
                        else if (j < 10) {
                            for (B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
                            R += 4, m[S] = m[S][j]
                        } else j < 12 ? (w = m[S--], m[S] = m[S] << w) : j < 14 && (m[++S] = E[R], R += 2);
                    else j < 3 ? (j = z) < 2 ? m[++S] = w : j < 11 ? (w = m[S -= 2][m[S + 1]] = m[S + 2], S--) : j < 13 && (w = m[S], m[++S] = w) : (j = z) > 12 ? m[++S] = p : j > 5 ? (w = m[S--], m[S] = m[S] !== w) : j > 3 ? (w = m[S--], m[S] = m[S] / w) : j > 1 ? R += 2 * (B = E[R]) - 2 : j > -1 && (m[S] = !m[S]);
                else if (j = 3 & z, z >>= 2, j < 1) {
                    if ((j = z) < 1) return [1, m[S--]];
                    j < 5 ? (w = m[S--], m[S] = m[S] * w) : j < 7 ? (w = m[S--], m[S] = m[S] != w) : j < 14 ? (A = m[S--], C = m[S--], (j = m[S--]).x === P ? j.y >= 1 ? m[++S] = F(e, j.c, j.l, A, j.z, C, null, 1) : (m[++S] = F(e, j.c, j.l, A, j.z, C, null, 0), j.y++) : m[++S] = j.apply(C, A)) : j < 16 && (B = E[R], (g = function b() {
                        var a = arguments;
                        return b.y > 0 || b.y++, F(e, b.c, b.l, a, b.z, this, null, 0)
                    }).c = R + 4, g.l = B - 2, g.x = P, g.y = 0, g.z = h, m[S] = g, R += 2 * B - 2)
                } else if (j < 2)(j = z) > 8 ? (w = m[S--], m[S] = typeof w) : j > 4 ? m[S -= 1] = m[S][m[S + 1]] : j > 2 && (A = m[S--], (j = m[S]).x === P ? j.y >= 1 ? m[S] = F(e, j.c, j.l, [A], j.z, C, null, 1) : (m[S] = F(e, j.c, j.l, [A], j.z, C, null, 0), j.y++) : m[S] = j(A));
                else if (j < 3) {
                    if ((j = z) < 9) {
                        for (w = m[S--], B = E[R], j = "", k = r.q[B][0]; k < r.q[B][1]; k++) j += String.fromCharCode(t ^ r.p[k]);
                        R += 4, m[S--][j] = w
                    } else if (j < 13) throw m[S--]
                } else(j = z) < 1 ? m[++S] = null : j < 3 ? (w = m[S--], m[S] = m[S] >= w) : j < 12 && (m[++S] = void 0);
        return [0, null]
    }

    function F(e, b, a, f, c, r, t, d) {
        null == r && (r = this), c && !c.d && (c.d = 0, c.$0 = c, c[1] = {});
        var i, n, s = {},
            o = s.d = c ? c.d + 1 : 0;
        for (s["$" + o] = s, n = 0; n < o; n++) s[i = "$" + n] = c[i];
        for (n = 0, o = s.length = f.length; n < o; n++) s[n] = f[n];
        return d && !T[b] && M(e, b, 2 * a), T[b] ? P(e, b, a, 0, s, r, null, 1)[1] : P(e, b, a, 0, s, r, null, 0)[1]
    }
};
var _0x397dc7 = "undefined" != typeof globalThis ? globalThis : void 0 !== window ? window : "undefined" != typeof global ? global : "undefined" != typeof self ? self : {},
    _0x124d1a = _0x5cd844(function(_0x770f81) {
        ! function() {
            var _0x250d36 = "input is invalid type",
                _0x4cfaee = !1,
                _0x1702f9 = {},
                _0x5ccbb3 = !_0x4cfaee && "object" == typeof self,
                _0x54d876 = !_0x1702f9.JS_MD5_NO_NODE_JS && "object" == typeof process && process.versions && process.versions.node,
                _0x185caf;
            _0x54d876 ? _0x1702f9 = _0x397dc7 : _0x5ccbb3 && (_0x1702f9 = self);
            var _0x17dcbf = !_0x1702f9.JS_MD5_NO_COMMON_JS && _0x770f81.exports,
                _0x554fed = !1,
                _0x2de28f = !_0x1702f9.JS_MD5_NO_ARRAY_BUFFER && "undefined" != typeof ArrayBuffer,
                _0x3a9a1b = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"],
                _0x465562 = [128, 32768, 8388608, -2147483648],
                _0x20b37e = [0, 8, 16, 24],
                _0x323604 = ["hex", "array", "digest", "buffer", "arrayBuffer", "base64"],
                _0x2c185e = ["A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M", "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z", "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m", "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z", "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/"],
                _0x4b59e0 = [];
            if (_0x2de28f) {
                var _0x395837 = new ArrayBuffer(68);
                _0x185caf = new Uint8Array(_0x395837), _0x4b59e0 = new Uint32Array(_0x395837)
            }!_0x1702f9.JS_MD5_NO_NODE_JS && Array.isArray || (Array.isArray = function(e) {
                return "[object Array]" === Object.prototype.toString.call(e)
            }), _0x2de28f && (_0x1702f9.JS_MD5_NO_ARRAY_BUFFER_IS_VIEW || !ArrayBuffer.isView) && (ArrayBuffer.isView = function(e) {
                return "object" == typeof e && e.buffer && e.buffer.constructor === ArrayBuffer
            });
            var _0x4e9930 = function(e) {
                    return function(b) {
                        return new _0x5887c8(!0).update(b)[e]()
                    }
                },
                _0x38ba77 = function() {
                    var e = _0x4e9930("hex");
                    _0x54d876 && (e = _0x474989(e)), e.create = function() {
                        return new _0x5887c8
                    }, e.update = function(b) {
                        return e.create().update(b)
                    };
                    for (var b = 0; b < _0x323604.length; ++b) {
                        var a = _0x323604[b];
                        e[a] = _0x4e9930(a)
                    }
                    return e
                },
                _0x474989 = function(_0x57eeaa) {
                    var _0x114910, _0x226465 = eval("require('crypto');"),
                        _0x1f6ae0 = eval("require('buffer')['Buffer'];");
                    return function(e) {
                        if ("string" == typeof e) return _0x226465.createHash("md5").update(e, "utf8").digest("hex");
                        if (null == e) throw _0x250d36;
                        return e.constructor === ArrayBuffer && (e = new Uint8Array(e)), Array.isArray(e) || ArrayBuffer.isView(e) || e.constructor === _0x1f6ae0 ? _0x226465.createHash("md5").update(new _0x1f6ae0.from(e)).digest("hex") : _0x57eeaa(e)
                    }
                };

            function _0x5887c8(e) {
                if (e) _0x4b59e0[0] = _0x4b59e0[16] = _0x4b59e0[1] = _0x4b59e0[2] = _0x4b59e0[3] = _0x4b59e0[4] = _0x4b59e0[5] = _0x4b59e0[6] = _0x4b59e0[7] = _0x4b59e0[8] = _0x4b59e0[9] = _0x4b59e0[10] = _0x4b59e0[11] = _0x4b59e0[12] = _0x4b59e0[13] = _0x4b59e0[14] = _0x4b59e0[15] = 0, this.blocks = _0x4b59e0, this.buffer8 = _0x185caf;
                else if (_0x2de28f) {
                    var b = new ArrayBuffer(68);
                    this.buffer8 = new Uint8Array(b), this.blocks = new Uint32Array(b)
                } else this.blocks = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
                this.h0 = this.h1 = this.h2 = this.h3 = this.start = this.bytes = this.hBytes = 0, this.finalized = this.hashed = !1, this.first = !0
            }
            _0x5887c8.prototype.update = function(e) {
                if (!this.finalized) {
                    var b, a = typeof e;
                    if ("string" !== a) {
                        if ("object" !== a || null === e) throw _0x250d36;
                        if (_0x2de28f && e.constructor === ArrayBuffer) e = new Uint8Array(e);
                        else if (!(Array.isArray(e) || _0x2de28f && ArrayBuffer.isView(e))) throw _0x250d36;
                        b = !0
                    }
                    for (var f, c, r = 0, t = e.length, d = this.blocks, i = this.buffer8; r < t;) {
                        if (this.hashed && (this.hashed = !1, d[0] = d[16], d[16] = d[1] = d[2] = d[3] = d[4] = d[5] = d[6] = d[7] = d[8] = d[9] = d[10] = d[11] = d[12] = d[13] = d[14] = d[15] = 0), b)
                            if (_0x2de28f)
                                for (c = this.start; r < t && c < 64; ++r) i[c++] = e[r];
                            else
                                for (c = this.start; r < t && c < 64; ++r) d[c >> 2] |= e[r] << _0x20b37e[3 & c++];
                        else if (_0x2de28f)
                            for (c = this.start; r < t && c < 64; ++r)(f = e.charCodeAt(r)) < 128 ? i[c++] = f : f < 2048 ? (i[c++] = 192 | f >> 6, i[c++] = 128 | 63 & f) : f < 55296 || f >= 57344 ? (i[c++] = 224 | f >> 12, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), i[c++] = 240 | f >> 18, i[c++] = 128 | f >> 12 & 63, i[c++] = 128 | f >> 6 & 63, i[c++] = 128 | 63 & f);
                        else
                            for (c = this.start; r < t && c < 64; ++r)(f = e.charCodeAt(r)) < 128 ? d[c >> 2] |= f << _0x20b37e[3 & c++] : f < 2048 ? (d[c >> 2] |= (192 | f >> 6) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : f < 55296 || f >= 57344 ? (d[c >> 2] |= (224 | f >> 12) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]) : (f = 65536 + ((1023 & f) << 10 | 1023 & e.charCodeAt(++r)), d[c >> 2] |= (240 | f >> 18) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 12 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | f >> 6 & 63) << _0x20b37e[3 & c++], d[c >> 2] |= (128 | 63 & f) << _0x20b37e[3 & c++]);
                        this.lastByteIndex = c, this.bytes += c - this.start, c >= 64 ? (this.start = c - 64, this.hash(), this.hashed = !0) : this.start = c
                    }
                    return this.bytes > 4294967295 && (this.hBytes += this.bytes / 4294967296 << 0, this.bytes = this.bytes % 4294967296), this
                }
            }, _0x5887c8.prototype.finalize = function() {
                if (!this.finalized) {
                    this.finalized = !0;
                    var e = this.blocks,
                        b = this.lastByteIndex;
                    e[b >> 2] |= _0x465562[3 & b], b >= 56 && (this.hashed || this.hash(), e[0] = e[16], e[16] = e[1] = e[2] = e[3] = e[4] = e[5] = e[6] = e[7] = e[8] = e[9] = e[10] = e[11] = e[12] = e[13] = e[14] = e[15] = 0), e[14] = this.bytes << 3, e[15] = this.hBytes << 3 | this.bytes >>> 29, this.hash()
                }
            }, _0x5887c8.prototype.hash = function() {
                var e, b, a, f, c, r, t = this.blocks;
                this.first ? b = ((b = ((e = ((e = t[0] - 680876937) << 7 | e >>> 25) - 271733879 << 0) ^ (a = ((a = (-271733879 ^ (f = ((f = (-1732584194 ^ 2004318071 & e) + t[1] - 117830708) << 12 | f >>> 20) + e << 0) & (-271733879 ^ e)) + t[2] - 1126478375) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1316259209) << 22 | b >>> 10) + a << 0 : (e = this.h0, b = this.h1, a = this.h2, b = ((b += ((e = ((e += ((f = this.h3) ^ b & (a ^ f)) + t[0] - 680876936) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[1] - 389564586) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[2] + 606105819) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[3] - 1044525330) << 22 | b >>> 10) + a << 0), b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[4] - 176418897) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[5] + 1200080426) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[6] - 1473231341) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[7] - 45705983) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[8] + 1770035416) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[9] - 1958414417) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[10] - 42063) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[11] - 1990404162) << 22 | b >>> 10) + a << 0, b = ((b += ((e = ((e += (f ^ b & (a ^ f)) + t[12] + 1804603682) << 7 | e >>> 25) + b << 0) ^ (a = ((a += (b ^ (f = ((f += (a ^ e & (b ^ a)) + t[13] - 40341101) << 12 | f >>> 20) + e << 0) & (e ^ b)) + t[14] - 1502002290) << 17 | a >>> 15) + f << 0) & (f ^ e)) + t[15] + 1236535329) << 22 | b >>> 10) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[1] - 165796510) << 5 | e >>> 27) + b << 0) ^ b)) + t[6] - 1069501632) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[11] + 643717713) << 14 | a >>> 18) + f << 0) ^ f)) + t[0] - 373897302) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[5] - 701558691) << 5 | e >>> 27) + b << 0) ^ b)) + t[10] + 38016083) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[15] - 660478335) << 14 | a >>> 18) + f << 0) ^ f)) + t[4] - 405537848) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[9] + 568446438) << 5 | e >>> 27) + b << 0) ^ b)) + t[14] - 1019803690) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[3] - 187363961) << 14 | a >>> 18) + f << 0) ^ f)) + t[8] + 1163531501) << 20 | b >>> 12) + a << 0, b = ((b += ((f = ((f += (b ^ a & ((e = ((e += (a ^ f & (b ^ a)) + t[13] - 1444681467) << 5 | e >>> 27) + b << 0) ^ b)) + t[2] - 51403784) << 9 | f >>> 23) + e << 0) ^ e & ((a = ((a += (e ^ b & (f ^ e)) + t[7] + 1735328473) << 14 | a >>> 18) + f << 0) ^ f)) + t[12] - 1926607734) << 20 | b >>> 12) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[5] - 378558) << 4 | e >>> 28) + b << 0)) + t[8] - 2022574463) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[11] + 1839030562) << 16 | a >>> 16) + f << 0)) + t[14] - 35309556) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[1] - 1530992060) << 4 | e >>> 28) + b << 0)) + t[4] + 1272893353) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[7] - 155497632) << 16 | a >>> 16) + f << 0)) + t[10] - 1094730640) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[13] + 681279174) << 4 | e >>> 28) + b << 0)) + t[0] - 358537222) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[3] - 722521979) << 16 | a >>> 16) + f << 0)) + t[6] + 76029189) << 23 | b >>> 9) + a << 0, b = ((b += ((r = (f = ((f += ((c = b ^ a) ^ (e = ((e += (c ^ f) + t[9] - 640364487) << 4 | e >>> 28) + b << 0)) + t[12] - 421815835) << 11 | f >>> 21) + e << 0) ^ e) ^ (a = ((a += (r ^ b) + t[15] + 530742520) << 16 | a >>> 16) + f << 0)) + t[2] - 995338651) << 23 | b >>> 9) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[0] - 198630844) << 6 | e >>> 26) + b << 0) | ~a)) + t[7] + 1126891415) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[14] - 1416354905) << 15 | a >>> 17) + f << 0) | ~e)) + t[5] - 57434055) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[12] + 1700485571) << 6 | e >>> 26) + b << 0) | ~a)) + t[3] - 1894986606) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[10] - 1051523) << 15 | a >>> 17) + f << 0) | ~e)) + t[1] - 2054922799) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[8] + 1873313359) << 6 | e >>> 26) + b << 0) | ~a)) + t[15] - 30611744) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[6] - 1560198380) << 15 | a >>> 17) + f << 0) | ~e)) + t[13] + 1309151649) << 21 | b >>> 11) + a << 0, b = ((b += ((f = ((f += (b ^ ((e = ((e += (a ^ (b | ~f)) + t[4] - 145523070) << 6 | e >>> 26) + b << 0) | ~a)) + t[11] - 1120210379) << 10 | f >>> 22) + e << 0) ^ ((a = ((a += (e ^ (f | ~b)) + t[2] + 718787259) << 15 | a >>> 17) + f << 0) | ~e)) + t[9] - 343485551) << 21 | b >>> 11) + a << 0, this.first ? (this.h0 = e + 1732584193 << 0, this.h1 = b - 271733879 << 0, this.h2 = a - 1732584194 << 0, this.h3 = f + 271733878 << 0, this.first = !1) : (this.h0 = this.h0 + e << 0, this.h1 = this.h1 + b << 0, this.h2 = this.h2 + a << 0, this.h3 = this.h3 + f << 0)
            }, _0x5887c8.prototype.hex = function() {
                this.finalize();
                var e = this.h0,
                    b = this.h1,
                    a = this.h2,
                    f = this.h3;
                return _0x3a9a1b[e >> 4 & 15] + _0x3a9a1b[15 & e] + _0x3a9a1b[e >> 12 & 15] + _0x3a9a1b[e >> 8 & 15] + _0x3a9a1b[e >> 20 & 15] + _0x3a9a1b[e >> 16 & 15] + _0x3a9a1b[e >> 28 & 15] + _0x3a9a1b[e >> 24 & 15] + _0x3a9a1b[b >> 4 & 15] + _0x3a9a1b[15 & b] + _0x3a9a1b[b >> 12 & 15] + _0x3a9a1b[b >> 8 & 15] + _0x3a9a1b[b >> 20 & 15] + _0x3a9a1b[b >> 16 & 15] + _0x3a9a1b[b >> 28 & 15] + _0x3a9a1b[b >> 24 & 15] + _0x3a9a1b[a >> 4 & 15] + _0x3a9a1b[15 & a] + _0x3a9a1b[a >> 12 & 15] + _0x3a9a1b[a >> 8 & 15] + _0x3a9a1b[a >> 20 & 15] + _0x3a9a1b[a >> 16 & 15] + _0x3a9a1b[a >> 28 & 15] + _0x3a9a1b[a >> 24 & 15] + _0x3a9a1b[f >> 4 & 15] + _0x3a9a1b[15 & f] + _0x3a9a1b[f >> 12 & 15] + _0x3a9a1b[f >> 8 & 15] + _0x3a9a1b[f >> 20 & 15] + _0x3a9a1b[f >> 16 & 15] + _0x3a9a1b[f >> 28 & 15] + _0x3a9a1b[f >> 24 & 15]
            }, _0x5887c8.prototype.toString = _0x5887c8.prototype.hex, _0x5887c8.prototype.digest = function() {
                this.finalize();
                var e = this.h0,
                    b = this.h1,
                    a = this.h2,
                    f = this.h3;
                return [255 & e, e >> 8 & 255, e >> 16 & 255, e >> 24 & 255, 255 & b, b >> 8 & 255, b >> 16 & 255, b >> 24 & 255, 255 & a, a >> 8 & 255, a >> 16 & 255, a >> 24 & 255, 255 & f, f >> 8 & 255, f >> 16 & 255, f >> 24 & 255]
            }, _0x5887c8.prototype.array = _0x5887c8.prototype.digest, _0x5887c8.prototype.arrayBuffer = function() {
                this.finalize();
                var e = new ArrayBuffer(16),
                    b = new Uint32Array(e);
                return b[0] = this.h0, b[1] = this.h1, b[2] = this.h2, b[3] = this.h3, e
            }, _0x5887c8.prototype.buffer = _0x5887c8.prototype.arrayBuffer, _0x5887c8.prototype.base64 = function() {
                for (var e, b, a, f = "", c = this.array(), r = 0; r < 15;) e = c[r++], b = c[r++], a = c[r++], f += _0x2c185e[e >>> 2] + _0x2c185e[63 & (e << 4 | b >>> 4)] + _0x2c185e[63 & (b << 2 | a >>> 6)] + _0x2c185e[63 & a];
                return f + (_0x2c185e[(e = c[r]) >>> 2] + _0x2c185e[e << 4 & 63] + "==")
            };
            var _0x4dd781 = _0x38ba77();
            _0x17dcbf ? _0x770f81.exports = _0x4dd781 : (_0x1702f9.md5 = _0x4dd781, _0x554fed && (void 0)(function() {
                return _0x4dd781
            }))
        }()
    });

function _0x178cef(e) {
    return jsvmp("484e4f4a403f52430038001eab0015840e8ee21a00000000000000621b000200001d000146000306000e271f001b000200021d00010500121b001b000b021b000b04041d0001071b000b0500000003000126207575757575757575757575757575757575757575757575757575757575757575", [, , void 0 !== _0x124d1a ? _0x124d1a : void 0, _0x178cef, e])
}
for (var _0xb55f3e = {
    boe: !1,
    aid: 0,
    dfp: !1,
    sdi: !1,
    enablePathList: [],
    _enablePathListRegex: [],
    urlRewriteRules: [],
    _urlRewriteRules: [],
    initialized: !1,
    enableTrack: !1,
    track: {
        unitTime: 0,
        unitAmount: 0,
        fre: 0
    },
    triggerUnload: !1,
    region: "",
    regionConf: {},
    umode: 0,
    v: !1,
    perf: !1,
    xxbg: !0
}, _0x3eaf64 = {
    debug: function(e, b) {
        let a = !1;
        a = !1
    }
}, _0x233455 = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"], _0x2e9f6d = [], _0x511f86 = [], _0x3d35de = 0; _0x3d35de < 256; _0x3d35de++) _0x2e9f6d[_0x3d35de] = _0x233455[_0x3d35de >> 4 & 15] + _0x233455[15 & _0x3d35de], _0x3d35de < 16 && (_0x3d35de < 10 ? _0x511f86[48 + _0x3d35de] = _0x3d35de : _0x511f86[87 + _0x3d35de] = _0x3d35de);
var _0x2ce54d = function(e) {
        for (var b = e.length, a = "", f = 0; f < b;) a += _0x2e9f6d[e[f++]];
        return a
    },
    _0x5960a2 = function(e) {
        for (var b = e.length >> 1, a = b << 1, f = new Uint8Array(b), c = 0, r = 0; r < a;) f[c++] = _0x511f86[e.charCodeAt(r++)] << 4 | _0x511f86[e.charCodeAt(r++)];
        return f
    },
    _0x4e46b6 = {
        encode: _0x2ce54d,
        decode: _0x5960a2
    };

function sign(e, b) {
    return jsvmp("484e4f4a403f5243001f240fbf2031ccf317480300000000000007181b0002012e1d00921b000b171b000b02402217000a1c1b000b1726402217000c1c1b000b170200004017002646000306000e271f001b000200021d00920500121b001b000b031b000b17041d0092071b000b041e012f17000d1b000b05260a0000101c1b000b06260a0000101c1b001b000b071e01301d00931b001b000b081e00081d00941b0048021d00951b001b000b1b1d00961b0048401d009e1b001b000b031b000b16041d009f1b001b000b09221e0131241b000b031b000b09221e0131241b000b1e0a000110040a0001101d00d51b001b000b09221e0131241b000b031b000b09221e0131241b000b180a000110040a0001101d00d71b001b000b0a1e00101d00d91b001b000b0b261b0
Download .txt
gitextract_1wxrt7hr/

├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── can-not-download-video.md
│   │   └── support-a-new-website.md
│   └── workflows/
│       ├── builder.yml
│       ├── ci.yml
│       ├── goreleaser.yml
│       ├── stream_acfun.yml
│       ├── stream_bcy.yml
│       ├── stream_bilibili.yml
│       ├── stream_bitchute.yml
│       ├── stream_douyin.yml
│       ├── stream_douyu.yml
│       ├── stream_eporner.yml
│       ├── stream_facebook.yml
│       ├── stream_geekbang.yml
│       ├── stream_haokan.yml
│       ├── stream_hupu.yml
│       ├── stream_huya.yml
│       ├── stream_instagram.yml
│       ├── stream_iqiyi.yml
│       ├── stream_ixigua.yml
│       ├── stream_kuaishou.yml
│       ├── stream_mgtv.yml
│       ├── stream_miaopai.yml
│       ├── stream_netease.yml
│       ├── stream_odysee.yml
│       ├── stream_pinterest.yml
│       ├── stream_pixivision.yml
│       ├── stream_pornhub.yml
│       ├── stream_qq.yml
│       ├── stream_reddit.yml
│       ├── stream_rumble.yml
│       ├── stream_streamtape.yml
│       ├── stream_tangdou.yml
│       ├── stream_threads.yml
│       ├── stream_tiktok.yml
│       ├── stream_tumblr.yml
│       ├── stream_twitter.yml
│       ├── stream_udn.yml
│       ├── stream_vimeo.yml
│       ├── stream_vk.yml
│       ├── stream_weibo.yml
│       ├── stream_xiaohongshu.yml
│       ├── stream_ximalaya.yml
│       ├── stream_xinpianchang.yml
│       ├── stream_xvideos.yml
│       ├── stream_yinyuetai.yml
│       ├── stream_youku.yml
│       ├── stream_youtube.yml
│       ├── stream_zhihu.yml
│       └── stream_zingmp3.yml
├── .gitignore
├── .golangci.yml
├── .goreleaser.yml
├── CONTRIBUTING.md
├── Cask.toml
├── LICENSE
├── README.md
├── app/
│   ├── app.go
│   └── register.go
├── codecov.yml
├── config/
│   └── config.go
├── downloader/
│   ├── downloader.go
│   ├── downloader_test.go
│   ├── types.go
│   └── utils.go
├── extractors/
│   ├── acfun/
│   │   ├── acfun.go
│   │   ├── acfun_test.go
│   │   └── types.go
│   ├── bcy/
│   │   ├── bcy.go
│   │   └── bcy_test.go
│   ├── bilibili/
│   │   ├── bilibili.go
│   │   ├── bilibili_test.go
│   │   └── types.go
│   ├── bitchute/
│   │   ├── bitchute.go
│   │   └── bitchute_test.go
│   ├── douyin/
│   │   ├── douyin.go
│   │   ├── douyin_test.go
│   │   ├── sign.js
│   │   └── types.go
│   ├── douyu/
│   │   ├── douyu.go
│   │   └── douyu_test.go
│   ├── eporner/
│   │   ├── eporner.go
│   │   └── eporner_test.go
│   ├── errors.go
│   ├── extractors.go
│   ├── facebook/
│   │   ├── facebook.go
│   │   └── facebook_test.go
│   ├── geekbang/
│   │   ├── geekbang.go
│   │   └── geekbang_test.go
│   ├── haokan/
│   │   ├── haokan.go
│   │   └── haokan_test.go
│   ├── hupu/
│   │   ├── hupu.go
│   │   └── hupu_test.go
│   ├── huya/
│   │   ├── huya.go
│   │   └── huya_test.go
│   ├── instagram/
│   │   ├── instagram.go
│   │   └── instagram_test.go
│   ├── iqiyi/
│   │   ├── iqiyi.go
│   │   └── iqiyi_test.go
│   ├── ixigua/
│   │   ├── ixigua.go
│   │   ├── ixigua_test.go
│   │   └── types.go
│   ├── kuaishou/
│   │   ├── kuaishou.go
│   │   └── kuaishou_test.go
│   ├── mgtv/
│   │   ├── mgtv.go
│   │   └── mgtv_test.go
│   ├── miaopai/
│   │   ├── miaopai.go
│   │   └── miaopai_test.go
│   ├── netease/
│   │   ├── netease.go
│   │   └── netease_test.go
│   ├── odysee/
│   │   ├── odysee.go
│   │   └── odysee_test.go
│   ├── pinterest/
│   │   ├── pinterest.go
│   │   └── pinterest_test.go
│   ├── pixivision/
│   │   ├── pixivision.go
│   │   └── pixivision_test.go
│   ├── pornhub/
│   │   ├── pornhub.go
│   │   └── pornhub_test.go
│   ├── qq/
│   │   ├── qq.go
│   │   └── qq_test.go
│   ├── reddit/
│   │   ├── reddit.go
│   │   └── reddit_test.go
│   ├── rumble/
│   │   ├── rumble.go
│   │   └── rumble_test.go
│   ├── streamtape/
│   │   ├── streamtape.go
│   │   └── streamtape_test.go
│   ├── tangdou/
│   │   ├── tangdou.go
│   │   └── tangdou_test.go
│   ├── threads/
│   │   ├── threads.go
│   │   └── threads_test.go
│   ├── tiktok/
│   │   ├── tiktok.go
│   │   └── tiktok_test.go
│   ├── tumblr/
│   │   ├── tumblr.go
│   │   └── tumblr_test.go
│   ├── twitter/
│   │   ├── twitter.go
│   │   └── twitter_test.go
│   ├── types.go
│   ├── udn/
│   │   ├── udn.go
│   │   └── udn_test.go
│   ├── universal/
│   │   ├── universal.go
│   │   └── universal_test.go
│   ├── vimeo/
│   │   ├── vimeo.go
│   │   └── vimeo_test.go
│   ├── vk/
│   │   ├── vk.go
│   │   └── vk_test.go
│   ├── weibo/
│   │   ├── weibo.go
│   │   └── weibo_test.go
│   ├── xiaohongshu/
│   │   ├── xiaohongshu.go
│   │   └── xiaohongshu_test.go
│   ├── ximalaya/
│   │   ├── types.go
│   │   ├── ximalaya.go
│   │   └── ximalaya_test.go
│   ├── xinpianchang/
│   │   ├── xinpianchang.go
│   │   └── xinpianchang_test.go
│   ├── xvideos/
│   │   ├── xvideos.go
│   │   └── xvideos_test.go
│   ├── yinyuetai/
│   │   ├── types.go
│   │   ├── yinyuetai.go
│   │   └── yinyuetai_test.go
│   ├── youku/
│   │   ├── youku.go
│   │   └── youku_test.go
│   ├── youtube/
│   │   ├── youtube.go
│   │   └── youtube_test.go
│   ├── zhihu/
│   │   ├── types.go
│   │   ├── zhihu.go
│   │   └── zhihu_test.go
│   └── zingmp3/
│       ├── zingmp3.go
│       └── zingmp3_test.go
├── go.mod
├── go.sum
├── main.go
├── parser/
│   ├── parser.go
│   └── parser_test.go
├── request/
│   ├── request.go
│   └── request_test.go
├── script/
│   ├── generate_github_action_template.js
│   └── github_action_template.yml
├── test/
│   └── utils.go
└── utils/
    ├── download.go
    ├── download_test.go
    ├── ffmpeg.go
    ├── pool.go
    ├── pool_test.go
    ├── utils.go
    └── utils_test.go
Download .txt
SYMBOL INDEX (554 symbols across 121 files)

FILE: app/app.go
  constant Name (line 21) | Name = "lux"
  function init (line 26) | func init() {
  function New (line 40) | func New() *cli.App {
  function download (line 279) | func download(c *cli.Context, videoURL string) error {

FILE: downloader/downloader.go
  type Options (line 28) | type Options struct
  type Downloader (line 52) | type Downloader struct
    method caption (line 78) | func (downloader *Downloader) caption(url, fileName, ext string, trans...
    method writeFile (line 111) | func (downloader *Downloader) writeFile(url string, file *os.File, hea...
    method save (line 128) | func (downloader *Downloader) save(part *extractors.Part, refer, fileN...
    method multiThreadSave (line 226) | func (downloader *Downloader) multiThreadSave(dataPart *extractors.Par...
    method aria2 (line 510) | func (downloader *Downloader) aria2(title string, stream *extractors.S...
    method Download (line 552) | func (downloader *Downloader) Download(data *extractors.Data) error {
  constant DOWNLOAD_FILE_EXT (line 58) | DOWNLOAD_FILE_EXT = ".download"
  function progressBar (line 61) | func progressBar(size int64) *pb.ProgressBar {
  function New (line 70) | func New(option Options) *Downloader {
  function filePartPath (line 407) | func filePartPath(filepath string, part *FilePartMeta) string {
  function computeEnd (line 411) | func computeEnd(s, chunkSize, max int64) int64 {
  function readDirAllFilePart (line 420) | func readDirAllFilePart(filePath, filename, extname string) ([]*FilePart...
  function parseFilePartMeta (line 448) | func parseFilePartMeta(filepath string, fileSize int64) (*FilePartMeta, ...
  function writeFilePartMeta (line 473) | func writeFilePartMeta(file *os.File, meta *FilePartMeta) error {
  function mergeMultiPart (line 477) | func mergeMultiPart(filepath string, parts []*FilePartMeta) error {

FILE: downloader/downloader_test.go
  function TestDownload (line 9) | func TestDownload(t *testing.T) {

FILE: downloader/types.go
  type Aria2RPCData (line 4) | type Aria2RPCData struct
  type Aria2Input (line 17) | type Aria2Input struct
  type FilePartMeta (line 25) | type FilePartMeta struct

FILE: downloader/utils.go
  function genSortedStreams (line 17) | func genSortedStreams(streams map[string]*extractors.Stream) []*extracto...
  function printHeader (line 30) | func printHeader(data *extractors.Data) {
  function printStream (line 40) | func printStream(stream *extractors.Stream) {
  function printInfo (line 52) | func printInfo(data *extractors.Data, sortedStreams []*extractors.Stream) {
  function printStreamInfo (line 78) | func printStreamInfo(data *extractors.Data, stream *extractors.Stream) {

FILE: extractors/acfun/acfun.go
  function init (line 17) | func init() {
  constant bangumiDataPattern (line 22) | bangumiDataPattern = "window.pageInfo = window.bangumiData = (.*);"
  constant bangumiListPattern (line 23) | bangumiListPattern = "window.bangumiList = (.*);"
  constant bangumiHTMLURL (line 25) | bangumiHTMLURL = "https://www.acfun.cn/bangumi/aa%d_36188_%d"
  constant referer (line 27) | referer = "https://www.acfun.cn"
  type extractor (line 30) | type extractor struct
    method Extract (line 38) | func (e *extractor) Extract(URL string, option extractors.Options) ([]...
  function New (line 33) | func New() extractors.Extractor {
  function concatURL (line 79) | func concatURL(epData *episodeData) string {
  function extractBangumi (line 83) | func extractBangumi(URL string) *extractors.Data {
  function resolvingData (line 146) | func resolvingData(html []byte) (*bangumiData, *videoInfo, error) {
  function resolvingEpisodes (line 165) | func resolvingEpisodes(html []byte) (*episodeList, error) {

FILE: extractors/acfun/acfun_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/acfun/types.go
  type episodeData (line 3) | type episodeData struct
  type bangumiData (line 10) | type bangumiData struct
  type videoInfo (line 18) | type videoInfo struct
  type streams (line 24) | type streams
  type episodeList (line 26) | type episodeList struct
  type stream (line 30) | type stream struct

FILE: extractors/bcy/bcy.go
  function init (line 15) | func init() {
  type bcyData (line 19) | type bcyData struct
  type extractor (line 29) | type extractor struct
    method Extract (line 37) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 32) | func New() extractors.Extractor {

FILE: extractors/bcy/bcy_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/bilibili/bilibili.go
  function init (line 20) | func init() {
  constant bilibiliAPI (line 27) | bilibiliAPI        = "https://api.bilibili.com/x/player/playurl?"
  constant bilibiliBangumiAPI (line 28) | bilibiliBangumiAPI = "https://api.bilibili.com/pgc/player/web/playurl?"
  constant bilibiliTokenAPI (line 29) | bilibiliTokenAPI   = "https://api.bilibili.com/x/player/playurl/token?"
  constant referer (line 32) | referer = "https://www.bilibili.com"
  function genAPI (line 36) | func genAPI(aid, cid, quality int, bvid string, bangumi bool, cookie str...
  type bilibiliOptions (line 86) | type bilibiliOptions struct
  function extractBangumi (line 97) | func extractBangumi(url, html string, extractOption extractors.Options) ...
  function getMultiPageData (line 184) | func getMultiPageData(html string) (*multiPage, error) {
  function extractFestival (line 199) | func extractFestival(url, html string, extractOption extractors.Options)...
  function extractNormalVideo (line 226) | func extractNormalVideo(url, html string, extractOption extractors.Optio...
  function multiEpisodeDownload (line 278) | func multiEpisodeDownload(url, html string, extractOption extractors.Opt...
  function multiPageDownload (line 307) | func multiPageDownload(url, html string, extractOption extractors.Option...
  type extractor (line 336) | type extractor struct
    method Extract (line 344) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 339) | func New() extractors.Extractor {
  function bilibiliDownload (line 366) | func bilibiliDownload(options bilibiliOptions, extractOption extractors....
  function getExtFromMimeType (line 518) | func getExtFromMimeType(mimeType string) string {
  function getSubTitleCaptionPart (line 526) | func getSubTitleCaptionPart(aid int, cid int) *extractors.CaptionPart {
  function subtitleTransform (line 547) | func subtitleTransform(body []byte) ([]byte, error) {

FILE: extractors/bilibili/bilibili_test.go
  function TestBilibili (line 10) | func TestBilibili(t *testing.T) {

FILE: extractors/bilibili/types.go
  type tokenData (line 5) | type tokenData struct
  type token (line 9) | type token struct
  type Interaction (line 15) | type Interaction struct
  type EpVideoInfo (line 19) | type EpVideoInfo struct
  type bangumiData (line 32) | type bangumiData struct
  type videoPagesData (line 37) | type videoPagesData struct
  type multiPageVideoData (line 43) | type multiPageVideoData struct
  type episode (line 48) | type episode struct
  type multiEpisodeData (line 55) | type multiEpisodeData struct
  type multiPage (line 60) | type multiPage struct
  type dashStream (line 67) | type dashStream struct
  type dashStreams (line 76) | type dashStreams struct
  type dashInfo (line 81) | type dashInfo struct
  type dURL (line 90) | type dURL struct
  type dash (line 95) | type dash struct
  type subtitleData (line 116) | type subtitleData struct
  type bilibiliSubtitleFormat (line 123) | type bilibiliSubtitleFormat struct
  type subtitleProperty (line 132) | type subtitleProperty struct
  type subtitleInfo (line 139) | type subtitleInfo struct
  type bilibiliWebInterfaceData (line 144) | type bilibiliWebInterfaceData struct
  type bilibiliWebInterface (line 149) | type bilibiliWebInterface struct
  type festival (line 154) | type festival struct

FILE: extractors/bitchute/bitchute.go
  function init (line 18) | func init() {
  type extractor (line 22) | type extractor struct
    method Extract (line 30) | func (e *extractor) Extract(u string, option extractors.Options) ([]*e...
  function New (line 25) | func New() extractors.Extractor {

FILE: extractors/bitchute/bitchute_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/douyin/douyin.go
  function init (line 21) | func init() {
  type extractor (line 30) | type extractor struct
    method Extract (line 38) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 33) | func New() extractors.Extractor {
  function createCookie (line 158) | func createCookie() (string, error) {
  function msToken (line 173) | func msToken(length int) (string, error) {
  function ttwid (line 186) | func ttwid() (string, error) {

FILE: extractors/douyin/douyin_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/douyin/sign.js
  function _0x5cd844 (line 3) | function _0x5cd844(e) {
  function f (line 11) | function f(e, b, a) {
  function c (line 32) | function c(e) {
  function M (line 133) | function M(e, b, a) {
  function P (line 141) | function P(e, b, a, n, h, p, y, v) {
  function F (line 321) | function F(e, b, a, f, c, r, t, d) {
  function _0x5887c8 (line 385) | function _0x5887c8(e) {
  function _0x178cef (line 456) | function _0x178cef(e) {
  function sign (line 501) | function sign(e, b) {

FILE: extractors/douyin/types.go
  type douyinData (line 3) | type douyinData struct

FILE: extractors/douyu/douyu.go
  function init (line 13) | func init() {
  type douyuData (line 17) | type douyuData struct
  type douyuURLInfo (line 24) | type douyuURLInfo struct
  function douyuM3u8 (line 29) | func douyuM3u8(url string) ([]douyuURLInfo, int64, error) {
  type extractor (line 55) | type extractor struct
    method Extract (line 63) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 58) | func New() extractors.Extractor {

FILE: extractors/douyu/douyu_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/eporner/eporner.go
  function init (line 17) | func init() {
  constant downloadclass (line 22) | downloadclass = ".dloaddivcol"
  type src (line 25) | type src struct
  function getSrcMeta (line 32) | func getSrcMeta(text string) *src {
  function getSrc (line 69) | func getSrc(html string) []*src {
  type extractor (line 99) | type extractor struct
    method Extract (line 107) | func (e *extractor) Extract(u string, option extractors.Options) ([]*e...
  function New (line 102) | func New() extractors.Extractor {

FILE: extractors/eporner/eporner_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/extractors.go
  function Register (line 17) | func Register(domain string, e Extractor) {
  function Extract (line 24) | func Extract(u string, option Options) ([]*Data, error) {

FILE: extractors/facebook/facebook.go
  function init (line 14) | func init() {
  type extractor (line 18) | type extractor struct
    method Extract (line 26) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 21) | func New() extractors.Extractor {

FILE: extractors/facebook/facebook_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/geekbang/geekbang.go
  function init (line 16) | func init() {
  type geekData (line 20) | type geekData struct
  type videoPlayAuth (line 30) | type videoPlayAuth struct
  type playInfo (line 38) | type playInfo struct
  type geekURLInfo (line 53) | type geekURLInfo struct
  function geekM3u8 (line 58) | func geekM3u8(url string) ([]geekURLInfo, error) {
  type extractor (line 79) | type extractor struct
    method Extract (line 87) | func (e *extractor) Extract(url string, _ extractors.Options) ([]*extr...
  function New (line 82) | func New() extractors.Extractor {

FILE: extractors/geekbang/geekbang_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/haokan/haokan.go
  function init (line 13) | func init() {
  type extractor (line 17) | type extractor struct
    method Extract (line 25) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 20) | func New() extractors.Extractor {

FILE: extractors/haokan/haokan_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/hupu/hupu.go
  function init (line 11) | func init() {
  type extractor (line 15) | type extractor struct
    method Extract (line 22) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 18) | func New() extractors.Extractor {

FILE: extractors/hupu/hupu_test.go
  function TestHupu (line 10) | func TestHupu(t *testing.T) {

FILE: extractors/huya/huya.go
  function init (line 11) | func init() {
  type extractor (line 15) | type extractor struct
    method Extract (line 24) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  constant huyaVideoHost (line 17) | huyaVideoHost = "https://videotx-platform.cdn.huya.com/"
  function New (line 20) | func New() extractors.Extractor {

FILE: extractors/huya/huya_test.go
  function TestHuya (line 10) | func TestHuya(t *testing.T) {

FILE: extractors/instagram/instagram.go
  function init (line 24) | func init() {
  type sliderItemNode (line 38) | type sliderItemNode struct
    method extractMediaURL (line 45) | func (s sliderItemNode) extractMediaURL() string {
  type instagramPayload (line 53) | type instagramPayload struct
    method isEmpty (line 64) | func (s instagramPayload) isEmpty() bool {
  function getPostWithCode (line 68) | func getPostWithCode(code string) ([]string, error) {
  function extractShortCodeFromLink (line 129) | func extractShortCodeFromLink(link string) (string, error) {
  type extractor (line 138) | type extractor struct
    method Extract (line 146) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 141) | func New() extractors.Extractor {

FILE: extractors/instagram/instagram_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/iqiyi/iqiyi.go
  function init (line 19) | func init() {
  type iqiyi (line 24) | type iqiyi struct
  type iqiyiURL (line 45) | type iqiyiURL struct
  type SiteType (line 50) | type SiteType
  constant SiteTypeIQ (line 54) | SiteTypeIQ SiteType = iota
  constant SiteTypeIqiyi (line 56) | SiteTypeIqiyi
  constant iqReferer (line 57) | iqReferer    = "https://www.iq.com"
  constant iqiyiReferer (line 58) | iqiyiReferer = "https://www.iqiyi.com"
  function getMacID (line 61) | func getMacID() string {
  function getVF (line 74) | func getVF(params string) string {
  function getVPS (line 93) | func getVPS(tvid, vid, refer string) (*iqiyi, error) {
  type extractor (line 113) | type extractor struct
    method Extract (line 125) | func (e *extractor) Extract(url string, _ extractors.Options) ([]*extr...
  function New (line 118) | func New(siteType SiteType) extractors.Extractor {

FILE: extractors/iqiyi/iqiyi_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/ixigua/ixigua.go
  function init (line 19) | func init() {
  type extractor (line 24) | type extractor struct
    method Extract (line 42) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  type Video (line 26) | type Video struct
  function New (line 37) | func New() extractors.Extractor {
  function base64Decode (line 122) | func base64Decode(t string) string {

FILE: extractors/ixigua/ixigua_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/ixigua/types.go
  type xiguanData (line 3) | type xiguanData struct

FILE: extractors/kuaishou/kuaishou.go
  function init (line 15) | func init() {
  type extractor (line 19) | type extractor struct
    method Extract (line 46) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 22) | func New() extractors.Extractor {
  function fetchCookies (line 27) | func fetchCookies(url string, headers map[string]string) (string, error) {

FILE: extractors/kuaishou/kuaishou_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/mgtv/mgtv.go
  function init (line 19) | func init() {
  type mgtvVideoStream (line 23) | type mgtvVideoStream struct
  type mgtvVideoInfo (line 29) | type mgtvVideoInfo struct
  type mgtvVideoData (line 34) | type mgtvVideoData struct
  type mgtv (line 40) | type mgtv struct
  type mgtvVideoAddr (line 44) | type mgtvVideoAddr struct
  type mgtvURLInfo (line 48) | type mgtvURLInfo struct
  type mgtvPm2Data (line 53) | type mgtvPm2Data struct
  function mgtvM3u8 (line 62) | func mgtvM3u8(url string) ([]mgtvURLInfo, int64, error) {
  function encodeTk2 (line 91) | func encodeTk2(str string) string {
  type extractor (line 103) | type extractor struct
    method Extract (line 111) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 106) | func New() extractors.Extractor {

FILE: extractors/mgtv/mgtv_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/miaopai/miaopai.go
  function init (line 16) | func init() {
  type miaopaiData (line 20) | type miaopaiData struct
  function getRandomString (line 31) | func getRandomString(l int) string {
  type extractor (line 43) | type extractor struct
    method Extract (line 51) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 46) | func New() extractors.Extractor {

FILE: extractors/miaopai/miaopai_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/netease/netease.go
  function init (line 14) | func init() {
  type extractor (line 18) | type extractor struct
    method Extract (line 26) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 21) | func New() extractors.Extractor {

FILE: extractors/netease/netease_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/odysee/odysee.go
  function init (line 16) | func init() {
  type extractor (line 20) | type extractor struct
    method Extract (line 35) | func (e *extractor) Extract(u string, option extractors.Options) ([]*e...
  type odyseePayload (line 22) | type odyseePayload struct
  function New (line 31) | func New() extractors.Extractor {

FILE: extractors/odysee/odysee_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/pinterest/pinterest.go
  function init (line 13) | func init() {
  type extractor (line 17) | type extractor struct
    method Extract (line 25) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 20) | func New() extractors.Extractor {

FILE: extractors/pinterest/pinterest_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/pixivision/pixivision.go
  function init (line 12) | func init() {
  type extractor (line 16) | type extractor struct
    method Extract (line 24) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 19) | func New() extractors.Extractor {

FILE: extractors/pixivision/pixivision_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/pornhub/pornhub.go
  function init (line 21) | func init() {
  type pornhubData (line 25) | type pornhubData struct
  type extractor (line 32) | type extractor struct
    method Extract (line 40) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 35) | func New() extractors.Extractor {

FILE: extractors/pornhub/pornhub_test.go
  function TestPornhub (line 10) | func TestPornhub(t *testing.T) {

FILE: extractors/qq/qq.go
  function init (line 17) | func init() {
  type qqVideoInfo (line 21) | type qqVideoInfo struct
  type qqKeyInfo (line 51) | type qqKeyInfo struct
  constant qqPlayerVersion (line 55) | qqPlayerVersion string = "3.2.19.333"
  function getVinfo (line 57) | func getVinfo(vid, defn, refer string) (qqVideoInfo, error) {
  function genStreams (line 79) | func genStreams(vid, cdn string, data qqVideoInfo) (map[string]*extracto...
  type extractor (line 177) | type extractor struct
    method Extract (line 185) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 180) | func New() extractors.Extractor {

FILE: extractors/qq/qq_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/reddit/reddit.go
  function init (line 14) | func init() {
  constant referer (line 19) | referer  = "https://www.reddit.com"
  constant siteName (line 20) | siteName = "Reddit reddit.com"
  constant redditMP4API (line 22) | redditMP4API = "https://v.redd.it/"
  constant redditIMGAPI (line 23) | redditIMGAPI = "https://i.redd.it/"
  constant audioURLPart (line 24) | audioURLPart = "/DASH_audio.mp4"
  type extractor (line 35) | type extractor struct
    method Extract (line 41) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 37) | func New() extractors.Extractor {

FILE: extractors/reddit/reddit_test.go
  function TestReddit (line 10) | func TestReddit(t *testing.T) {

FILE: extractors/rumble/rumble.go
  function init (line 22) | func init() {
  type extractor (line 26) | type extractor struct
    method Extract (line 44) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 29) | func New() extractors.Extractor {
  type rumbleData (line 33) | type rumbleData struct
  function readPayload (line 104) | func readPayload(html string) (*rumbleData, error) {
  function getVideoID (line 124) | func getVideoID(embedURL string) (string, error) {
  type rumbleResponse (line 134) | type rumbleResponse struct
  type streamInfo (line 139) | type streamInfo struct
  type videoQualities (line 150) | type videoQualities struct
  type rumbleStreams (line 162) | type rumbleStreams struct
    method UnmarshalJSON (line 177) | func (r *rumbleStreams) UnmarshalJSON(b []byte) error {
    method makeAllVODStreams (line 205) | func (rs *rumbleStreams) makeAllVODStreams(m map[string]*extractors.St...
    method makeAllLiveStreams (line 220) | func (rs *rumbleStreams) makeAllLiveStreams(m map[string]*extractors.S...
    method makeAllNewVodStreams (line 273) | func (rs *rumbleStreams) makeAllNewVodStreams(m map[string]*extractors...
  function fetchVideoQuality (line 304) | func fetchVideoQuality(videoID string) (map[string]*extractors.Stream, e...
  function makeStreamMeta (line 341) | func makeStreamMeta(q, ext string, info *streamInfo) *extractors.Stream {

FILE: extractors/rumble/rumble_test.go
  function TestRumble (line 10) | func TestRumble(t *testing.T) {

FILE: extractors/streamtape/streamtape.go
  function init (line 14) | func init() {
  type extractor (line 20) | type extractor struct
    method Extract (line 28) | func (e *extractor) Extract(url string, _ extractors.Options) ([]*extr...
  function New (line 23) | func New() extractors.Extractor {

FILE: extractors/streamtape/streamtape_test.go
  function TestStreamtape (line 10) | func TestStreamtape(t *testing.T) {

FILE: extractors/tangdou/tangdou.go
  function init (line 13) | func init() {
  type extractor (line 17) | type extractor struct
    method Extract (line 33) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 20) | func New() extractors.Extractor {
  function tangdouDownload (line 38) | func tangdouDownload(uri string) *extractors.Data {

FILE: extractors/tangdou/tangdou_test.go
  function TestTangDou (line 10) | func TestTangDou(t *testing.T) {

FILE: extractors/threads/threads.go
  function init (line 19) | func init() {
  type extractor (line 23) | type extractor struct
    method Extract (line 48) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 28) | func New() extractors.Extractor {
  type media (line 42) | type media struct

FILE: extractors/threads/threads_test.go
  function TestDownload (line 11) | func TestDownload(t *testing.T) {

FILE: extractors/tiktok/tiktok.go
  function init (line 13) | func init() {
  type extractor (line 17) | type extractor struct
    method Extract (line 25) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 20) | func New() extractors.Extractor {

FILE: extractors/tiktok/tiktok_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/tumblr/tumblr.go
  function init (line 15) | func init() {
  type imageList (line 19) | type imageList struct
  type tumblrImageList (line 23) | type tumblrImageList struct
  type tumblrImage (line 27) | type tumblrImage struct
  function genURLData (line 31) | func genURLData(url, referer string) (*extractors.Part, int64, error) {
  function tumblrImageDownload (line 47) | func tumblrImageDownload(url, html, title string) ([]*extractors.Data, e...
  function tumblrVideoDownload (line 103) | func tumblrVideoDownload(url, html, title string) ([]*extractors.Data, e...
  type extractor (line 146) | type extractor struct
    method Extract (line 154) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 149) | func New() extractors.Extractor {

FILE: extractors/tumblr/tumblr_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/twitter/twitter.go
  function init (line 16) | func init() {
  type twitter (line 20) | type twitter struct
  type extractor (line 28) | type extractor struct
    method Extract (line 36) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 31) | func New() extractors.Extractor {
  function download (line 78) | func download(data twitter, uri string) ([]*extractors.Data, error) {

FILE: extractors/twitter/twitter_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/types.go
  type Part (line 4) | type Part struct
  type CaptionPart (line 10) | type CaptionPart struct
  type Stream (line 16) | type Stream struct
  type DataType (line 34) | type DataType
  constant DataTypeVideo (line 38) | DataTypeVideo DataType = "video"
  constant DataTypeImage (line 40) | DataTypeImage DataType = "image"
  constant DataTypeAudio (line 42) | DataTypeAudio DataType = "audio"
  type Data (line 46) | type Data struct
    method FillUpStreamsData (line 61) | func (d *Data) FillUpStreamsData() {
  function EmptyData (line 94) | func EmptyData(url string, err error) *Data {
  type Options (line 102) | type Options struct
  type Extractor (line 125) | type Extractor interface

FILE: extractors/udn/udn.go
  function init (line 13) | func init() {
  constant startFlag (line 18) | startFlag = `',
  constant endFlag (line 20) | endFlag = `'
  function getCDNUrl (line 25) | func getCDNUrl(html string) string {
  function prepareEmbedURL (line 32) | func prepareEmbedURL(url string) string {
  type extractor (line 43) | type extractor struct
    method Extract (line 51) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 46) | func New() extractors.Extractor {

FILE: extractors/udn/udn_test.go
  function TestExtract (line 10) | func TestExtract(t *testing.T) {

FILE: extractors/universal/universal.go
  function init (line 11) | func init() {
  type extractor (line 15) | type extractor struct
    method Extract (line 23) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 18) | func New() extractors.Extractor {

FILE: extractors/universal/universal_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/vimeo/vimeo.go
  function init (line 14) | func init() {
  type vimeoProgressive (line 18) | type vimeoProgressive struct
  type vimeoFiles (line 26) | type vimeoFiles struct
  type vimeoRequest (line 30) | type vimeoRequest struct
  type vimeoVideo (line 34) | type vimeoVideo struct
  type vimeo (line 38) | type vimeo struct
  type extractor (line 43) | type extractor struct
    method Extract (line 51) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 46) | func New() extractors.Extractor {

FILE: extractors/vimeo/vimeo_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/vk/vk.go
  function init (line 14) | func init() {
  type extractor (line 27) | type extractor struct
    method Extract (line 33) | func (e extractor) Extract(url string, option extractors.Options) ([]*...
  function New (line 29) | func New() extractors.Extractor {

FILE: extractors/vk/vk_test.go
  function TestVK (line 10) | func TestVK(t *testing.T) {

FILE: extractors/weibo/weibo.go
  function init (line 20) | func init() {
  type playInfo (line 24) | type playInfo struct
  type playData (line 29) | type playData struct
  type weiboData (line 33) | type weiboData struct
  function getXSRFToken (line 39) | func getXSRFToken() (string, error) {
  function downloadWeiboVideo (line 69) | func downloadWeiboVideo(url string) ([]*extractors.Data, error) {
  function downloadWeiboTV (line 142) | func downloadWeiboTV(url string) ([]*extractors.Data, error) {
  type extractor (line 225) | type extractor struct
    method Extract (line 233) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 228) | func New() extractors.Extractor {

FILE: extractors/weibo/weibo_test.go
  function TestToken (line 10) | func TestToken(t *testing.T) {
  function TestDownload (line 16) | func TestDownload(t *testing.T) {

FILE: extractors/xiaohongshu/xiaohongshu.go
  function init (line 17) | func init() {
  type extractor (line 21) | type extractor struct
    method Extract (line 31) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 24) | func New() extractors.Extractor {
  constant mp4VideoType (line 28) | mp4VideoType = "mp4"

FILE: extractors/xiaohongshu/xiaohongshu_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/ximalaya/types.go
  type ximalayaData (line 3) | type ximalayaData struct

FILE: extractors/ximalaya/ximalaya.go
  function init (line 14) | func init() {
  type extractor (line 18) | type extractor struct
    method Extract (line 26) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 21) | func New() extractors.Extractor {

FILE: extractors/ximalaya/ximalaya_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/xinpianchang/xinpianchang.go
  function init (line 16) | func init() {
  type extractor (line 20) | type extractor struct
    method Extract (line 38) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  type Video (line 22) | type Video struct
  function New (line 33) | func New() extractors.Extractor {

FILE: extractors/xinpianchang/xinpianchang_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/xvideos/xvideos.go
  function init (line 14) | func init() {
  constant lowFlag (line 19) | lowFlag      = "html5player.setVideoUrlLow('"
  constant lowFinalFlag (line 20) | lowFinalFlag = `');
  constant highFlag (line 22) | highFlag      = "html5player.setVideoUrlHigh('"
  constant highFinalFlag (line 23) | highFinalFlag = `');
  constant qualityLow (line 25) | qualityLow  = "low"
  constant qualityHigh (line 26) | qualityHigh = "high"
  type src (line 34) | type src struct
  function getSrc (line 39) | func getSrc(html string) []*src {
  type extractor (line 84) | type extractor struct
    method Extract (line 92) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 87) | func New() extractors.Extractor {

FILE: extractors/xvideos/xvideos_test.go
  function TestExtract (line 10) | func TestExtract(t *testing.T) {

FILE: extractors/yinyuetai/types.go
  type yinyuetaiMvData (line 3) | type yinyuetaiMvData struct
  type videoInfo (line 9) | type videoInfo struct
  type coreVideoInfo (line 13) | type coreVideoInfo struct
  type videoURLModel (line 23) | type videoURLModel struct

FILE: extractors/yinyuetai/yinyuetai.go
  function init (line 14) | func init() {
  constant yinyuetaiAPI (line 18) | yinyuetaiAPI = "https://ext.yinyuetai.com/main/"
  constant actionGetMvInfo (line 21) | actionGetMvInfo = "get-h-mv-info"
  function genAPI (line 24) | func genAPI(action string, param string) string {
  type extractor (line 28) | type extractor struct
    method Extract (line 36) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 31) | func New() extractors.Extractor {

FILE: extractors/yinyuetai/yinyuetai_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/youku/youku.go
  function init (line 23) | func init() {
  type errorData (line 27) | type errorData struct
  type segs (line 32) | type segs struct
  type stream (line 37) | type stream struct
  type youkuVideo (line 46) | type youkuVideo struct
  type youkuShow (line 50) | type youkuShow struct
  type data (line 54) | type data struct
  type youkuData (line 61) | type youkuData struct
  constant youkuReferer (line 65) | youkuReferer = "https://v.youku.com"
  function getAudioLang (line 67) | func getAudioLang(lang string) string {
  function youkuUps (line 85) | func youkuUps(vid string, option extractors.Options) (*youkuData, error) {
  function getBytes (line 136) | func getBytes(val int32) []byte {
  function hashCode (line 142) | func hashCode(s string) int32 {
  function hmacSha1 (line 150) | func hmacSha1(key []byte, msg []byte) []byte {
  function generateUtdid (line 156) | func generateUtdid() string {
  function genData (line 170) | func genData(youkuData data) map[string]*extractors.Stream {
  type extractor (line 211) | type extractor struct
    method Extract (line 219) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 214) | func New() extractors.Extractor {

FILE: extractors/youku/youku_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/youtube/youtube.go
  function init (line 18) | func init() {
  constant referer (line 24) | referer = "https://www.youtube.com"
  type extractor (line 26) | type extractor struct
    method Extract (line 44) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
    method youtubeDownload (line 95) | func (e *extractor) youtubeDownload(url string, video *youtube.Video) ...
    method genPartByFormat (line 163) | func (e *extractor) genPartByFormat(video *youtube.Video, f *youtube.F...
  function New (line 31) | func New() extractors.Extractor {
  function getVideoAudio (line 180) | func getVideoAudio(v *youtube.Video, mimeType string) (*youtube.Format, ...
  function getStreamExt (line 189) | func getStreamExt(streamType string) string {

FILE: extractors/youtube/youtube_test.go
  function TestYoutube (line 10) | func TestYoutube(t *testing.T) {

FILE: extractors/zhihu/types.go
  type video (line 4) | type video struct
  type resolution (line 13) | type resolution struct

FILE: extractors/zhihu/zhihu.go
  constant videoURL (line 16) | videoURL = "www.zhihu.com/zvideo"
  constant api (line 17) | api      = "https://lens.zhihu.com/api/v4/videos/"
  function init (line 20) | func init() {
  type extractor (line 24) | type extractor struct
    method Extract (line 30) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 26) | func New() extractors.Extractor {

FILE: extractors/zhihu/zhihu_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: extractors/zingmp3/zingmp3.go
  function init (line 23) | func init() {
  type extractor (line 29) | type extractor struct
    method Extract (line 49) | func (e *extractor) Extract(url string, option extractors.Options) ([]...
  function New (line 32) | func New() extractors.Extractor {
  type params (line 36) | type params
  constant Domain (line 46) | Domain = "https://zingmp3.vn"
  function callApi (line 145) | func callApi(urlType string, p params) []byte {
  function updatingCookies (line 152) | func updatingCookies() error {
  function generateApi (line 176) | func generateApi(urlType string, p params) string {
  function generateSig (line 194) | func generateSig(slugApi string, p params) string {
  function sortedParams (line 209) | func sortedParams(p params) params {

FILE: extractors/zingmp3/zingmp3_test.go
  function TestDownload (line 10) | func TestDownload(t *testing.T) {

FILE: main.go
  function main (line 12) | func main() {

FILE: parser/parser.go
  function GetDoc (line 12) | func GetDoc(html string) (*goquery.Document, error) {
  function GetImages (line 21) | func GetImages(html, imgClass string, urlHandler func(string) string) (s...
  function Title (line 42) | func Title(doc *goquery.Document) string {

FILE: parser/parser_test.go
  function TestGetDoc (line 8) | func TestGetDoc(t *testing.T) {
  function TestGetImages (line 36) | func TestGetImages(t *testing.T) {
  function TestGetTitle (line 65) | func TestGetTitle(t *testing.T) {

FILE: request/request.go
  type Options (line 32) | type Options struct
  function SetOptions (line 42) | func SetOptions(opt Options) {
  function Request (line 51) | func Request(method, url string, body io.Reader, headers map[string]stri...
  function Get (line 142) | func Get(url, refer string, headers map[string]string) (string, error) {
  function GetByte (line 148) | func GetByte(url, refer string, headers map[string]string) ([]byte, erro...
  function Headers (line 180) | func Headers(url, refer string) (http.Header, error) {
  function Size (line 193) | func Size(url, refer string) (int64, error) {
  function ContentType (line 210) | func ContentType(url, refer string) (string, error) {

FILE: request/request_test.go
  function TestGet (line 7) | func TestGet(t *testing.T) {
  function TestHeaders (line 61) | func TestHeaders(t *testing.T) {
  function TestSize (line 89) | func TestSize(t *testing.T) {
  function TestContentType (line 117) | func TestContentType(t *testing.T) {

FILE: script/generate_github_action_template.js
  function generateCITemplate (line 10) | function generateCITemplate(moduleName) {

FILE: test/utils.go
  type Args (line 12) | type Args struct
  function CheckData (line 20) | func CheckData(args, data Args) bool {
  function Check (line 35) | func Check(t *testing.T, args Args, data *extractors.Data) {
  function CheckError (line 58) | func CheckError(t *testing.T, err error) {

FILE: utils/download.go
  function NeedDownloadList (line 9) | func NeedDownloadList(items string, itemStart, itemEnd, length int) []int {

FILE: utils/download_test.go
  function TestNeedDownloadList (line 8) | func TestNeedDownloadList(t *testing.T) {

FILE: utils/ffmpeg.go
  function findFFmpegExecutable (line 15) | func findFFmpegExecutable() string {
  function runMergeCmd (line 31) | func runMergeCmd(cmd *exec.Cmd, paths []string, mergeFilePath string) er...
  function MergeFilesWithSameExtension (line 51) | func MergeFilesWithSameExtension(paths []string, mergedFilePath string) ...
  function MergeToMP4 (line 64) | func MergeToMP4(paths []string, mergedFilePath string, filename string) ...
  function toISO639 (line 90) | func toISO639(lang string) string {
  function subtitleCodec (line 105) | func subtitleCodec(ext string) string {
  function EmbedSubtitles (line 117) | func EmbedSubtitles(videoPath string, subtitlePaths []string, langs []st...

FILE: utils/pool.go
  type WaitGroupPool (line 9) | type WaitGroupPool struct
    method Add (line 27) | func (p *WaitGroupPool) Add() {
    method Done (line 34) | func (p *WaitGroupPool) Done() {
    method Wait (line 41) | func (p *WaitGroupPool) Wait() {
  function NewWaitGroupPool (line 15) | func NewWaitGroupPool(size int) *WaitGroupPool {

FILE: utils/pool_test.go
  function TestWaitGroupPool (line 8) | func TestWaitGroupPool(t *testing.T) {

FILE: utils/utils.go
  function ConvertXMLToSRT (line 24) | func ConvertXMLToSRT(xmlContent []byte) (string, error) {
  function ConvertXMLFileToSRT (line 72) | func ConvertXMLFileToSRT(xmlPath string) (string, error) {
  function formatSRTTime (line 85) | func formatSRTTime(ms int) string {
  function MatchOneOf (line 96) | func MatchOneOf(text string, patterns ...string) []string {
  function MatchAll (line 115) | func MatchAll(text, pattern string) [][]string {
  function FileSize (line 122) | func FileSize(filePath string) (int64, bool, error) {
  function Domain (line 134) | func Domain(url string) string {
  function LimitLength (line 147) | func LimitLength(s string, length int) string {
  function FileName (line 162) | func FileName(name, ext string, length int) string {
  function FilePath (line 177) | func FilePath(name, ext string, length int, outputPath string, escape bo...
  function FileLineCounter (line 193) | func FileLineCounter(r io.Reader) (int, error) {
  function ParseInputFile (line 213) | func ParseInputFile(r io.Reader, items string, itemStart, itemEnd int) [...
  function GetNameAndExt (line 239) | func GetNameAndExt(uri string) (string, string, error) {
  function Md5 (line 260) | func Md5(text string) string {
  function M3u8URLs (line 267) | func M3u8URLs(uri string) ([]string, error) {
  function Reverse (line 300) | func Reverse(s string) string {
  function Range (line 309) | func Range(min, max int) []int {

FILE: utils/utils_test.go
  function TestMatchOneOf (line 9) | func TestMatchOneOf(t *testing.T) {
  function TestMatchAll (line 47) | func TestMatchAll(t *testing.T) {
  function TestFileSize (line 82) | func TestFileSize(t *testing.T) {
  function TestDomain (line 108) | func TestDomain(t *testing.T) {
  function TestLimitLength (line 169) | func TestLimitLength(t *testing.T) {
  function TestFileName (line 205) | func TestFileName(t *testing.T) {
  function TestFilePath (line 245) | func TestFilePath(t *testing.T) {
  function TestGetNameAndExt (line 284) | func TestGetNameAndExt(t *testing.T) {
  function TestMd5 (line 332) | func TestMd5(t *testing.T) {
  function TestReverse (line 358) | func TestReverse(t *testing.T) {
  function TestRange (line 384) | func TestRange(t *testing.T) {
  function TestLineCount (line 420) | func TestLineCount(t *testing.T) {
  function TestParsingFile (line 455) | func TestParsingFile(t *testing.T) {
  function TestConvertXMLToSRT (line 535) | func TestConvertXMLToSRT(t *testing.T) {
Condensed preview — 186 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (516K chars).
[
  {
    "path": ".gitattributes",
    "chars": 34,
    "preview": "*.go text eol=lf\n*.md text eol=lf\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/can-not-download-video.md",
    "chars": 744,
    "preview": "---\nname: Can not download video\nabout: If you find that a website download is not working\ntitle: \"[download fail]: the "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/support-a-new-website.md",
    "chars": 432,
    "preview": "---\nname: Support a new website\nabout: If you want to request support for a new website\ntitle: \"[new website require]: t"
  },
  {
    "path": ".github/workflows/builder.yml",
    "chars": 3392,
    "preview": "name: Builder\n\non:\n  push:\n    branches: \"*\"\n    paths:\n      - \"**/*.go\"\n      - \"go.mod\"\n      - \"go.sum\"\n      - \".gi"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 867,
    "preview": "name: ci\non:\n  push:\n  pull_request:\n  schedule:\n    # run ci weekly\n    - cron: \"0 0 * * 0\"\njobs:\n  ci:\n    runs-on: ${"
  },
  {
    "path": ".github/workflows/goreleaser.yml",
    "chars": 551,
    "preview": "name: goreleaser\non:\n  push:\n    tags:\n      - '*'\npermissions:\n  contents: write\njobs:\n  goreleaser:\n    runs-on: ubunt"
  },
  {
    "path": ".github/workflows/stream_acfun.yml",
    "chars": 697,
    "preview": "name: acfun\n\non:\n  push:\n    paths:\n      - \"extractors/acfun/*.go\"\n      - \".github/workflows/stream_acfun.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_bcy.yml",
    "chars": 685,
    "preview": "name: bcy\n\non:\n  push:\n    paths:\n      - \"extractors/bcy/*.go\"\n      - \".github/workflows/stream_bcy.yml\"\n  pull_reques"
  },
  {
    "path": ".github/workflows/stream_bilibili.yml",
    "chars": 715,
    "preview": "name: bilibili\n\non:\n  push:\n    paths:\n      - \"extractors/bilibili/*.go\"\n      - \".github/workflows/stream_bilibili.yml"
  },
  {
    "path": ".github/workflows/stream_bitchute.yml",
    "chars": 715,
    "preview": "name: bitchute\n\non:\n  push:\n    paths:\n      - \"extractors/bitchute/*.go\"\n      - \".github/workflows/stream_bitchute.yml"
  },
  {
    "path": ".github/workflows/stream_douyin.yml",
    "chars": 703,
    "preview": "name: douyin\n\non:\n  push:\n    paths:\n      - \"extractors/douyin/*.go\"\n      - \".github/workflows/stream_douyin.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_douyu.yml",
    "chars": 697,
    "preview": "name: douyu\n\non:\n  push:\n    paths:\n      - \"extractors/douyu/*.go\"\n      - \".github/workflows/stream_douyu.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_eporner.yml",
    "chars": 709,
    "preview": "name: eporner\n\non:\n  push:\n    paths:\n      - \"extractors/eporner/*.go\"\n      - \".github/workflows/stream_eporner.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_facebook.yml",
    "chars": 715,
    "preview": "name: facebook\n\non:\n  push:\n    paths:\n      - \"extractors/facebook/*.go\"\n      - \".github/workflows/stream_facebook.yml"
  },
  {
    "path": ".github/workflows/stream_geekbang.yml",
    "chars": 715,
    "preview": "name: geekbang\n\non:\n  push:\n    paths:\n      - \"extractors/geekbang/*.go\"\n      - \".github/workflows/stream_geekbang.yml"
  },
  {
    "path": ".github/workflows/stream_haokan.yml",
    "chars": 703,
    "preview": "name: haokan\n\non:\n  push:\n    paths:\n      - \"extractors/haokan/*.go\"\n      - \".github/workflows/stream_haokan.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_hupu.yml",
    "chars": 691,
    "preview": "name: hupu\n\non:\n  push:\n    paths:\n      - \"extractors/hupu/*.go\"\n      - \".github/workflows/stream_hupu.yml\"\n  pull_req"
  },
  {
    "path": ".github/workflows/stream_huya.yml",
    "chars": 691,
    "preview": "name: huya\n\non:\n  push:\n    paths:\n      - \"extractors/huya/*.go\"\n      - \".github/workflows/stream_huya.yml\"\n  pull_req"
  },
  {
    "path": ".github/workflows/stream_instagram.yml",
    "chars": 721,
    "preview": "name: instagram\n\non:\n  push:\n    paths:\n      - \"extractors/instagram/*.go\"\n      - \".github/workflows/stream_instagram."
  },
  {
    "path": ".github/workflows/stream_iqiyi.yml",
    "chars": 697,
    "preview": "name: iqiyi\n\non:\n  push:\n    paths:\n      - \"extractors/iqiyi/*.go\"\n      - \".github/workflows/stream_iqiyi.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_ixigua.yml",
    "chars": 703,
    "preview": "name: ixigua\n\non:\n  push:\n    paths:\n      - \"extractors/ixigua/*.go\"\n      - \".github/workflows/stream_ixigua.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_kuaishou.yml",
    "chars": 715,
    "preview": "name: kuaishou\n\non:\n  push:\n    paths:\n      - \"extractors/kuaishou/*.go\"\n      - \".github/workflows/stream_kuaishou.yml"
  },
  {
    "path": ".github/workflows/stream_mgtv.yml",
    "chars": 691,
    "preview": "name: mgtv\n\non:\n  push:\n    paths:\n      - \"extractors/mgtv/*.go\"\n      - \".github/workflows/stream_mgtv.yml\"\n  pull_req"
  },
  {
    "path": ".github/workflows/stream_miaopai.yml",
    "chars": 709,
    "preview": "name: miaopai\n\non:\n  push:\n    paths:\n      - \"extractors/miaopai/*.go\"\n      - \".github/workflows/stream_miaopai.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_netease.yml",
    "chars": 709,
    "preview": "name: netease\n\non:\n  push:\n    paths:\n      - \"extractors/netease/*.go\"\n      - \".github/workflows/stream_netease.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_odysee.yml",
    "chars": 703,
    "preview": "name: odysee\n\non:\n  push:\n    paths:\n      - \"extractors/odysee/*.go\"\n      - \".github/workflows/stream_odysee.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_pinterest.yml",
    "chars": 721,
    "preview": "name: pinterest\n\non:\n  push:\n    paths:\n      - \"extractors/pinterest/*.go\"\n      - \".github/workflows/stream_pinterest."
  },
  {
    "path": ".github/workflows/stream_pixivision.yml",
    "chars": 727,
    "preview": "name: pixivision\n\non:\n  push:\n    paths:\n      - \"extractors/pixivision/*.go\"\n      - \".github/workflows/stream_pixivisi"
  },
  {
    "path": ".github/workflows/stream_pornhub.yml",
    "chars": 709,
    "preview": "name: pornhub\n\non:\n  push:\n    paths:\n      - \"extractors/pornhub/*.go\"\n      - \".github/workflows/stream_pornhub.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_qq.yml",
    "chars": 679,
    "preview": "name: qq\n\non:\n  push:\n    paths:\n      - \"extractors/qq/*.go\"\n      - \".github/workflows/stream_qq.yml\"\n  pull_request:\n"
  },
  {
    "path": ".github/workflows/stream_reddit.yml",
    "chars": 703,
    "preview": "name: reddit\n\non:\n  push:\n    paths:\n      - \"extractors/reddit/*.go\"\n      - \".github/workflows/stream_reddit.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_rumble.yml",
    "chars": 703,
    "preview": "name: rumble\n\non:\n  push:\n    paths:\n      - \"extractors/rumble/*.go\"\n      - \".github/workflows/stream_rumble.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_streamtape.yml",
    "chars": 727,
    "preview": "name: streamtape\n\non:\n  push:\n    paths:\n      - \"extractors/streamtape/*.go\"\n      - \".github/workflows/stream_streamta"
  },
  {
    "path": ".github/workflows/stream_tangdou.yml",
    "chars": 709,
    "preview": "name: tangdou\n\non:\n  push:\n    paths:\n      - \"extractors/tangdou/*.go\"\n      - \".github/workflows/stream_tangdou.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_threads.yml",
    "chars": 711,
    "preview": "name: instagram\n\non:\n  push:\n    paths:\n      - \"extractors/threads/*.go\"\n      - \".github/workflows/stream_threads.yml\""
  },
  {
    "path": ".github/workflows/stream_tiktok.yml",
    "chars": 703,
    "preview": "name: tiktok\n\non:\n  push:\n    paths:\n      - \"extractors/tiktok/*.go\"\n      - \".github/workflows/stream_tiktok.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_tumblr.yml",
    "chars": 703,
    "preview": "name: tumblr\n\non:\n  push:\n    paths:\n      - \"extractors/tumblr/*.go\"\n      - \".github/workflows/stream_tumblr.yml\"\n  pu"
  },
  {
    "path": ".github/workflows/stream_twitter.yml",
    "chars": 709,
    "preview": "name: twitter\n\non:\n  push:\n    paths:\n      - \"extractors/twitter/*.go\"\n      - \".github/workflows/stream_twitter.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_udn.yml",
    "chars": 685,
    "preview": "name: udn\n\non:\n  push:\n    paths:\n      - \"extractors/udn/*.go\"\n      - \".github/workflows/stream_udn.yml\"\n  pull_reques"
  },
  {
    "path": ".github/workflows/stream_vimeo.yml",
    "chars": 697,
    "preview": "name: vimeo\n\non:\n  push:\n    paths:\n      - \"extractors/vimeo/*.go\"\n      - \".github/workflows/stream_vimeo.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_vk.yml",
    "chars": 679,
    "preview": "name: vk\n\non:\n  push:\n    paths:\n      - \"extractors/vk/*.go\"\n      - \".github/workflows/stream_vk.yml\"\n  pull_request:\n"
  },
  {
    "path": ".github/workflows/stream_weibo.yml",
    "chars": 697,
    "preview": "name: weibo\n\non:\n  push:\n    paths:\n      - \"extractors/weibo/*.go\"\n      - \".github/workflows/stream_weibo.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_xiaohongshu.yml",
    "chars": 733,
    "preview": "name: xiaohongshu\n\non:\n  push:\n    paths:\n      - \"extractors/xiaohongshu/*.go\"\n      - \".github/workflows/stream_xiaoho"
  },
  {
    "path": ".github/workflows/stream_ximalaya.yml",
    "chars": 715,
    "preview": "name: ximalaya\n\non:\n  push:\n    paths:\n      - \"extractors/ximalaya/*.go\"\n      - \".github/workflows/stream_ximalaya.yml"
  },
  {
    "path": ".github/workflows/stream_xinpianchang.yml",
    "chars": 739,
    "preview": "name: xinpianchang\n\non:\n  push:\n    paths:\n      - \"extractors/xinpianchang/*.go\"\n      - \".github/workflows/stream_xinp"
  },
  {
    "path": ".github/workflows/stream_xvideos.yml",
    "chars": 709,
    "preview": "name: xvideos\n\non:\n  push:\n    paths:\n      - \"extractors/xvideos/*.go\"\n      - \".github/workflows/stream_xvideos.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_yinyuetai.yml",
    "chars": 721,
    "preview": "name: yinyuetai\n\non:\n  push:\n    paths:\n      - \"extractors/yinyuetai/*.go\"\n      - \".github/workflows/stream_yinyuetai."
  },
  {
    "path": ".github/workflows/stream_youku.yml",
    "chars": 697,
    "preview": "name: youku\n\non:\n  push:\n    paths:\n      - \"extractors/youku/*.go\"\n      - \".github/workflows/stream_youku.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_youtube.yml",
    "chars": 709,
    "preview": "name: youtube\n\non:\n  push:\n    paths:\n      - \"extractors/youtube/*.go\"\n      - \".github/workflows/stream_youtube.yml\"\n "
  },
  {
    "path": ".github/workflows/stream_zhihu.yml",
    "chars": 697,
    "preview": "name: zhihu\n\non:\n  push:\n    paths:\n      - \"extractors/zhihu/*.go\"\n      - \".github/workflows/stream_zhihu.yml\"\n  pull_"
  },
  {
    "path": ".github/workflows/stream_zingmp3.yml",
    "chars": 709,
    "preview": "name: zingmp3\n\non:\n  push:\n    paths:\n      - \"extractors/zingmp3/*.go\"\n      - \".github/workflows/stream_zingmp3.yml\"\n "
  },
  {
    "path": ".gitignore",
    "chars": 379,
    "preview": "# Binaries for programs and plugins\n*.exe\n*.exe~\n*.dll\n*.so\n*.dylib\n\n# Test binary, build with `go test -c`\n*.test\n\n# Ou"
  },
  {
    "path": ".golangci.yml",
    "chars": 484,
    "preview": "run:\n  concurrency: 2\n  timeout: 5m\n  go: 1.24\n\nlinter-settings:\n  goconst:\n    min-len: 2\n    min-occurrences: 2\n\nlinte"
  },
  {
    "path": ".goreleaser.yml",
    "chars": 865,
    "preview": "project_name: lux\nenv:\n  - GO111MODULE=on\n  - CGO_ENABLED=0\nbefore:\n  hooks:\n    - go mod download\nbuilds:\n- binary: lux"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 906,
    "preview": "# Contributing Guide\n\n* [Style Guide](#style-guide)\n* [Build](#build)\n* [Features Requested](#features-requested)\n\n## St"
  },
  {
    "path": "Cask.toml",
    "chars": 1009,
    "preview": "[package]\nname = \"github.com/iawia002/lux\"\nbin = \"lux\"\nauthors = [\"Xinzhao Xu <z2d@jifangcheng.com>\"]\nkeywords = [\"go\", "
  },
  {
    "path": "LICENSE",
    "chars": 1070,
    "preview": "MIT License\n\nCopyright 2018-present, iawia002\n\nPermission is hereby granted, free of charge, to any person obtaining a c"
  },
  {
    "path": "README.md",
    "chars": 33251,
    "preview": "<h1 align=\"center\">Lux</h1>\n\n<p align=\"center\"><i>Let there be Lux!</i></p>\n\n<div align=\"center\">\n  <a href=\"https://cod"
  },
  {
    "path": "app/app.go",
    "chars": 8608,
    "preview": "package app\n\nimport (\n\t\"encoding/json\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com"
  },
  {
    "path": "app/register.go",
    "chars": 2212,
    "preview": "package app\n\nimport (\n\t_ \"github.com/iawia002/lux/extractors/acfun\"\n\t_ \"github.com/iawia002/lux/extractors/bcy\"\n\t_ \"gith"
  },
  {
    "path": "codecov.yml",
    "chars": 55,
    "preview": "codecov:\n  token: e0f2d44f-c6a7-469a-a688-37c72c0f18f9\n"
  },
  {
    "path": "config/config.go",
    "chars": 435,
    "preview": "package config\n\n// FakeHeaders fake http headers\nvar FakeHeaders = map[string]string{\n\t\"Accept\":          \"text/html,app"
  },
  {
    "path": "downloader/downloader.go",
    "chars": 19533,
    "preview": "package downloader\n\nimport (\n\t\"bytes\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path\"\n\t\"path/"
  },
  {
    "path": "downloader/downloader_test.go",
    "chars": 2316,
    "preview": "package downloader\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n)\n\nfunc TestDownload(t *testing.T) {\n\ttes"
  },
  {
    "path": "downloader/types.go",
    "chars": 871,
    "preview": "package downloader\n\n// Aria2RPCData defines the data structure of json RPC 2.0 info for Aria2\ntype Aria2RPCData struct {"
  },
  {
    "path": "downloader/utils.go",
    "chars": 2152,
    "preview": "package downloader\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/fatih/color\"\n\n\t\"github.com/iawia002/lux/extractors\"\n)\n\nvar (\n\t"
  },
  {
    "path": "extractors/acfun/acfun.go",
    "chars": 4027,
    "preview": "package acfun\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\n\tjsoniter \"github.com/json-iterator/go\"\n\t\"github.com/pkg/errors\"\n\n\t"
  },
  {
    "path": "extractors/acfun/acfun_test.go",
    "chars": 564,
    "preview": "package acfun\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDo"
  },
  {
    "path": "extractors/acfun/types.go",
    "chars": 886,
    "preview": "package acfun\n\ntype episodeData struct {\n\tItemID      int64  `json:\"itemId\"`\n\tEpisodeName string `json:\"episodeName\"`\n\tB"
  },
  {
    "path": "extractors/bcy/bcy.go",
    "chars": 2169,
    "preview": "package bcy\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"git"
  },
  {
    "path": "extractors/bcy/bcy_test.go",
    "chars": 601,
    "preview": "package bcy\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDown"
  },
  {
    "path": "extractors/bilibili/bilibili.go",
    "chars": 15751,
    "preview": "package bilibili\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/err"
  },
  {
    "path": "extractors/bilibili/bilibili_test.go",
    "chars": 2033,
    "preview": "package bilibili\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Tes"
  },
  {
    "path": "extractors/bilibili/types.go",
    "chars": 4698,
    "preview": "package bilibili\n\n// {\"code\":0,\"message\":\"0\",\"ttl\":1,\"data\":{\"token\":\"aaa\"}}\n// {\"code\":-101,\"message\":\"账号未登录\",\"ttl\":1}\n"
  },
  {
    "path": "extractors/bitchute/bitchute.go",
    "chars": 2572,
    "preview": "package bitchute\n\nimport (\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.co"
  },
  {
    "path": "extractors/bitchute/bitchute_test.go",
    "chars": 744,
    "preview": "package bitchute\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Tes"
  },
  {
    "path": "extractors/douyin/douyin.go",
    "chars": 6209,
    "preview": "package douyin\n\nimport (\n\t\"crypto/rand\"\n\t_ \"embed\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\tnetURL \"net/url\"\n\t\"regexp\"\n\t\"str"
  },
  {
    "path": "extractors/douyin/douyin_test.go",
    "chars": 728,
    "preview": "package douyin\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestD"
  },
  {
    "path": "extractors/douyin/sign.js",
    "chars": 50688,
    "preview": "var window = null;\n\nfunction _0x5cd844(e) {\n    var b = {\n        exports: {}\n    };\n    return e(b, b.exports), b.expor"
  },
  {
    "path": "extractors/douyin/types.go",
    "chars": 21693,
    "preview": "package douyin\n\ntype douyinData struct {\n\tStatusCode  int `json:\"status_code\"`\n\tAwemeDetail struct {\n\t\tAdmireAuth struct"
  },
  {
    "path": "extractors/douyu/douyu.go",
    "chars": 2631,
    "preview": "package douyu\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/i"
  },
  {
    "path": "extractors/douyu/douyu_test.go",
    "chars": 523,
    "preview": "package douyu\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDo"
  },
  {
    "path": "extractors/eporner/eporner.go",
    "chars": 3004,
    "preview": "package eporner\n\nimport (\n\t\"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"github.com/pkg/errors\"\n\n"
  },
  {
    "path": "extractors/eporner/eporner_test.go",
    "chars": 658,
    "preview": "package eporner\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/errors.go",
    "chars": 380,
    "preview": "package extractors\n\nimport (\n\t\"errors\"\n)\n\nvar (\n\t// ErrURLParseFailed defines url parse failed error.\n\tErrURLParseFailed"
  },
  {
    "path": "extractors/extractors.go",
    "chars": 1369,
    "preview": "package extractors\n\nimport (\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/utils\"\n)"
  },
  {
    "path": "extractors/facebook/facebook.go",
    "chars": 1797,
    "preview": "package facebook\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"githu"
  },
  {
    "path": "extractors/facebook/facebook_test.go",
    "chars": 665,
    "preview": "package facebook\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Tes"
  },
  {
    "path": "extractors/geekbang/geekbang.go",
    "chars": 4436,
    "preview": "package geekbang\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia0"
  },
  {
    "path": "extractors/geekbang/geekbang_test.go",
    "chars": 588,
    "preview": "package geekbang\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Tes"
  },
  {
    "path": "extractors/haokan/haokan.go",
    "chars": 1751,
    "preview": "package haokan\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia0"
  },
  {
    "path": "extractors/haokan/haokan_test.go",
    "chars": 592,
    "preview": "package haokan\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestD"
  },
  {
    "path": "extractors/hupu/hupu.go",
    "chars": 1471,
    "preview": "package hupu\n\nimport (\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/request"
  },
  {
    "path": "extractors/hupu/hupu_test.go",
    "chars": 598,
    "preview": "package hupu\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestHup"
  },
  {
    "path": "extractors/huya/huya.go",
    "chars": 1510,
    "preview": "package huya\n\nimport (\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/request"
  },
  {
    "path": "extractors/huya/huya_test.go",
    "chars": 568,
    "preview": "package huya\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestHuy"
  },
  {
    "path": "extractors/instagram/instagram.go",
    "chars": 4811,
    "preview": "package instagram\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\tnetURL \"net/url\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t"
  },
  {
    "path": "extractors/instagram/instagram_test.go",
    "chars": 916,
    "preview": "package instagram\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Te"
  },
  {
    "path": "extractors/iqiyi/iqiyi.go",
    "chars": 5633,
    "preview": "package iqiyi\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/pkg/errors\"\n\n\t\""
  },
  {
    "path": "extractors/iqiyi/iqiyi_test.go",
    "chars": 1073,
    "preview": "package iqiyi\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDo"
  },
  {
    "path": "extractors/ixigua/ixigua.go",
    "chars": 3140,
    "preview": "package ixigua\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\tbrowser \"github.c"
  },
  {
    "path": "extractors/ixigua/ixigua_test.go",
    "chars": 995,
    "preview": "package ixigua\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestD"
  },
  {
    "path": "extractors/ixigua/types.go",
    "chars": 1154,
    "preview": "package ixigua\n\ntype xiguanData struct {\n\tAnyVideo struct {\n\t\tGidInformation struct {\n\t\t\tGid        string `json:\"gid\"`\n"
  },
  {
    "path": "extractors/kuaishou/kuaishou.go",
    "chars": 2468,
    "preview": "package kuaishou\n\nimport (\n\t\"net/http\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extract"
  },
  {
    "path": "extractors/kuaishou/kuaishou_test.go",
    "chars": 706,
    "preview": "package kuaishou\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Tes"
  },
  {
    "path": "extractors/mgtv/mgtv.go",
    "chars": 4967,
    "preview": "package mgtv\n\nimport (\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/"
  },
  {
    "path": "extractors/mgtv/mgtv_test.go",
    "chars": 929,
    "preview": "package mgtv\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDow"
  },
  {
    "path": "extractors/miaopai/miaopai.go",
    "chars": 2374,
    "preview": "package miaopai\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"math/rand\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia0"
  },
  {
    "path": "extractors/miaopai/miaopai_test.go",
    "chars": 592,
    "preview": "package miaopai\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/netease/netease.go",
    "chars": 1803,
    "preview": "package netease\n\nimport (\n\tnetURL \"net/url\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n"
  },
  {
    "path": "extractors/netease/netease_test.go",
    "chars": 759,
    "preview": "package netease\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/odysee/odysee.go",
    "chars": 2080,
    "preview": "package odysee\n\nimport (\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\n\t\"github.com/i"
  },
  {
    "path": "extractors/odysee/odysee_test.go",
    "chars": 828,
    "preview": "package odysee\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestD"
  },
  {
    "path": "extractors/pinterest/pinterest.go",
    "chars": 2027,
    "preview": "package pinterest\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"gith"
  },
  {
    "path": "extractors/pinterest/pinterest_test.go",
    "chars": 834,
    "preview": "package pinterest\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Te"
  },
  {
    "path": "extractors/pixivision/pixivision.go",
    "chars": 1382,
    "preview": "package pixivision\n\nimport (\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/p"
  },
  {
    "path": "extractors/pixivision/pixivision_test.go",
    "chars": 587,
    "preview": "package pixivision\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc T"
  },
  {
    "path": "extractors/pornhub/pornhub.go",
    "chars": 4046,
    "preview": "package pornhub\n\nimport (\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"regexp\"\n\t\"strin"
  },
  {
    "path": "extractors/pornhub/pornhub_test.go",
    "chars": 522,
    "preview": "package pornhub\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/qq/qq.go",
    "chars": 5595,
    "preview": "package qq\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iaw"
  },
  {
    "path": "extractors/qq/qq_test.go",
    "chars": 1066,
    "preview": "package qq\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDownl"
  },
  {
    "path": "extractors/reddit/reddit.go",
    "chars": 4224,
    "preview": "package reddit\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com"
  },
  {
    "path": "extractors/reddit/reddit_test.go",
    "chars": 1682,
    "preview": "package reddit\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestR"
  },
  {
    "path": "extractors/rumble/rumble.go",
    "chars": 8578,
    "preview": "package rumble\n\nimport (\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\""
  },
  {
    "path": "extractors/rumble/rumble_test.go",
    "chars": 1017,
    "preview": "package rumble\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestR"
  },
  {
    "path": "extractors/streamtape/streamtape.go",
    "chars": 1973,
    "preview": "package streamtape\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\t\"github.com/robertkrimen/otto\"\n\n\t\"github.com/iawia002/lux"
  },
  {
    "path": "extractors/streamtape/streamtape_test.go",
    "chars": 614,
    "preview": "package streamtape\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc T"
  },
  {
    "path": "extractors/tangdou/tangdou.go",
    "chars": 2068,
    "preview": "package tangdou\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia"
  },
  {
    "path": "extractors/tangdou/tangdou_test.go",
    "chars": 978,
    "preview": "package tangdou\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/threads/threads.go",
    "chars": 3165,
    "preview": "package threads\n\nimport (\n\t\"fmt\"\n\t\"net\"\n\t\"net/http\"\n\tnetURL \"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gocolly/colly/v2\""
  },
  {
    "path": "extractors/threads/threads_test.go",
    "chars": 1217,
    "preview": "package threads_test\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/extractors/th"
  },
  {
    "path": "extractors/tiktok/tiktok.go",
    "chars": 2351,
    "preview": "package tiktok\n\nimport (\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github."
  },
  {
    "path": "extractors/tiktok/tiktok_test.go",
    "chars": 944,
    "preview": "package tiktok\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestD"
  },
  {
    "path": "extractors/tumblr/tumblr.go",
    "chars": 3989,
    "preview": "package tumblr\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\""
  },
  {
    "path": "extractors/tumblr/tumblr_test.go",
    "chars": 982,
    "preview": "package tumblr\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestD"
  },
  {
    "path": "extractors/twitter/twitter.go",
    "chars": 3557,
    "preview": "package twitter\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002"
  },
  {
    "path": "extractors/twitter/twitter_test.go",
    "chars": 1086,
    "preview": "package twitter\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/types.go",
    "chars": 3690,
    "preview": "package extractors\n\n// Part is the data structure for a single part of the video stream information.\ntype Part struct {\n"
  },
  {
    "path": "extractors/udn/udn.go",
    "chars": 2093,
    "preview": "package udn\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/"
  },
  {
    "path": "extractors/udn/udn_test.go",
    "chars": 567,
    "preview": "package udn\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestExtr"
  },
  {
    "path": "extractors/universal/universal.go",
    "chars": 1145,
    "preview": "package universal\n\nimport (\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/re"
  },
  {
    "path": "extractors/universal/universal_test.go",
    "chars": 634,
    "preview": "package universal\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Te"
  },
  {
    "path": "extractors/vimeo/vimeo.go",
    "chars": 2422,
    "preview": "package vimeo\n\nimport (\n\t\"encoding/json\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"g"
  },
  {
    "path": "extractors/vimeo/vimeo_test.go",
    "chars": 706,
    "preview": "package vimeo\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDo"
  },
  {
    "path": "extractors/vk/vk.go",
    "chars": 2611,
    "preview": "package vk\n\nimport (\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/config\"\n\t\"github.com/iawia002/lux/e"
  },
  {
    "path": "extractors/vk/vk_test.go",
    "chars": 589,
    "preview": "package vk\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestVK(t "
  },
  {
    "path": "extractors/weibo/weibo.go",
    "chars": 7504,
    "preview": "package weibo\n\nimport (\n\t\"compress/gzip\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\tnetURL \"net/url\"\n\t\"strconv\"\n\t\"string"
  },
  {
    "path": "extractors/weibo/weibo_test.go",
    "chars": 1356,
    "preview": "package weibo\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestTo"
  },
  {
    "path": "extractors/xiaohongshu/xiaohongshu.go",
    "chars": 2259,
    "preview": "package xiaohongshu\n\nimport (\n\t\"encoding/json\"\n\tneturl \"net/url\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"gith"
  },
  {
    "path": "extractors/xiaohongshu/xiaohongshu_test.go",
    "chars": 603,
    "preview": "package xiaohongshu\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc "
  },
  {
    "path": "extractors/ximalaya/types.go",
    "chars": 335,
    "preview": "package ximalaya\n\ntype ximalayaData struct {\n\tStatusCode int `json:\"ret\"`\n\tData       struct {\n\t\tTrackId         int    "
  },
  {
    "path": "extractors/ximalaya/ximalaya.go",
    "chars": 2001,
    "preview": "package ximalaya\n\nimport (\n\t\"encoding/json\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.co"
  },
  {
    "path": "extractors/ximalaya/ximalaya_test.go",
    "chars": 566,
    "preview": "package ximalaya\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Tes"
  },
  {
    "path": "extractors/xinpianchang/xinpianchang.go",
    "chars": 2654,
    "preview": "package xinpianchang\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"regexp\"\n\t\"strings\"\n\n\t\"github.com/itchyny/gojq\"\n\t\"github.com/pkg"
  },
  {
    "path": "extractors/xinpianchang/xinpianchang_test.go",
    "chars": 620,
    "preview": "package xinpianchang\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc"
  },
  {
    "path": "extractors/xvideos/xvideos.go",
    "chars": 2579,
    "preview": "package xvideos\n\nimport (\n\t\"strings\"\n\t\"sync\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.c"
  },
  {
    "path": "extractors/xvideos/xvideos_test.go",
    "chars": 695,
    "preview": "package xvideos\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/yinyuetai/types.go",
    "chars": 1011,
    "preview": "package yinyuetai\n\ntype yinyuetaiMvData struct {\n\tError     bool      `json:\"error\"`\n\tMessage   string    `json:\"message"
  },
  {
    "path": "extractors/yinyuetai/yinyuetai.go",
    "chars": 2295,
    "preview": "package yinyuetai\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"g"
  },
  {
    "path": "extractors/yinyuetai/yinyuetai_test.go",
    "chars": 523,
    "preview": "package yinyuetai\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Te"
  },
  {
    "path": "extractors/youku/youku.go",
    "chars": 6366,
    "preview": "package youku\n\nimport (\n\t\"bytes\"\n\t\"crypto/hmac\"\n\t\"crypto/sha1\"\n\t\"encoding/base64\"\n\t\"encoding/binary\"\n\t\"encoding/json\"\n\t\""
  },
  {
    "path": "extractors/youku/youku_test.go",
    "chars": 584,
    "preview": "package youku\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDo"
  },
  {
    "path": "extractors/youtube/youtube.go",
    "chars": 5053,
    "preview": "package youtube\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"slices\"\n\t\"strconv\"\n\t\"strings\"\n\n\t\"github.com/kkdai/youtube/v2\"\n\t\"github.co"
  },
  {
    "path": "extractors/youtube/youtube_test.go",
    "chars": 1570,
    "preview": "package youtube\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "extractors/zhihu/types.go",
    "chars": 326,
    "preview": "package zhihu\n\n// minimum field\ntype video struct {\n\tPlayList struct {\n\t\tFHD resolution `json:\"FHD\"`\n\t\tHD  resolution `j"
  },
  {
    "path": "extractors/zhihu/zhihu.go",
    "chars": 1819,
    "preview": "package zhihu\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors\"\n\n\t\"github.com/iawia002/lux/extracto"
  },
  {
    "path": "extractors/zhihu/zhihu_test.go",
    "chars": 570,
    "preview": "package zhihu\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc TestDo"
  },
  {
    "path": "extractors/zingmp3/zingmp3.go",
    "chars": 5767,
    "preview": "package zingmp3\n\nimport (\n\t\"crypto/hmac\"\n\t\"crypto/sha256\"\n\t\"crypto/sha512\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"maps\"\n\t\"net/http\"\n\tn"
  },
  {
    "path": "extractors/zingmp3/zingmp3_test.go",
    "chars": 900,
    "preview": "package zingmp3\n\nimport (\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n\t\"github.com/iawia002/lux/test\"\n)\n\nfunc Test"
  },
  {
    "path": "go.mod",
    "chars": 2489,
    "preview": "module github.com/iawia002/lux\n\ngo 1.24\n\nrequire (\n\tgithub.com/EDDYCJY/fake-useragent v0.2.0\n\tgithub.com/MercuryEngineer"
  },
  {
    "path": "go.sum",
    "chars": 18452,
    "preview": "github.com/EDDYCJY/fake-useragent v0.2.0 h1:Jcnkk2bgXmDpX0z+ELlUErTkoLb/mxFBNd2YdcpvJBs=\ngithub.com/EDDYCJY/fake-userage"
  },
  {
    "path": "main.go",
    "chars": 305,
    "preview": "package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\n\t\"github.com/fatih/color\"\n\n\t\"github.com/iawia002/lux/app\"\n)\n\nfunc main() {\n\tif err "
  },
  {
    "path": "parser/parser.go",
    "chars": 1382,
    "preview": "package parser\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/PuerkitoBio/goquery\"\n\t\"github.com/pkg/errors\"\n)\n\n// GetDoc retu"
  },
  {
    "path": "parser/parser_test.go",
    "chars": 2178,
    "preview": "package parser\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestGetDoc(t *testing.T) {\n\ttype args struct {\n\t\thtml string\n\t}\n\t"
  },
  {
    "path": "request/request.go",
    "chars": 4909,
    "preview": "package request\n\nimport (\n\t\"compress/flate\"\n\t\"compress/gzip\"\n\t\"crypto/tls\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/http/cookiejar"
  },
  {
    "path": "request/request_test.go",
    "chars": 2370,
    "preview": "package request\n\nimport (\n\t\"testing\"\n)\n\nfunc TestGet(t *testing.T) {\n\tvar err error\n\ttype args struct {\n\t\turl     string"
  },
  {
    "path": "script/generate_github_action_template.js",
    "chars": 808,
    "preview": "const path = require(\"path\");\nconst fs = require(\"fs\");\n\nconst extractorDir = path.join(__dirname, \"..\", \"extractors\");\n"
  },
  {
    "path": "script/github_action_template.yml",
    "chars": 727,
    "preview": "name: {{module}}\n\non:\n  push:\n    paths:\n      - \"extractors/{{module}}/*.go\"\n      - \".github/workflows/stream_{{module"
  },
  {
    "path": "test/utils.go",
    "chars": 1356,
    "preview": "package test\n\nimport (\n\t\"fmt\"\n\t\"sort\"\n\t\"testing\"\n\n\t\"github.com/iawia002/lux/extractors\"\n)\n\n// Args Arguments for extract"
  },
  {
    "path": "utils/download.go",
    "chars": 843,
    "preview": "package utils\n\nimport (\n\t\"strconv\"\n\t\"strings\"\n)\n\n// NeedDownloadList return the indices of playlist that need download\nf"
  },
  {
    "path": "utils/download_test.go",
    "chars": 1471,
    "preview": "package utils\n\nimport (\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestNeedDownloadList(t *testing.T) {\n\ttype args struct {\n\t\tlen int"
  },
  {
    "path": "utils/ffmpeg.go",
    "chars": 4040,
    "preview": "package utils\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"github.com/pkg/errors"
  },
  {
    "path": "utils/pool.go",
    "chars": 889,
    "preview": "package utils\n\nimport (\n\t\"math\"\n\t\"sync\"\n)\n\n// WaitGroupPool pool of WaitGroup\ntype WaitGroupPool struct {\n\tpool chan str"
  },
  {
    "path": "utils/pool_test.go",
    "chars": 384,
    "preview": "package utils\n\nimport (\n\t\"sync/atomic\"\n\t\"testing\"\n)\n\nfunc TestWaitGroupPool(t *testing.T) {\n\twgp := NewWaitGroupPool(10)"
  },
  {
    "path": "utils/utils.go",
    "chars": 7266,
    "preview": "package utils\n\nimport (\n\t\"bufio\"\n\t\"bytes\"\n\t\"crypto/md5\"\n\t\"encoding/xml\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t"
  },
  {
    "path": "utils/utils_test.go",
    "chars": 10105,
    "preview": "package utils\n\nimport (\n\t\"os\"\n\t\"reflect\"\n\t\"testing\"\n)\n\nfunc TestMatchOneOf(t *testing.T) {\n\ttype args struct {\n\t\tpattern"
  }
]

About this extraction

This page contains the full source code of the iawia002/lux GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 186 files (449.2 KB), approximately 156.3k tokens, and a symbol index with 554 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!