Full Code of guiyumin/vget for AI

main 4cb325d9aaaa cached
304 files
1.8 MB
487.8k tokens
1622 symbols
1 requests
Download .txt
Showing preview only (1,937K chars total). Download the full file or copy to clipboard to get everything.
Repository: guiyumin/vget
Branch: main
Commit: 4cb325d9aaaa
Files: 304
Total size: 1.8 MB

Directory structure:
gitextract_53wrt5os/

├── .dockerignore
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── .vscode/
│   └── settings.json
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── README_de.md
├── README_es.md
├── README_fr.md
├── README_jp.md
├── README_kr.md
├── README_zh.md
├── TODO.md
├── cmd/
│   ├── vget/
│   │   └── main.go
│   └── vget-server/
│       └── main.go
├── compose.yml
├── docker/
│   └── vget/
│       ├── Dockerfile
│       ├── Dockerfile.arm64
│       ├── entrypoint-arm64.sh
│       └── entrypoint.sh
├── docs/
│   ├── FAQs.md
│   ├── PRD.md
│   ├── YOUTUBE_NOTES.md
│   ├── bilibili-port-plan.md
│   ├── bugfix/
│   │   └── docker-browser-launch.md
│   ├── homebrew-distribution.md
│   ├── http-server-mode.md
│   ├── multi-binary-architecture.md
│   ├── seedbox.md
│   ├── tauri.md
│   ├── telegram.md
│   ├── torrent-dispatch.md
│   ├── tui-file-browser.md
│   ├── webdav-browsing.md
│   ├── webdav.md
│   ├── xhs-mcp-analysis.md
│   └── zsh-completion-limit.md
├── go.mod
├── go.sum
├── internal/
│   ├── cli/
│   │   ├── batch.go
│   │   ├── browse.go
│   │   ├── completion.go
│   │   ├── config.go
│   │   ├── extract.go
│   │   ├── init.go
│   │   ├── kuaidi100.go
│   │   ├── login/
│   │   │   ├── bilibili.go
│   │   │   └── qrwriter.go
│   │   ├── login.go
│   │   ├── ls.go
│   │   ├── root.go
│   │   ├── search.go
│   │   ├── search_tui.go
│   │   ├── telegram.go
│   │   ├── update.go
│   │   └── version.go
│   ├── core/
│   │   ├── config/
│   │   │   ├── config.go
│   │   │   ├── config_test.go
│   │   │   ├── sites.go
│   │   │   └── wizard.go
│   │   ├── downloader/
│   │   │   ├── downloader.go
│   │   │   ├── ffmpeg.go
│   │   │   ├── hls.go
│   │   │   ├── hls_parser.go
│   │   │   ├── magic.go
│   │   │   ├── multistream.go
│   │   │   └── progress.go
│   │   ├── extractor/
│   │   │   ├── bilibili.go
│   │   │   ├── browser.go
│   │   │   ├── direct.go
│   │   │   ├── instagram.go
│   │   │   ├── itunes.go
│   │   │   ├── m3u8.go
│   │   │   ├── registry.go
│   │   │   ├── telegram/
│   │   │   │   ├── constants.go
│   │   │   │   ├── download.go
│   │   │   │   ├── extractor.go
│   │   │   │   ├── media.go
│   │   │   │   ├── parser.go
│   │   │   │   ├── session.go
│   │   │   │   └── takeout.go
│   │   │   ├── telegram.go
│   │   │   ├── tiktok.go
│   │   │   ├── twitter.go
│   │   │   ├── types.go
│   │   │   ├── types_test.go
│   │   │   ├── xiaohongshu.go
│   │   │   ├── xiaoyuzhou.go
│   │   │   └── youtube.go
│   │   ├── i18n/
│   │   │   ├── i18n.go
│   │   │   └── locales/
│   │   │       ├── de.yml
│   │   │       ├── en.yml
│   │   │       ├── es.yml
│   │   │       ├── fr.yml
│   │   │       ├── jp.yml
│   │   │       ├── kr.yml
│   │   │       └── zh.yml
│   │   ├── site/
│   │   │   └── bilibili/
│   │   │       └── auth.go
│   │   ├── tracker/
│   │   │   └── kuaidi100.go
│   │   ├── version/
│   │   │   └── version.go
│   │   └── webdav/
│   │       └── client.go
│   ├── server/
│   │   ├── auth.go
│   │   ├── bilibili.go
│   │   ├── embed.go
│   │   ├── history.go
│   │   ├── job.go
│   │   ├── podcast.go
│   │   ├── server.go
│   │   └── webdav_browse.go
│   ├── torrent/
│   │   ├── client.go
│   │   ├── qbittorrent.go
│   │   ├── synology.go
│   │   └── transmission.go
│   └── updater/
│       └── updater.go
├── sites.md
├── tauri/
│   ├── .gitignore
│   ├── Makefile
│   ├── components.json
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── components/
│   │   │   ├── AppSidebar.tsx
│   │   │   ├── home/
│   │   │   │   ├── DownloadItem.tsx
│   │   │   │   ├── HomePage.tsx
│   │   │   │   └── types.ts
│   │   │   ├── icons/
│   │   │   │   └── PdfIcon.tsx
│   │   │   ├── media-tools/
│   │   │   │   ├── MediaToolsPage.tsx
│   │   │   │   ├── panels/
│   │   │   │   │   ├── AudioConvertPanel.tsx
│   │   │   │   │   ├── CompressPanel.tsx
│   │   │   │   │   ├── ConvertPanel.tsx
│   │   │   │   │   ├── ExtractAudioPanel.tsx
│   │   │   │   │   ├── ExtractFramesPanel.tsx
│   │   │   │   │   ├── TrimPanel.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── pdf-tools/
│   │   │   │   ├── PDFToolsPage.tsx
│   │   │   │   ├── panels/
│   │   │   │   │   ├── DeletePagesPanel.tsx
│   │   │   │   │   ├── ImagesToPdfPanel.tsx
│   │   │   │   │   ├── Md2PdfPanel.tsx
│   │   │   │   │   ├── MergePdfPanel.tsx
│   │   │   │   │   ├── RemoveWatermarkPanel.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── settings/
│   │   │   │   ├── AboutSettings.tsx
│   │   │   │   ├── GeneralSettings.tsx
│   │   │   │   ├── SettingsPage.tsx
│   │   │   │   ├── SiteSettings.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   └── ui/
│   │   │       ├── accordion.tsx
│   │   │       ├── alert-dialog.tsx
│   │   │       ├── alert.tsx
│   │   │       ├── aspect-ratio.tsx
│   │   │       ├── avatar.tsx
│   │   │       ├── badge.tsx
│   │   │       ├── breadcrumb.tsx
│   │   │       ├── button-group.tsx
│   │   │       ├── button.tsx
│   │   │       ├── calendar.tsx
│   │   │       ├── card.tsx
│   │   │       ├── carousel.tsx
│   │   │       ├── chart.tsx
│   │   │       ├── checkbox.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── command.tsx
│   │   │       ├── context-menu.tsx
│   │   │       ├── dialog.tsx
│   │   │       ├── drawer.tsx
│   │   │       ├── dropdown-menu.tsx
│   │   │       ├── empty.tsx
│   │   │       ├── field.tsx
│   │   │       ├── file-drop-input.tsx
│   │   │       ├── form.tsx
│   │   │       ├── hover-card.tsx
│   │   │       ├── input-group.tsx
│   │   │       ├── input-otp.tsx
│   │   │       ├── input.tsx
│   │   │       ├── item.tsx
│   │   │       ├── kbd.tsx
│   │   │       ├── label.tsx
│   │   │       ├── menubar.tsx
│   │   │       ├── navigation-menu.tsx
│   │   │       ├── pagination.tsx
│   │   │       ├── popover.tsx
│   │   │       ├── progress.tsx
│   │   │       ├── radio-group.tsx
│   │   │       ├── resizable.tsx
│   │   │       ├── scroll-area.tsx
│   │   │       ├── select.tsx
│   │   │       ├── separator.tsx
│   │   │       ├── sheet.tsx
│   │   │       ├── sidebar.tsx
│   │   │       ├── skeleton.tsx
│   │   │       ├── slider.tsx
│   │   │       ├── sonner.tsx
│   │   │       ├── spinner.tsx
│   │   │       ├── switch.tsx
│   │   │       ├── table.tsx
│   │   │       ├── tabs.tsx
│   │   │       ├── textarea.tsx
│   │   │       ├── toggle-group.tsx
│   │   │       ├── toggle.tsx
│   │   │       └── tooltip.tsx
│   │   ├── hooks/
│   │   │   ├── use-mobile.ts
│   │   │   └── useDropZone.ts
│   │   ├── i18n/
│   │   │   ├── index.ts
│   │   │   └── locales/
│   │   │       ├── de.yml
│   │   │       ├── en.yml
│   │   │       ├── es.yml
│   │   │       ├── fr.yml
│   │   │       ├── jp.yml
│   │   │       ├── kr.yml
│   │   │       └── zh.yml
│   │   ├── index.css
│   │   ├── lib/
│   │   │   └── utils.ts
│   │   ├── main.tsx
│   │   ├── routeTree.gen.ts
│   │   ├── routes/
│   │   │   ├── __root.tsx
│   │   │   ├── index.tsx
│   │   │   ├── media-tools.tsx
│   │   │   ├── pdf-tools.tsx
│   │   │   └── settings.tsx
│   │   ├── services/
│   │   │   └── dockerApi.ts
│   │   ├── stores/
│   │   │   ├── auth.ts
│   │   │   └── downloads.ts
│   │   └── vite-env.d.ts
│   ├── src-tauri/
│   │   ├── Cargo.toml
│   │   ├── binaries/
│   │   │   └── .gitkeep
│   │   ├── build.rs
│   │   ├── capabilities/
│   │   │   └── default.json
│   │   ├── gen/
│   │   │   └── schemas/
│   │   │       ├── acl-manifests.json
│   │   │       ├── capabilities.json
│   │   │       ├── desktop-schema.json
│   │   │       └── macOS-schema.json
│   │   ├── icons/
│   │   │   ├── android/
│   │   │   │   ├── mipmap-anydpi-v26/
│   │   │   │   │   └── ic_launcher.xml
│   │   │   │   └── values/
│   │   │   │       └── ic_launcher_background.xml
│   │   │   └── icon.icns
│   │   ├── rust-toolchain.toml
│   │   ├── src/
│   │   │   ├── auth.rs
│   │   │   ├── config.rs
│   │   │   ├── downloader/
│   │   │   │   ├── mod.rs
│   │   │   │   └── simple.rs
│   │   │   ├── extractor/
│   │   │   │   ├── bilibili.rs
│   │   │   │   ├── direct.rs
│   │   │   │   ├── mod.rs
│   │   │   │   ├── twitter.rs
│   │   │   │   └── types.rs
│   │   │   ├── ffmpeg.rs
│   │   │   ├── lib.rs
│   │   │   ├── main.rs
│   │   │   ├── md2pdf.rs
│   │   │   └── pdf.rs
│   │   └── tauri.conf.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
└── ui/
    ├── .gitignore
    ├── README.md
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── src/
    │   ├── components/
    │   │   ├── ConfigEditor.tsx
    │   │   ├── ConfigRow.tsx
    │   │   ├── DownloadJobCard.tsx
    │   │   ├── Kuaidi100.tsx
    │   │   ├── Layout.tsx
    │   │   ├── Sidebar.tsx
    │   │   ├── Toast.tsx
    │   │   ├── Torrent.tsx
    │   │   └── TorrentSettings.tsx
    │   ├── context/
    │   │   └── AppContext.tsx
    │   ├── index.css
    │   ├── main.tsx
    │   ├── pages/
    │   │   ├── BilibiliPage.tsx
    │   │   ├── BulkDownloadPage.tsx
    │   │   ├── ConfigPage.tsx
    │   │   ├── DownloadPage.tsx
    │   │   ├── HistoryPage.tsx
    │   │   ├── Kuaidi100Page.tsx
    │   │   ├── PodcastPage.tsx
    │   │   ├── TokenPage.tsx
    │   │   ├── TorrentPage.tsx
    │   │   └── WebDAVPage.tsx
    │   ├── routeTree.gen.ts
    │   ├── routes/
    │   │   ├── __root.tsx
    │   │   ├── bilibili.tsx
    │   │   ├── bulk.tsx
    │   │   ├── config.tsx
    │   │   ├── history.tsx
    │   │   ├── index.tsx
    │   │   ├── kuaidi100.tsx
    │   │   ├── podcast.tsx
    │   │   ├── token.tsx
    │   │   ├── torrent.tsx
    │   │   └── webdav.tsx
    │   └── utils/
    │       ├── apis.ts
    │       └── translations.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts

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

================================================
FILE: .dockerignore
================================================
# Build artifacts
build/
tmp/
*.exe
*.dll
*.so
*.dylib

# UI build artifacts (will be built in container)
ui/node_modules/
ui/dist/

# Embedded UI dist (will be copied from ui-builder stage)
internal/server/dist/

# Development files
.git/
.gitignore
.air.toml
.vscode/
.idea/
*.md
!README.md

# Test files
*_test.go
**/*_test.go

# Config and secrets
*.env
*.local
config.yml
config.yaml

# OS files
.DS_Store
Thumbs.db

# Docker files (not needed inside container)
Dockerfile
docker-compose*.yml
.dockerignore


================================================
FILE: .github/workflows/ci.yml
================================================
name: CI

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

jobs:
  test:
    name: Test
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Create placeholders for embed
        run: |
          mkdir -p internal/server/dist && touch internal/server/dist/.gitkeep

      - name: Run tests
        run: CGO_ENABLED=0 go test -v ./...

  lint:
    name: Lint
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version-file: go.mod

      - name: Create placeholders for embed
        run: |
          mkdir -p internal/server/dist && touch internal/server/dist/.gitkeep

      - name: Run staticcheck
        uses: dominikh/staticcheck-action@v1
        with:
          version: "latest"
        env:
          CGO_ENABLED: "0"


================================================
FILE: .github/workflows/release.yml
================================================
name: Release

on:
  push:
    tags:
      - "v*.*.*"

permissions:
  contents: write
  packages: write

jobs:
  release:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v5
        with:
          fetch-depth: 0

      - name: Set up Go
        uses: actions/setup-go@v5
        with:
          go-version: "1.25.4"

      - name: Set up Node.js
        uses: actions/setup-node@v4
        with:
          node-version: "22"

      - name: Build UI
        run: |
          cd ui && npm install && npm run build
          cd ..
          mkdir -p internal/server/dist
          rm -rf internal/server/dist/*
          cp -r ui/dist/* internal/server/dist/

      - name: Run GoReleaser
        uses: goreleaser/goreleaser-action@v6
        with:
          distribution: goreleaser
          version: "~> v2"
          args: release --clean
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAP_GITHUB_TOKEN: ${{ secrets.TAP_GITHUB_TOKEN }}

      - name: Extract version
        id: version
        run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT

      - name: Prepare latest zips
        run: |
          mkdir -p latest
          cp dist/vget_${{ steps.version.outputs.VERSION }}_darwin_amd64.zip latest/vget-darwin-amd64.zip
          cp dist/vget_${{ steps.version.outputs.VERSION }}_darwin_arm64.zip latest/vget-darwin-arm64.zip
          cp dist/vget_${{ steps.version.outputs.VERSION }}_linux_amd64.zip latest/vget-linux-amd64.zip
          cp dist/vget_${{ steps.version.outputs.VERSION }}_linux_arm64.zip latest/vget-linux-arm64.zip
          cp dist/vget_${{ steps.version.outputs.VERSION }}_windows_amd64.zip latest/vget-windows-amd64.zip

      - name: Upload latest zips
        uses: softprops/action-gh-release@v2
        with:
          files: |
            latest/vget-darwin-amd64.zip
            latest/vget-darwin-arm64.zip
            latest/vget-linux-amd64.zip
            latest/vget-linux-arm64.zip
            latest/vget-windows-amd64.zip
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

  docker-amd64:
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - name: Free disk space
        run: |
          sudo rm -rf /usr/share/dotnet /usr/local/lib/android /opt/ghc /opt/hostedtoolcache/CodeQL
          sudo docker image prune --all --force

      - name: Checkout
        uses: actions/checkout@v5

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/vget/Dockerfile
          platforms: linux/amd64
          outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true

  docker-arm64:
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-24.04-arm
    outputs:
      digest: ${{ steps.build.outputs.digest }}
    steps:
      - name: Checkout
        uses: actions/checkout@v5

      - name: Set up Docker Buildx
        uses: docker/setup-buildx-action@v3

      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Build and push by digest
        id: build
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ./docker/vget/Dockerfile.arm64
          platforms: linux/arm64
          outputs: type=image,name=ghcr.io/${{ github.repository }},push-by-digest=true,name-canonical=true,push=true

  docker-manifest:
    if: ${{ !contains(github.ref_name, '-') }}
    runs-on: ubuntu-latest
    needs: [docker-amd64, docker-arm64]
    steps:
      - name: Login to GitHub Container Registry
        uses: docker/login-action@v3
        with:
          registry: ghcr.io
          username: ${{ github.repository_owner }}
          password: ${{ secrets.GITHUB_TOKEN }}

      - name: Extract metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository }}
          tags: |
            type=semver,pattern={{version}}
            type=semver,pattern={{major}}.{{minor}}
            type=raw,value=latest

      - name: Create and push manifest
        run: |
          for tag in $(echo "${{ steps.meta.outputs.tags }}" | tr ',' '\n'); do
            docker buildx imagetools create -t "$tag" \
              "ghcr.io/${{ github.repository }}@${{ needs.docker-amd64.outputs.digest }}" \
              "ghcr.io/${{ github.repository }}@${{ needs.docker-arm64.outputs.digest }}"
          done


================================================
FILE: .gitignore
================================================
# Build output
/build/
/dist/
/internal/server/dist/

# Binary
/vget

# IDE
.idea/
*.swp
*.swo

# OS
.DS_Store
Thumbs.db

# Test output
coverage.out
*.test

# Temporary files
*.tmp
.vget-meta.json
/tmp/
/downloads/


================================================
FILE: .goreleaser.yaml
================================================
version: 2

project_name: vget

before:
  hooks:
    - go mod tidy

builds:
  - main: ./cmd/vget
    binary: vget
    env:
      - CGO_ENABLED=0
    ldflags:
      - -s -w
      - -X github.com/guiyumin/vget/internal/core/version.Version={{.Version}}
      - -X github.com/guiyumin/vget/internal/core/version.Commit={{.Commit}}
      - -X github.com/guiyumin/vget/internal/core/version.Date={{.Date}}
    goos:
      - darwin
      - linux
      - windows
    goarch:
      - amd64
      - arm64
    ignore:
      - goos: windows
        goarch: arm64

archives:
  - formats:
      - zip
    name_template: >-
      {{ .ProjectName }}_
      {{- .Version }}_
      {{- .Os }}_
      {{- .Arch }}
      {{- if .Arm }}v{{ .Arm }}{{ end }}
    files:
      - README.md
      - LICENSE*

checksum:
  name_template: "checksums.txt"
  algorithm: sha256

changelog:
  sort: asc
  filters:
    exclude:
      - "^docs:"
      - "^test:"
      - "^chore:"
      - "^ci:"
      - Merge pull request
      - Merge branch

release:
  github:
    owner: guiyumin
    name: vget
  draft: false
  prerelease: auto
  name_template: "v{{.Version}}"

brews:
  - repository:
      owner: guiyumin
      name: homebrew-tap
      token: "{{ .Env.TAP_GITHUB_TOKEN }}"
    homepage: "https://github.com/guiyumin/vget"
    description: "Media downloader CLI for various platforms"
    license: "Apache-2.0"
    directory: Formula
    commit_author:
      name: goreleaserbot
      email: bot@goreleaser.com
    commit_msg_template: "{{ .ProjectName }}: update to {{ .Tag }}"


================================================
FILE: .vscode/settings.json
================================================
{
  "makefile.configureOnOpen": false,
  "diffEditor.renderSideBySide": false,
  "diffEditor.hideUnchangedRegions.enabled": false,
  "gopls": {
    "build.env": {
      "CGO_ENABLED": "0"
    }
  }
}


================================================
FILE: CLAUDE.md
================================================
# CLAUDE.md

This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.

## Build Commands

```bash
# Build
go build ./cmd/vget

# Build to specific directory
go build -o build/vget ./cmd/vget

# Run directly
go run ./cmd/vget

# Build with version info (for releases)
go build -ldflags "-X github.com/guiyumin/vget/internal/version.Version=1.0.0" ./cmd/vget
```

## Architecture

vget is a media downloader CLI built with Go. It uses Cobra for command parsing and Bubbletea for interactive TUI elements (spinners, progress bars).

### Core Flow

1. **CLI Layer** (`internal/cli/`) - Cobra commands parse flags and dispatch to handlers
2. **Extractor Layer** (`internal/extractor/`) - URL matching and media metadata extraction
3. **Downloader Layer** (`internal/downloader/`) - HTTP download with Bubbletea progress TUI

### Media Types

The `MediaType` enum in `internal/extractor/extractor.go` defines supported media types:

- `MediaTypeVideo` - Video files (Twitter, YouTube, etc.)
- `MediaTypeAudio` - Audio files (podcasts)
- `MediaTypePDF` - PDF documents
- `MediaTypeEPUB` - EPUB ebooks
- `MediaTypeMOBI` - MOBI ebooks
- `MediaTypeAZW` - AZW ebooks
- `MediaTypeUnknown` - Fallback (treated as video)

Each type has specific terminal output formatting in `internal/cli/extract.go`.

### Extractor Pattern

To add support for a new site, implement the `Extractor` interface in `internal/extractor/`:

```go
type Extractor interface {
    Name() string
    Match(url string) bool
    Extract(url string) (*VideoInfo, error)
}
```

Set the appropriate `MediaType` in the returned `VideoInfo`:

```go
return &VideoInfo{
    ID:        "...",
    Title:     "...",
    MediaType: MediaTypeAudio, // or MediaTypeVideo, etc.
    Formats:   []Format{...},
}, nil
```

Extractors are auto-registered via `init()` functions. See `xiaoyuzhou.go` or `twitter.go` for examples.

### Commands

- `vget <url>` - Download media from URL
- `vget init` - Interactive config wizard (TUI)
- `vget update` - Self-update to latest version
- `vget search --podcast <query>` - Search Xiaoyuzhou podcasts
- `vget ls <remote>:<path>` - List WebDAV remote directory
- `vget config show` - Show current configuration
- `vget config set <key> <value>` - Set config value (non-interactive)
- `vget config get <key>` - Get config value
- `vget config webdav ...` - Manage WebDAV servers

### i18n

Translations are embedded YAML files in `internal/i18n/locales/`. Supported: en, zh, jp, kr, es, fr, de.

Access translations via `i18n.T(langCode)` which returns a `*Translations` struct with typed fields.

### Config

User config lives in `~/.config/vget/config.yml`. Two ways to configure:

1. **Interactive (TUI):** `vget init` - Bubbletea wizard for first-time setup
2. **Non-interactive:** `vget config set <key> <value>` - For scripting/Docker

Supported keys for `vget config set`:

- `language` - Language code (en, zh, jp, kr, es, fr, de)
- `output_dir` - Default download directory
- `format` - Preferred format (mp4, webm, best)
- `quality` - Default quality (1080p, 720p, best)
- `twitter.auth_token` - Twitter auth for NSFW content

**IMPORTANT:** Config is read fresh on every command execution (not cached at startup). This is intentional and MUST be preserved:

- Enables config changes without restart
- Critical for Docker UX (no container restart needed)
- Never change this behavior

### Xiaohongshu (XHS) Extractor

The XHS extractor (`internal/extractor/xiaohongshu.go`) uses browser automation:

- **Browser**: Rod's auto-downloaded Chromium (NOT system Chrome)
- **Binary location**: `~/.cache/rod/browser/`
- **User data**: `~/.config/vget/browser/` (persistent, shared by all extractors)
- **Stealth**: Uses `go-rod/stealth` for anti-bot detection

**Important**: Never use system Chrome profiles with browser automation - it can corrupt session data.

### Self-Update

`internal/updater/` uses go-selfupdate to fetch releases from GitHub (`guiyumin/vget`). Version is set in `internal/version/version.go`.

# My Rules

- **MUST** USE ./build AS THE BUILD OUTPUT DIRECTORY
- **MUST** USE CGO_ENABLED=0 when building vget cli locally
- **MUST NOT** RUN npm run dev in @ui
- **MUST NOT** VERIFY or TEST your work, since I will test and verify it


================================================
FILE: LICENSE
================================================
Copyright 2025 Yumin

Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at

    http://www.apache.org/licenses/LICENSE-2.0

Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

================================================
FILE: Makefile
================================================
.PHONY: build build-ui build-metal build-cuda build-nocgo build-whisper push version patch minor major

BUILD_DIR := ./build
VERSION_FILE := internal/core/version/version.go
UI_DIR := ./ui
SERVER_DIST := ./internal/server/dist

# Get current version from latest git tag (strips 'v' prefix)
CURRENT_VERSION := $(shell git describe --tags --abbrev=0 2>/dev/null | sed 's/^v//' || echo "0.0.0")

# Get whisper.cpp module path
WHISPER_PATH := $(shell go list -m -f '{{.Dir}}' github.com/ggerganov/whisper.cpp/bindings/go 2>/dev/null)

build-ui:
	cd $(UI_DIR) && npm install && npm run build
	rm -rf $(SERVER_DIST)/*
	cp -r $(UI_DIR)/dist/* $(SERVER_DIST)/

# Build whisper.cpp static library
build-whisper:
	@if [ -z "$(WHISPER_PATH)" ]; then \
		echo "Error: whisper.cpp module not found. Run 'go mod download' first."; \
		exit 1; \
	fi
	cd "$(WHISPER_PATH)" && make whisper
	@echo "whisper.cpp library built at $(WHISPER_PATH)/libwhisper.a"

# Standard build with CGO for whisper.cpp (CPU only)
# Requires: make build-whisper (run once)
build: build-ui
	CGO_ENABLED=1 \
	C_INCLUDE_PATH="$(WHISPER_PATH)" \
	LIBRARY_PATH="$(WHISPER_PATH)" \
	go build -o $(BUILD_DIR)/vget ./cmd/vget
	CGO_ENABLED=1 \
	C_INCLUDE_PATH="$(WHISPER_PATH)" \
	LIBRARY_PATH="$(WHISPER_PATH)" \
	go build -o $(BUILD_DIR)/vget-server ./cmd/vget-server

# macOS with Metal acceleration (Apple Silicon)
# Requires: WHISPER_METAL=1 make build-whisper (run once)
build-metal: build-ui
	CGO_ENABLED=1 \
	C_INCLUDE_PATH="$(WHISPER_PATH)" \
	LIBRARY_PATH="$(WHISPER_PATH)" \
	go build -tags metal -o $(BUILD_DIR)/vget ./cmd/vget
	CGO_ENABLED=1 \
	C_INCLUDE_PATH="$(WHISPER_PATH)" \
	LIBRARY_PATH="$(WHISPER_PATH)" \
	go build -tags metal -o $(BUILD_DIR)/vget-server ./cmd/vget-server

# Linux with CUDA acceleration (NVIDIA GPU)
# Requires: GGML_CUDA=1 make build-whisper (run once)
build-cuda: build-ui
	CGO_ENABLED=1 \
	C_INCLUDE_PATH="$(WHISPER_PATH)" \
	LIBRARY_PATH="$(WHISPER_PATH)" \
	CGO_CFLAGS="-I/usr/local/cuda/include" \
	CGO_LDFLAGS="-L/usr/local/cuda/lib64" \
	go build -tags cuda -o $(BUILD_DIR)/vget ./cmd/vget
	CGO_ENABLED=1 \
	C_INCLUDE_PATH="$(WHISPER_PATH)" \
	LIBRARY_PATH="$(WHISPER_PATH)" \
	CGO_CFLAGS="-I/usr/local/cuda/include" \
	CGO_LDFLAGS="-L/usr/local/cuda/lib64" \
	go build -tags cuda -o $(BUILD_DIR)/vget-server ./cmd/vget-server

# Build without CGO (uses embedded whisper.cpp binary)
build-nocgo: build-ui
	CGO_ENABLED=0 go build -o $(BUILD_DIR)/vget ./cmd/vget
	CGO_ENABLED=0 go build -o $(BUILD_DIR)/vget-server ./cmd/vget-server

push:
	git push origin main --tags

# Version bump: make version <patch|minor|major>
version:
	@if [ -z "$(filter patch minor major,$(MAKECMDGOALS))" ]; then \
		echo "Usage: make version <patch|minor|major>"; \
		echo "Current version: $(CURRENT_VERSION)"; \
		exit 1; \
	fi

patch minor major: version
	@TYPE=$@ && \
	echo "Current version: $(CURRENT_VERSION)" && \
	NEW_VERSION=$$(echo "$(CURRENT_VERSION)" | awk -F. -v type="$$TYPE" '{ \
		split($$3, parts, "-"); \
		patch = parts[1]; \
		if (index($$3, "-") > 0) { print $$1"."$$2"."patch } \
		else if (type == "major") { print $$1+1".0.0" } \
		else if (type == "minor") { print $$1"."$$2+1".0" } \
		else { print $$1"."$$2"."$$3+1 } \
	}') && \
	BUILD_DATE=$$(date -u +"%Y-%m-%d") && \
	echo "New version: $$NEW_VERSION" && \
	echo "Build date: $$BUILD_DATE" && \
	sed -i '' 's/Version = ".*"/Version = "'$$NEW_VERSION'"/' $(VERSION_FILE) && \
	sed -i '' 's/Date    = ".*"/Date    = "'$$BUILD_DATE'"/' $(VERSION_FILE) && \
	git add $(VERSION_FILE) && \
	git commit -m "chore: bump version to v$$NEW_VERSION" && \
	git tag "v$$NEW_VERSION" && \
	echo "Created tag v$$NEW_VERSION" && \
	echo "Run 'make push' to push changes and trigger release"


================================================
FILE: README.md
================================================
# vget

Versatile downloader for audio, video, podcasts, PDFs and more. Available as CLI and Docker

[简体中文](README_zh.md) | [日本語](README_jp.md) | [한국어](README_kr.md) | [Español](README_es.md) | [Français](README_fr.md) | [Deutsch](README_de.md)

## Installation

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

Download `vget-windows-amd64.zip` from [Releases](https://github.com/guiyumin/vget/releases/latest), extract it, and add to your PATH.

## Screenshots

### Download Progress

![Download Progress](screenshots/pikpak_download.png)

### Docker Server UI

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## Supported Sources

See [sites.md](sites.md) for the full list of supported sites.

## Commands

| Command                                | Description                              |
| -------------------------------------- | ---------------------------------------- |
| `vget [url]`                           | Download media (`-o`, `-q`, `--info`)    |
| `vget ls <remote>:<path>`              | List remote directory (`--json`)         |
| `vget init`                            | Interactive config wizard                |
| `vget update`                          | Self-update (use `sudo` on Mac/Linux)    |
| `vget search --podcast <query>`        | Search podcasts                          |
| `vget completion [shell]`              | Generate shell completion script         |
| `vget config show`                     | Show config                              |
| `vget config set <key> <value>`        | Set config value (non-interactive)       |
| `vget config get <key>`                | Get config value                         |
| `vget config path`                     | Show config file path                    |
| `vget config webdav list`              | List configured WebDAV servers           |
| `vget config webdav add <name>`        | Add a WebDAV server                      |
| `vget config webdav show <name>`       | Show server details                      |
| `vget config webdav delete <name>`     | Delete a server                          |
| `vget telegram login --import-desktop` | Import Telegram session from desktop app |

### Examples

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://www.xiaohongshu.com/explore/abc123  # XHS video/image
vget https://example.com/video -o my_video.mp4
vget --info https://example.com/video
vget search --podcast "tech news"
vget pikpak:/path/to/file.mp4              # WebDAV download
vget ls pikpak:/Movies                     # List remote directory
```

## Configuration

Config file location:

| OS          | Path                        |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

Run `vget init` to create the config file interactively, or create it manually:

```yaml
language: en # en, zh, jp, kr, es, fr, de
```

**Note:** Config is read fresh on every command. No restart required after changes (useful for Docker).

## Updating

To update vget to the latest version:

**macOS / Linux:**

```bash
sudo vget update
```

**Windows (run PowerShell as Administrator):**

```powershell
vget update
```

## Languages

vget supports multiple languages:

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## License

Apache License 2.0


================================================
FILE: README_de.md
================================================
# vget

Vielseitiger Downloader für Audio, Video, Podcasts, PDFs und mehr. Verfügbar als CLI und Docker.

[English](README.md) | [简体中文](README_zh.md) | [日本語](README_jp.md) | [한국어](README_kr.md) | [Español](README_es.md) | [Français](README_fr.md)

## Installation

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

Laden Sie `vget-windows-amd64.zip` von [Releases](https://github.com/guiyumin/vget/releases/latest) herunter, entpacken Sie es und fügen Sie es zum PATH hinzu.

## Screenshots

### Download-Fortschritt

![Download-Fortschritt](screenshots/pikpak_download.png)

### Docker Server-Benutzeroberfläche

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## Unterstützte Quellen

Siehe [sites.md](sites.md) für die vollständige Liste der unterstützten Seiten.

## Befehle

| Befehl                             | Beschreibung                          |
|------------------------------------|---------------------------------------|
| `vget [url]`                       | Medien herunterladen (`-o`, `-q`, `--info`) |
| `vget ls <remote>:<path>`          | Remote-Verzeichnis auflisten (`--json`) |
| `vget init`                        | Interaktiver Konfigurationsassistent  |
| `vget update`                      | Aktualisieren (`sudo` auf Mac/Linux)  |
| `vget search --podcast <query>`    | Podcasts suchen                       |
| `vget completion [shell]`          | Shell-Vervollständigung generieren    |
| `vget config show`                 | Konfiguration anzeigen                |
| `vget config set <key> <value>`    | Konfigurationswert setzen (nicht interaktiv) |
| `vget config get <key>`            | Konfigurationswert abrufen            |
| `vget config path`                 | Konfigurationsdateipfad anzeigen      |
| `vget config webdav list`          | Konfigurierte WebDAV-Server auflisten |
| `vget config webdav add <name>`    | WebDAV-Server hinzufügen              |
| `vget config webdav show <name>`   | Serverdetails anzeigen                |
| `vget config webdav delete <name>` | Server löschen                        |
| `vget telegram login --import-desktop` | Telegram-Sitzung von Desktop-App importieren |

### Beispiele

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://example.com/video -o mein_video.mp4
vget --info https://example.com/video
vget search --podcast "tech news"
vget pikpak:/path/to/file.mp4              # WebDAV-Download
vget ls pikpak:/Movies                     # Remote-Verzeichnis auflisten
```

## Konfiguration

Speicherort der Konfigurationsdatei:

| OS          | Pfad                        |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

Führen Sie `vget init` aus, um die Konfigurationsdatei interaktiv zu erstellen, oder erstellen Sie sie manuell:

```yaml
language: de # en, zh, jp, kr, es, fr, de
```

**Hinweis:** Die Konfiguration wird bei jedem Befehl neu gelesen. Kein Neustart nach Änderungen erforderlich (nützlich für Docker).

## Aktualisierung

Um vget auf die neueste Version zu aktualisieren:

**macOS / Linux:**
```bash
sudo vget update
```

**Windows (PowerShell als Administrator ausführen):**
```powershell
vget update
```

## Sprachen

vget unterstützt mehrere Sprachen:

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## Lizenz

Apache License 2.0


================================================
FILE: README_es.md
================================================
# vget

Descargador versátil para audio, video, podcasts, PDFs y más. Disponible como CLI y Docker.

[English](README.md) | [简体中文](README_zh.md) | [日本語](README_jp.md) | [한국어](README_kr.md) | [Français](README_fr.md) | [Deutsch](README_de.md)

## Instalación

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

Descarga `vget-windows-amd64.zip` desde [Releases](https://github.com/guiyumin/vget/releases/latest), extráelo y agrégalo al PATH.

## Capturas de pantalla

### Progreso de descarga

![Progreso de descarga](screenshots/pikpak_download.png)

### Interfaz del servidor Docker

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## Fuentes compatibles

Consulta [sites.md](sites.md) para la lista completa de sitios compatibles.

## Comandos

| Comando                            | Descripción                           |
|------------------------------------|---------------------------------------|
| `vget [url]`                       | Descargar medios (`-o`, `-q`, `--info`) |
| `vget ls <remote>:<path>`          | Listar directorio remoto (`--json`)   |
| `vget init`                        | Asistente de configuración interactivo |
| `vget update`                      | Actualizar (usar `sudo` en Mac/Linux) |
| `vget search --podcast <query>`    | Buscar podcasts                       |
| `vget completion [shell]`          | Generar script de autocompletado      |
| `vget config show`                 | Mostrar configuración                 |
| `vget config set <key> <value>`    | Establecer valor de config (no interactivo) |
| `vget config get <key>`            | Obtener valor de configuración        |
| `vget config path`                 | Mostrar ruta del archivo de config    |
| `vget config webdav list`          | Listar servidores WebDAV configurados |
| `vget config webdav add <name>`    | Agregar servidor WebDAV               |
| `vget config webdav show <name>`   | Mostrar detalles del servidor         |
| `vget config webdav delete <name>` | Eliminar servidor                     |
| `vget telegram login --import-desktop` | Importar sesión de Telegram desde la app de escritorio |

### Ejemplos

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://example.com/video -o mi_video.mp4
vget --info https://example.com/video
vget search --podcast "tech news"
vget pikpak:/path/to/file.mp4              # Descarga WebDAV
vget ls pikpak:/Movies                     # Listar directorio remoto
```

## Configuración

Ubicación del archivo de configuración:

| SO          | Ruta                        |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

Ejecuta `vget init` para crear el archivo de configuración interactivamente, o créalo manualmente:

```yaml
language: es # en, zh, jp, kr, es, fr, de
```

**Nota:** La configuración se lee en cada comando. No se requiere reinicio después de cambios (útil para Docker).

## Actualización

Para actualizar vget a la última versión:

**macOS / Linux:**
```bash
sudo vget update
```

**Windows (ejecutar PowerShell como Administrador):**
```powershell
vget update
```

## Idiomas

vget soporta múltiples idiomas:

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## Licencia

Apache License 2.0


================================================
FILE: README_fr.md
================================================
# vget

Téléchargeur polyvalent pour audio, vidéo, podcasts, PDFs et plus. Disponible en CLI et Docker.

[English](README.md) | [简体中文](README_zh.md) | [日本語](README_jp.md) | [한국어](README_kr.md) | [Español](README_es.md) | [Deutsch](README_de.md)

## Installation

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

Téléchargez `vget-windows-amd64.zip` depuis [Releases](https://github.com/guiyumin/vget/releases/latest), extrayez-le et ajoutez-le au PATH.

## Captures d'écran

### Progression du téléchargement

![Progression du téléchargement](screenshots/pikpak_download.png)

### Interface serveur Docker

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## Sources prises en charge

Consultez [sites.md](sites.md) pour la liste complète des sites pris en charge.

## Commandes

| Commande                           | Description                           |
|------------------------------------|---------------------------------------|
| `vget [url]`                       | Télécharger des médias (`-o`, `-q`, `--info`) |
| `vget ls <remote>:<path>`          | Lister un répertoire distant (`--json`) |
| `vget init`                        | Assistant de configuration interactif |
| `vget update`                      | Mise à jour (`sudo` sur Mac/Linux)    |
| `vget search --podcast <query>`    | Rechercher des podcasts               |
| `vget completion [shell]`          | Générer un script d'autocomplétion    |
| `vget config show`                 | Afficher la configuration             |
| `vget config set <key> <value>`    | Définir une valeur de config (non interactif) |
| `vget config get <key>`            | Obtenir une valeur de configuration   |
| `vget config path`                 | Afficher le chemin du fichier config  |
| `vget config webdav list`          | Lister les serveurs WebDAV configurés |
| `vget config webdav add <name>`    | Ajouter un serveur WebDAV             |
| `vget config webdav show <name>`   | Afficher les détails du serveur       |
| `vget config webdav delete <name>` | Supprimer un serveur                  |
| `vget telegram login --import-desktop` | Importer la session Telegram depuis l'app de bureau |

### Exemples

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://example.com/video -o ma_video.mp4
vget --info https://example.com/video
vget search --podcast "tech news"
vget pikpak:/path/to/file.mp4              # Téléchargement WebDAV
vget ls pikpak:/Movies                     # Lister un répertoire distant
```

## Configuration

Emplacement du fichier de configuration :

| OS          | Chemin                      |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

Exécutez `vget init` pour créer le fichier de configuration de manière interactive, ou créez-le manuellement :

```yaml
language: fr # en, zh, jp, kr, es, fr, de
```

**Note :** La configuration est lue à chaque commande. Pas de redémarrage nécessaire après modification (utile pour Docker).

## Mise à jour

Pour mettre à jour vget vers la dernière version :

**macOS / Linux :**
```bash
sudo vget update
```

**Windows (exécuter PowerShell en tant qu'Administrateur) :**
```powershell
vget update
```

## Langues

vget prend en charge plusieurs langues :

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## Licence

Apache License 2.0


================================================
FILE: README_jp.md
================================================
# vget

オーディオ、ビデオ、ポッドキャスト、PDFなどをダウンロードする多機能ツール。CLI と Docker で利用可能。

[English](README.md) | [简体中文](README_zh.md) | [한국어](README_kr.md) | [Español](README_es.md) | [Français](README_fr.md) | [Deutsch](README_de.md)

## インストール

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

[Releases](https://github.com/guiyumin/vget/releases/latest) から `vget-windows-amd64.zip` をダウンロードし、解凍して PATH に追加してください。

## スクリーンショット

### ダウンロード進捗

![ダウンロード進捗](screenshots/pikpak_download.png)

### Docker サーバー UI

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## 対応ソース

対応サイトの一覧は [sites.md](sites.md) をご覧ください。

## コマンド

| コマンド                           | 説明                                  |
|------------------------------------|---------------------------------------|
| `vget [url]`                       | メディアをダウンロード (`-o`, `-q`, `--info`) |
| `vget ls <remote>:<path>`          | リモートディレクトリを一覧表示 (`--json`) |
| `vget init`                        | 対話式設定ウィザード                  |
| `vget update`                      | 自動更新(Mac/Linux は `sudo` が必要)|
| `vget search --podcast <query>`    | ポッドキャスト検索                    |
| `vget completion [shell]`          | シェル補完スクリプトを生成            |
| `vget config show`                 | 設定を表示                            |
| `vget config set <key> <value>`    | 設定値を設定(非対話式)              |
| `vget config get <key>`            | 設定値を取得                          |
| `vget config path`                 | 設定ファイルのパスを表示              |
| `vget config webdav list`          | 設定済み WebDAV サーバー一覧          |
| `vget config webdav add <name>`    | WebDAV サーバーを追加                 |
| `vget config webdav show <name>`   | サーバー詳細を表示                    |
| `vget config webdav delete <name>` | サーバーを削除                        |
| `vget telegram login --import-desktop` | デスクトップアプリから Telegram セッションをインポート |

### 例

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://example.com/video -o my_video.mp4
vget --info https://example.com/video
vget search --podcast "tech news"
vget pikpak:/path/to/file.mp4              # WebDAV ダウンロード
vget ls pikpak:/Movies                     # リモートディレクトリを一覧表示
```

## 設定

設定ファイルの場所:

| OS          | パス                        |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

`vget init` で対話的に設定ファイルを作成するか、手動で作成してください:

```yaml
language: jp # en, zh, jp, kr, es, fr, de
```

**注意:** 設定はコマンド実行ごとに読み込まれます。変更後の再起動は不要です(Docker に便利)。

## 更新

vget を最新バージョンに更新:

**macOS / Linux:**
```bash
sudo vget update
```

**Windows(管理者として PowerShell を実行):**
```powershell
vget update
```

## 言語

vget は複数の言語をサポートしています:

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## ライセンス

Apache License 2.0


================================================
FILE: README_kr.md
================================================
# vget

오디오, 비디오, 팟캐스트, PDF 등을 다운로드하는 다목적 도구. CLI 및 Docker로 사용 가능.

[English](README.md) | [简体中文](README_zh.md) | [日本語](README_jp.md) | [Español](README_es.md) | [Français](README_fr.md) | [Deutsch](README_de.md)

## 설치

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

[Releases](https://github.com/guiyumin/vget/releases/latest)에서 `vget-windows-amd64.zip`을 다운로드하고 압축을 푼 후 PATH에 추가하세요.

## 스크린샷

### 다운로드 진행률

![다운로드 진행률](screenshots/pikpak_download.png)

### Docker 서버 UI

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## 지원 소스

지원 사이트 전체 목록은 [sites.md](sites.md)를 참조하세요.

## 명령어

| 명령어                             | 설명                                  |
|------------------------------------|---------------------------------------|
| `vget [url]`                       | 미디어 다운로드 (`-o`, `-q`, `--info`) |
| `vget ls <remote>:<path>`          | 원격 디렉토리 목록 (`--json`)         |
| `vget init`                        | 대화형 설정 마법사                    |
| `vget update`                      | 자동 업데이트 (Mac/Linux는 `sudo` 필요) |
| `vget search --podcast <query>`    | 팟캐스트 검색                         |
| `vget completion [shell]`          | 쉘 자동완성 스크립트 생성             |
| `vget config show`                 | 설정 표시                             |
| `vget config set <key> <value>`    | 설정 값 지정 (비대화형)               |
| `vget config get <key>`            | 설정 값 가져오기                      |
| `vget config path`                 | 설정 파일 경로 표시                   |
| `vget config webdav list`          | 설정된 WebDAV 서버 목록               |
| `vget config webdav add <name>`    | WebDAV 서버 추가                      |
| `vget config webdav show <name>`   | 서버 상세 정보 표시                   |
| `vget config webdav delete <name>` | 서버 삭제                             |
| `vget telegram login --import-desktop` | 데스크톱 앱에서 Telegram 세션 가져오기 |

### 예시

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://example.com/video -o my_video.mp4
vget --info https://example.com/video
vget search --podcast "tech news"
vget pikpak:/path/to/file.mp4              # WebDAV 다운로드
vget ls pikpak:/Movies                     # 원격 디렉토리 목록
```

## 설정

설정 파일 위치:

| OS          | 경로                        |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

`vget init`으로 대화형으로 설정 파일을 생성하거나 수동으로 생성하세요:

```yaml
language: kr # en, zh, jp, kr, es, fr, de
```

**참고:** 설정은 명령 실행 시마다 새로 읽습니다. 변경 후 재시작이 필요 없습니다 (Docker에 유용).

## 업데이트

vget을 최신 버전으로 업데이트:

**macOS / Linux:**
```bash
sudo vget update
```

**Windows (관리자 권한으로 PowerShell 실행):**
```powershell
vget update
```

## 언어

vget은 여러 언어를 지원합니다:

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## 라이선스

Apache License 2.0


================================================
FILE: README_zh.md
================================================
# vget

多功能下载工具,支持音频、视频、播客、PDF等。提供 CLI 和 Docker 两种方式。

[English](README.md) | [日本語](README_jp.md) | [한국어](README_kr.md) | [Español](README_es.md) | [Français](README_fr.md) | [Deutsch](README_de.md)

## 安装

### macOS

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-darwin-arm64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Linux / WSL

```bash
curl -fsSL https://github.com/guiyumin/vget/releases/latest/download/vget-linux-amd64.zip -o vget.zip
unzip vget.zip
sudo mv vget /usr/local/bin/
rm vget.zip
```

### Windows

从 [Releases](https://github.com/guiyumin/vget/releases/latest) 下载 `vget-windows-amd64.zip`,解压后添加到系统 PATH。

## 截图

### 下载进度

![下载进度](screenshots/pikpak_download.png)

### Docker 服务器界面

![](screenshots/vget_server_ui.png)

## Docker

```bash
docker run -d -p 8080:8080 -v ~/downloads:/home/vget/downloads ghcr.io/guiyumin/vget:latest
```

## 支持的来源

查看 [sites.md](sites.md) 获取完整的支持网站列表。

## 命令

| 命令                               | 描述                                  |
|------------------------------------|---------------------------------------|
| `vget [url]`                       | 下载媒体 (`-o`, `-q`, `--info`)       |
| `vget ls <remote>:<path>`          | 列出远程目录 (`--json`)               |
| `vget init`                        | 交互式配置向导                        |
| `vget update`                      | 自动更新(Mac/Linux 需使用 `sudo`)   |
| `vget search --podcast <query>`    | 搜索播客                              |
| `vget completion [shell]`          | 生成 shell 补全脚本                   |
| `vget config show`                 | 显示配置                              |
| `vget config set <key> <value>`    | 设置配置值(非交互式)                |
| `vget config get <key>`            | 获取配置值                            |
| `vget config path`                 | 显示配置文件路径                      |
| `vget config webdav list`          | 列出已配置的 WebDAV 服务器            |
| `vget config webdav add <name>`    | 添加 WebDAV 服务器                    |
| `vget config webdav show <name>`   | 显示服务器详情                        |
| `vget config webdav delete <name>` | 删除服务器                            |
| `vget telegram login --import-desktop` | 从桌面应用导入 Telegram 会话      |
| `vget kuaidi100 <单号>`            | 查询快递物流信息(需配置快递100 API) |

### 示例

```bash
vget https://twitter.com/user/status/123456789
vget https://www.xiaoyuzhoufm.com/episode/abc123
vget https://www.xiaohongshu.com/explore/abc123  # 小红书视频/图片
vget https://example.com/video -o my_video.mp4
vget --info https://example.com/video
vget search --podcast "科技"
vget pikpak:/path/to/file.mp4              # WebDAV 下载
vget ls pikpak:/Movies                     # 列出远程目录
```

## 配置

配置文件位置:

| 操作系统    | 路径                        |
| ----------- | --------------------------- |
| macOS/Linux | `~/.config/vget/config.yml` |
| Windows     | `%APPDATA%\vget\config.yml` |

运行 `vget init` 交互式创建配置文件,或手动创建:

```yaml
language: zh # en, zh, jp, kr, es, fr, de
```

**注意:** 配置文件在每次命令执行时重新读取,修改后无需重启(适用于 Docker)。

## 更新

将 vget 更新到最新版本:

**macOS / Linux:**
```bash
sudo vget update
```

**Windows(以管理员身份运行 PowerShell):**
```powershell
vget update
```

## 语言

vget 支持多种语言:

- English (en)
- 中文 (zh)
- 日本語 (jp)
- 한국어 (kr)
- Español (es)
- Français (fr)
- Deutsch (de)

## 代理 / 翻墙

如果你需要翻墙(绕过 GFW),推荐使用 Clash。

**Clash 有两种模式:**

1. **系统代理模式** - 设置系统级 HTTP/HTTPS 代理。支持系统代理的应用会自动使用。
2. **TUN 模式** - 创建虚拟网卡,在网络层捕获所有流量。

**推荐使用 TUN 模式**:开启后,所有应用的流量都会自动经过 Clash,无需任何配置。vget 会自动走代理,无需额外设置。

**如果使用系统代理模式**:Clash 会设置 `HTTP_PROXY` / `HTTPS_PROXY` 环境变量,vget 会自动读取并使用这些代理设置。

简而言之:**只要 Clash 正常运行,vget 就能正常工作**,无需在 vget 中配置代理。

## 许可证

Apache License 2.0


================================================
FILE: TODO.md
================================================
# TODO

## Tomorrow's Tasks

3. [x] kuaidi100 - Bring Your Own Key (API is expensive)

## Features

- [x] `vget init` command
  - Language preference
  - Default output directory
  - Default format/quality
- [x] Self update
- [x] m3u8 streaming support
- [x] Bulk download from txt file
  - Read URLs from txt file
  - Sequential or parallel processing
- [x] Format/quality selection (`-q` flag)
- [x] Audio extraction (podcasts)
- [ ] Resume interrupted downloads
- [ ] Retry on failure
- [x] Progress bar with speed/ETA
- [ ] Quiet/verbose modes
- [ ] Dry run mode
- [ ] More extractors (YouTube, TikTok, etc.)
- [ ] Playlist support
- [x] Concurrent downloads
- [ ] Rate limiting
- [x] Cookie/auth support
- [ ] Metadata embedding
  - Audio (MP3/M4A): ID3 tags - title, artist, album, cover art
  - Video (MP4): title, description, thumbnail
  - Auto-fill from source (podcast name, episode title, artwork)
  - Media players (Apple Music, VLC, etc.) would then show this info instead of just the filename.
- [x] `vget server` - HTTP server mode
  - REST API for remote downloads
  - Run as background daemon (`vget server start -d`)
  - Web UI for submitting URLs
  - systemd service installation (`vget server install`)
- [x] WebDAV client integration
  - Connect to PikPak, other WebDAV-compatible cloud storage
  - Download files from cloud (`vget <remote>:<path>`)
  - Browse and select files with TUI (`vget ls <remote>:<path>`)

## Extractors

- [x] Twitter/X
- [x] Xiaoyuzhou (小宇宙) podcasts
  - [x] Episode download
  - [x] Search (`vget search --podcast <query>`)
  - [x] Podcast listing (all episodes)
- [x] YouTube (Docker only, uses yt-dlp/youtube-dl)
- [ ] TikTok
- [x] Apple Podcasts
- [x] Xiaohongshu (小红书/RED)
  - Requires browser automation (Rod) + cookie auth
  - Reference: [xpzouying/xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp) (7.2k stars, stable 1+ year)
  - Extraction approach:
    - Navigate to `https://www.xiaohongshu.com/explore/{feedID}?xsec_token=...`
    - Extract `window.__INITIAL_STATE__.note.noteDetailMap` via JS
    - Parse JSON for images (`urlDefault`) and video URLs
  - Feasibility: Moderate effort, more achievable than Instagram
  - Note: yt-dlp also has extractor but frequently breaks due to bot detection

## Tracking (Versatile Get)

- [ ] FedEx tracking
  - [ ] Scraping (default, no setup)
  - [ ] API mode (user provides own keys in config.yml)
- [ ] UPS tracking
  - [ ] Scraping (default, no setup)
  - [ ] API mode (user provides own keys in config.yml)
- [ ] USPS tracking
  - [ ] Scraping (default, no setup)
  - [ ] API mode (user provides own keys in config.yml)
  - [ ] kuaidi100 - Bring Your Own Key (API is expensive)

## DevOps

- [x] GoReleaser + GitHub Actions for tagged releases
- [x] Dockerfile for NAS deployment
  - Multi-stage build for minimal image
  - Support for Synology/QNAP/TrueNAS
  - compose.yml with NAS path examples


================================================
FILE: cmd/vget/main.go
================================================
package main

import (
	"os"

	"github.com/guiyumin/vget/internal/cli"
)

func main() {
	if err := cli.Execute(); err != nil {
		os.Exit(1)
	}
}


================================================
FILE: cmd/vget-server/main.go
================================================
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"os"
	"os/signal"
	"path/filepath"
	"syscall"
	"time"

	"github.com/guiyumin/vget/internal/core/config"
	"github.com/guiyumin/vget/internal/core/version"
	"github.com/guiyumin/vget/internal/server"
)

func main() {
	// Command-line flags
	port := flag.Int("port", 0, "HTTP listen port (default: 8080)")
	output := flag.String("output", "", "output directory for downloads")
	showVersion := flag.Bool("version", false, "show version")
	flag.Parse()

	if *showVersion {
		fmt.Printf("vget-server %s\n", version.Version)
		return
	}

	// Load configuration
	cfg := config.LoadOrDefault()

	// Resolve port (flag > config > default)
	serverPort := *port
	if serverPort == 0 {
		if cfg.Server.Port > 0 {
			serverPort = cfg.Server.Port
		} else {
			serverPort = 8080
		}
	}

	// Resolve output directory (flag > config > default)
	outputDir := *output
	if outputDir == "" {
		if cfg.OutputDir != "" {
			outputDir = cfg.OutputDir
		} else {
			outputDir = config.DefaultDownloadDir()
		}
	}

	// Expand ~ in path
	if len(outputDir) >= 2 && outputDir[:2] == "~/" {
		home, _ := os.UserHomeDir()
		outputDir = filepath.Join(home, outputDir[2:])
	}

	// Resolve max concurrent (config > default)
	maxConcurrent := cfg.Server.MaxConcurrent
	if maxConcurrent <= 0 {
		maxConcurrent = 10
	}

	// Get API key from config
	apiKey := cfg.Server.APIKey

	// Create and start server
	srv := server.NewServer(serverPort, outputDir, apiKey, maxConcurrent)

	// Handle graceful shutdown
	sigChan := make(chan os.Signal, 1)
	signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

	go func() {
		<-sigChan
		log.Println("Shutting down server...")
		ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
		defer cancel()
		srv.Stop(ctx)
	}()

	log.Printf("Starting vget server on port %d", serverPort)
	log.Printf("Output directory: %s", outputDir)

	if err := srv.Start(); err != nil {
		log.Fatalf("Server error: %v", err)
	}
}


================================================
FILE: compose.yml
================================================
# vget Docker Compose
#
# Image variants (choose based on your system):
#   ghcr.io/guiyumin/vget:latest      - No models, downloads on first use (~200MB)
#   ghcr.io/guiyumin/vget:full-small  - Whisper Small bundled (~500MB)
#   ghcr.io/guiyumin/vget:full-medium - Whisper Medium bundled (~1.5GB)
#   ghcr.io/guiyumin/vget:full-large  - Whisper Large V3 Turbo bundled (~1.8GB)
#
# Whisper models support 99 languages including Chinese, Japanese, Korean.
#
# Configure via .env file or environment variables:
#   VGET_PORT=8080              # Web UI port
#   VGET_DOWNLOADS=./downloads  # Download directory
#   VGET_CONFIG=./config        # Config directory
#   TZ=Asia/Shanghai            # Timezone
#
# Example paths by NAS:
#   Synology:  /volume1/vget/downloads
#   QNAP:      /share/vget/downloads
#   Unraid:    /mnt/user/vget/downloads
#   TrueNAS:   /mnt/pool/vget/downloads

services:
  vget:
    # Change to :full-small, :full-medium, or :full-large based on your RAM/GPU
    image: ghcr.io/guiyumin/vget:latest
    container_name: vget
    restart: unless-stopped
    ports:
      - "${VGET_PORT:-8080}:8080"
    volumes:
      - ${VGET_DOWNLOADS:?Set VGET_DOWNLOADS in .env}:/home/vget/downloads
      - ${VGET_CONFIG:?Set VGET_CONFIG in .env}:/home/vget/.config/vget
    environment:
      - TZ=${TZ:-Asia/Shanghai}
    # Required for NAS systems (Synology, QNAP, etc.) to allow ffmpeg thread creation
    security_opt:
      - seccomp:unconfined


================================================
FILE: docker/vget/Dockerfile
================================================
# vget Docker Image

# Build stage for UI
FROM node:22-slim AS ui-builder
WORKDIR /app/ui
COPY ui/package*.json ./
RUN npm ci
COPY ui/ ./
RUN npm run build

# Go builder stage
FROM golang:1.25-bookworm AS go-builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
COPY --from=ui-builder /app/ui/dist ./internal/server/dist

RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /vget-server ./cmd/vget-server

# Runtime stage
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    chromium \
    fonts-noto-cjk \
    fonts-noto-color-emoji \
    python3 \
    python3-pip \
    python3-venv \
    ffmpeg \
    nodejs \
    gosu \
    curl \
    bzip2 \
    && rm -rf /var/lib/apt/lists/*

RUN pip3 install --no-cache-dir --break-system-packages \
    yt-dlp \
    youtube-dl

RUN (getent group 1000 >/dev/null || groupadd -g 1000 vget) && \
    (id -u 1000 >/dev/null 2>&1 || useradd -u 1000 -g 1000 -m -d /home/vget vget) && \
    mkdir -p /home/vget/downloads /home/vget/.config/vget && \
    chown -R 1000:1000 /home/vget

COPY --from=go-builder /vget-server /usr/local/bin/vget-server

ENV ROD_BROWSER=/usr/bin/chromium

COPY docker/vget/entrypoint.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /home/vget
EXPOSE 8080
VOLUME ["/home/vget/downloads", "/home/vget/.config/vget"]

ENTRYPOINT ["entrypoint.sh"]
CMD []


================================================
FILE: docker/vget/Dockerfile.arm64
================================================
# vget Docker Image for ARM64 (Apple Silicon, Raspberry Pi, ARM servers)

# Build stage for UI
FROM node:22-slim AS ui-builder
WORKDIR /app/ui
COPY ui/package*.json ./
RUN npm ci
COPY ui/ ./
RUN npm run build

# Go builder stage
FROM golang:1.25-bookworm AS go-builder

WORKDIR /app

COPY go.mod go.sum ./
RUN go mod download

COPY . .
COPY --from=ui-builder /app/ui/dist ./internal/server/dist

RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /vget-server ./cmd/vget-server

# Runtime stage
FROM debian:bookworm-slim

RUN apt-get update && apt-get install -y --no-install-recommends \
    ca-certificates \
    chromium \
    fonts-noto-cjk \
    fonts-noto-color-emoji \
    python3 \
    python3-pip \
    python3-venv \
    ffmpeg \
    nodejs \
    gosu \
    curl \
    bzip2 \
    && rm -rf /var/lib/apt/lists/*

RUN pip3 install --no-cache-dir --break-system-packages \
    yt-dlp \
    youtube-dl

RUN (getent group 1000 >/dev/null || groupadd -g 1000 vget) && \
    (id -u 1000 >/dev/null 2>&1 || useradd -u 1000 -g 1000 -m -d /home/vget vget) && \
    mkdir -p /home/vget/downloads /home/vget/.config/vget && \
    chown -R 1000:1000 /home/vget

COPY --from=go-builder /vget-server /usr/local/bin/vget-server

ENV ROD_BROWSER=/usr/bin/chromium

COPY docker/vget/entrypoint-arm64.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

WORKDIR /home/vget
EXPOSE 8080
VOLUME ["/home/vget/downloads", "/home/vget/.config/vget"]

ENTRYPOINT ["entrypoint.sh"]
CMD []


================================================
FILE: docker/vget/entrypoint-arm64.sh
================================================
#!/bin/bash
set -e

# Fix ownership of mounted volumes if running as root
if [ "$(id -u)" = "0" ]; then
    chown -R 1000:1000 /home/vget/downloads /home/vget/.config/vget
    exec gosu 1000:1000 vget-server "$@"
else
    exec vget-server "$@"
fi


================================================
FILE: docker/vget/entrypoint.sh
================================================
#!/bin/bash
set -e

# Fix ownership of mounted volumes if running as root
if [ "$(id -u)" = "0" ]; then
    chown -R 1000:1000 /home/vget/downloads /home/vget/.config/vget
    exec gosu 1000:1000 vget-server "$@"
else
    exec vget-server "$@"
fi


================================================
FILE: docs/FAQs.md
================================================
# FAQs & Troubleshooting

## FFmpeg Merge Failed: thread_create failed

**Error message:**

```
ffmpeg merge failed: thread_create failed: Operation not permitted.
Try to increase 'ulimit -v' or decrease 'ulimit -s'.
```

**Cause:** This is a system resource limitation, not a vget bug. FFmpeg cannot create threads due to OS-level restrictions on your system.

**Common scenarios:**

- Running in a Docker container with restricted resources
- VPS or shared hosting with strict ulimit settings
- Systems with low thread/memory limits

**Solutions:**

### If running in Docker

Add ulimit settings to your container:

**compose.yml:**

```yaml
services:
  vget:
    image: your-vget-image
    ulimits:
      nproc: 65535
      nofile:
        soft: 65535
        hard: 65535
```

**docker run:**

```bash
docker run --ulimit nproc=65535 --ulimit nofile=65535:65535 your-vget-image
```

### If running on bare Linux

Adjust ulimit settings before running vget:

```bash
# Reduce stack size
ulimit -s 8192

# Or increase virtual memory limit
ulimit -v unlimited
```

To make changes permanent, edit `/etc/security/limits.conf`:

```
*    soft    nproc     65535
*    hard    nproc     65535
*    soft    nofile    65535
*    hard    nofile    65535
```

### Alternative workaround

If you cannot change system limits, download video and audio separately without merging (if supported by the source).


================================================
FILE: docs/PRD.md
================================================
# vget – Product Requirement Document (PRD)

**Version:** 1.2
**Author:** Yumin
**Language:** Golang
**UI:** Bubble Tea (TUI)
**Purpose:** A modern, multi-source video downloader with elegant CLI & TUI.

---

## 1. Product Vision & Core Positioning

### One-Line Vision

**vget:** A modern, minimalist, high-speed video downloader that works like wget, with a beautiful Bubble Tea TUI. Starting with X/Twitter, expanding to more platforms.

### Core Philosophy

vget's core value is not "protocol-level innovation", but rather:

- **Ultimate user experience** - Simple CLI, beautiful TUI
- **Single binary distribution** - No Python/Node dependencies
- **Clean architecture** - Extensible extractor system
- **Modern developer experience** - Golang + Bubble Tea + Worker Pool

### Why Not Just Use yt-dlp?

| Aspect       | yt-dlp       | vget                  |
| ------------ | ------------ | --------------------- |
| Installation | Python + pip | Single binary         |
| UI           | CLI only     | CLI + Bubble Tea TUI  |
| Complexity   | 500+ flags   | Minimal, opinionated  |
| Focus        | 1000+ sites  | Quality over quantity |

vget aims to be the "modern wget for videos" - simple, fast, beautiful.

---

## 2. Product Goals

### 2.1 MVP Goals (v0.1 - Twitter Focus)

**Target:** Working Twitter/X video downloader

- [x] Project structure setup
- [x] Twitter/X extractor (native Go, no yt-dlp dependency) ✅
  - Bearer token + guest token authentication
  - Tweet API parsing
  - Video variant extraction (multiple qualities)
- [ ] Direct MP4 downloader with progress bar
- [ ] HLS (.m3u8) support (Twitter uses this for some videos)
- [x] Simple CLI: `vget <twitter-url>` ✅
- [x] Auto-select best quality ✅
- [ ] Basic retry on failure

### 2.2 v0.2 Goals

- [x] Multi-threaded segmented downloads (range requests) ✅ **Implemented**
- [x] Output filename customization (`-o`) ✅

### 2.3 v0.3 Goals

- Bubble Tea TUI (`vget --ui`)
- More platform extractors (based on demand)
- Optional yt-dlp bridge for unsupported sites

---

## 3. User Experience (UX) Goals

### CLI Minimalism

```bash
vget https://example.com/video
```

### TUI Mode (Bubble Tea)

```bash
vget --ui URL
```

### Display Features

- Per-thread speed
- Total speed
- ETA
- Progress bar
- Task queue
- Pause/Resume capability
- Download history

### Automatic Content Type Detection

```
URL → Extractor → (MP4 / HLS / DASH / Playlist)
```

**Fully automatic:** Users don't need to think about the underlying protocol.

---

## 4. Feature Specification

### 4.1 Downloader Engine (Core)

| Feature               | Description                                           | Status         |
| --------------------- | ----------------------------------------------------- | -------------- |
| Multi-Stream Download | HTTP Range requests with parallel streams (default 8) | ✅ Implemented |
| Concurrent Download   | goroutine + worker pool pattern                       | ✅ Implemented |
| Chunk-based Transfer  | 16MB chunks with 128KB buffers per stream             | ✅ Implemented |
| Progress Display      | Real-time speed, ETA, elapsed time, avg speed         | ✅ Implemented |
| Auto Retry            | Exponential backoff retry (5 retries per chunk)       | ✅ Implemented |
| File Merge            | Merge multiple segments into MP4                      | Planned        |
| Verification          | Support md5/sha256 (optional)                         | Planned        |
| Speed Limit           | Throttle mode (optional)                              | Planned        |
| Download Queue        | Multiple simultaneous tasks                           | Planned        |

#### Multi-Stream Download Architecture (Implemented)

```
┌─────────────────────────────────────────────────────────────┐
│                    MultiStreamConfig                         │
│  Streams: 8 (parallel connections)                          │
│  ChunkSize: 16MB (per chunk)                                │
│  BufferSize: 128KB (per stream read buffer)                 │
└─────────────────────────────────────────────────────────────┘
                              │
                              ▼
┌─────────────────────────────────────────────────────────────┐
│                 HEAD Request (Check Support)                 │
│  - Get Content-Length                                       │
│  - Check Accept-Ranges: bytes                               │
└─────────────────────────────────────────────────────────────┘
                              │
              ┌───────────────┴───────────────┐
              ▼                               ▼
    [Range Supported]                [Range Not Supported]
              │                               │
              ▼                               ▼
┌─────────────────────────┐       ┌─────────────────────────┐
│   Calculate Chunks      │       │  Single-Stream Fallback │
│   File ÷ ChunkSize      │       │  (128KB buffer)         │
└─────────────────────────┘       └─────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────────────┐
│                    Worker Pool (8 workers)                   │
│  ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐   │
│  │ W1  │ │ W2  │ │ W3  │ │ W4  │ │ W5  │ │ W6  │ │ W7  │...│
│  └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘ └──┬──┘   │
│     │       │       │       │       │       │       │       │
│     ▼       ▼       ▼       ▼       ▼       ▼       ▼       │
│  Range:  Range:  Range:  Range:  Range:  Range:  Range:     │
│  0-16M   16M-32M 32M-48M ...                                │
└─────────────────────────────────────────────────────────────┘
              │
              ▼
┌─────────────────────────────────────────────────────────────┐
│              file.WriteAt(data, offset)                      │
│              (Thread-safe positional writes)                 │
└─────────────────────────────────────────────────────────────┘
```

**Performance Comparison:**

| Metric         | Before (Single Stream) | After (Multi-Stream)     |
| -------------- | ---------------------- | ------------------------ |
| Streams        | 1                      | 8 (configurable)         |
| Buffer         | 32KB                   | 128KB per stream         |
| Typical Speed  | ~10-20 MB/s            | ~50-80 MB/s              |
| WebDAV Support | Basic                  | Full with Range requests |

### 4.2 Extractor Layer (URL Parsing)

#### Extractor Interface

```go
type Extractor interface {
    // Match returns true if this extractor can handle the URL
    Match(url string) bool
    // Extract returns video info (title, formats, etc.)
    Extract(url string) (*VideoInfo, error)
}

type VideoInfo struct {
    ID       string
    Title    string
    Formats  []Format  // Multiple qualities available
    Duration int
}

type Format struct {
    URL      string
    Quality  string    // "1080p", "720p", etc.
    Ext      string    // "mp4", "m3u8"
    Width    int
    Height   int
    Bitrate  int
}
```

#### Supported Extractors

| Extractor     | Status | Notes                    |
| ------------- | ------ | ------------------------ |
| Twitter/X     | MVP    | Native Go implementation |
| Direct MP4    | MVP    | Content-Type detection   |
| HLS           | MVP    | m3u8 parsing             |
| DASH          | v0.2   | mpd XML parsing          |
| yt-dlp bridge | v0.3   | Optional fallback        |

#### Twitter/X Extractor Details

```
URL: https://x.com/user/status/123456789
           ↓
    Extract tweet ID
           ↓
    Get guest token (POST /1.1/guest/activate.json)
           ↓
    Fetch tweet (GET /1.1/statuses/show/{id}.json)
           ↓
    Parse extended_entities.media[].video_info.variants
           ↓
    Return VideoInfo with all quality options
```

### 4.3 CLI Specification

```bash
# Basic download
vget <url>

# Specify quality
vget -q 1080p <url>

# Segment thread count
vget -t 32 <url>

# Output filename
vget -o out.mp4 <url>

# Cookie
vget --cookies cookies.txt <url>

# Custom headers
vget -H "Referer: https://xxx" <url>

# Parse only, don't download
vget --info <url>

# Configuration management
vget init                           # Interactive config wizard (TUI)
vget config show                    # Show current config
vget config set language en         # Set config value (non-interactive)
vget config get language            # Get config value
```

### 4.4 TUI (Bubble Tea) Design

#### Components

- Header (speed, ETA)
- Global progress bar
- Per-thread speed bars
- Error messages
- Undo/Pause/Resume controls
- Log window
- Task queue

#### Keyboard Shortcuts

| Key     | Function     |
| ------- | ------------ |
| `space` | Pause/Resume |
| `p`     | Pause        |
| `r`     | Retry        |
| `q`     | Quit         |
| `↑↓`    | Switch tasks |

#### TUI Aesthetic

- lipgloss + Nord theme
- Clean and minimalist
- Style similar to glow, gh-dash, gum

---

## 5. Architecture Design

```
/cmd/vget
    main.go              # Entry point, CLI parsing
/internal
    /cli
        root.go          # Main command & WebDAV download handler
        config.go        # Config management commands
        extract.go       # Extraction with spinner
        ls.go            # Directory listing command
        search.go        # Search command
        completion.go    # Shell completion
    /extractor
        extractor.go     # Extractor interface & media types
        twitter.go       # Twitter/X extractor
        xiaoyuzhou.go    # Xiaoyuzhou podcast extractor
        instagram.go     # Instagram extractor
        tiktok.go        # TikTok extractor
        xiaohongshu.go   # Xiaohongshu extractor
        registry.go      # Extractor registration & matching
    /downloader
        downloader.go    # Download interface
        progress.go      # Progress tracking & Bubble Tea TUI
        multistream.go   # Multi-stream parallel downloader ✅ NEW
        utils.go         # Helper functions
    /webdav
        client.go        # WebDAV client with Range request support ✅ NEW
    /config
        config.go        # User configuration & WebDAV servers
                         # IMPORTANT: Config is read fresh per-command (no restart needed)
    /i18n
        i18n.go          # Internationalization
        /locales/*.yml   # Translation files (en, zh, jp, kr, es, fr, de)
    /updater
        updater.go       # Self-update functionality
    /version
        version.go       # Version info
```

---

## 6. Technical Implementation Details

### 6.1 Extractor Logic

**Pseudocode:**

```
if url endsWith .mp4 → MP4Extractor
if content-type == application/vnd.apple.mpegurl → HLSExtractor
if content-type == application/dash+xml → DASHExtractor
if URL contains "playlist" → PlaylistExtractor
```

#### HLS Flow

1. Download m3u8
2. Find master playlist
3. Select highest bitrate
4. Parse TS segments
5. Build task list in order

#### DASH Flow

1. Download mpd XML
2. Extract mediaBaseURL + segmentTemplate
3. Select a Representation
4. Generate task list for all segments

### 6.2 Downloader Engine (Implemented)

**Multi-Stream Configuration:**

```go
type MultiStreamConfig struct {
    Streams    int   // Number of parallel streams (default 8)
    ChunkSize  int64 // Size of each chunk (default 16MB)
    BufferSize int   // Buffer size per stream (default 128KB)
}
```

**Worker Pool Pattern:**

```go
// Create chunk channel and feed all chunks
chunkChan := make(chan chunk, len(chunks))
for _, c := range chunks {
    chunkChan <- c
}
close(chunkChan)

// Start N worker goroutines
for i := 0; i < config.Streams; i++ {
    go func() {
        for c := range chunkChan {
            downloadChunk(ctx, client, url, file, c, state)
        }
    }()
}
```

**Chunk Download with Range Requests:**

```go
req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", chunk.start, chunk.end))
// ...
file.WriteAt(data, offset)  // Thread-safe positional write
```

**Progress Tracking:**

```go
type downloadState struct {
    current     int64       // Atomic counter across all streams
    total       int64
    speed       float64     // Real-time speed
    startTime   time.Time
    endTime     time.Time
    finalSpeed  float64     // Average speed at completion
}
```

### 6.3 WebDAV Support (Implemented)

**Features:**

- Remote path syntax: `vget pikpak:/path/to/file.mp4`
- Full URL syntax: `vget webdav://user:pass@host/path`
- Multi-stream parallel downloads with HTTP Range requests
- Automatic fallback to single-stream if Range not supported
- Directory listing: `vget ls pikpak:/movies`

**WebDAV Client Architecture:**

```go
type Client struct {
    client   *webdav.Client  // go-webdav for PROPFIND/etc
    baseURL  string
    username string
    password string
}

// Methods
func (c *Client) Stat(ctx, path) (*FileInfo, error)
func (c *Client) List(ctx, path) ([]FileInfo, error)
func (c *Client) Open(ctx, path) (io.ReadCloser, int64, error)
func (c *Client) GetFileURL(path) string      // For Range requests
func (c *Client) GetAuthHeader() string       // Basic Auth header
func (c *Client) SupportsRangeRequests(ctx, path) (bool, error)
```

**Download Flow:**

```
pikpak:/movies/video.mp4
        │
        ▼
┌─────────────────────┐
│  Load config.yml    │
│  Get server creds   │
└─────────────────────┘
        │
        ▼
┌─────────────────────┐
│  client.Stat()      │
│  Get file size      │
└─────────────────────┘
        │
        ▼
┌─────────────────────┐
│  HEAD request       │
│  Check Range support│
└─────────────────────┘
        │
        ▼
┌─────────────────────┐
│  Multi-stream DL    │
│  8 parallel streams │
└─────────────────────┘
```

### 6.4 Merge (mp4 / ts / m4s)

**HLS:**

```bash
cat part*.ts | ffmpeg -i - -c copy out.mp4
```

**DASH:**

- mp4box or pure Go mux (can be supported after v1)

---

## 7. Future Roadmap

### TODO

- **Optimize download speed for WebDAV/PikPak** - Current multi-stream implementation is significantly slower than rclone. Target: 30MB/s for PikPak. Investigate:
  - Connection reuse / keep-alive
  - Chunk size tuning
  - Number of parallel streams
  - Buffer sizes
  - TCP tuning

### v1 (MVP)

- MP4 / HLS / DASH download
- CLI
- TUI
- Multi-threaded segmentation
- Resume support
- Auto quality detection

### v1.5

- Multi-task queue
- History records
- Graceful pause/resume

### v2

- Plugin system (extractor plugins)
- `.vget/plugins/*.wasm` for custom site loaders

### v3

- Distributed downloading
- Integration with S3 / OSS / R2
- Become a true "media download platform"

---

## 8. Success Metrics

| Metric         | Target                                 |
| -------------- | -------------------------------------- |
| GitHub Stars   | 1,000 (first month) / 5,000 (6 months) |
| CLI Installs   | 5K+                                    |
| TUI Open Rate  | > 40%                                  |
| Issue Feedback | > 20 (community engagement)            |
| Pull Requests  | At least 5 external contributors       |

---

## 9. Top Selling Points (Highlight in README)

- **Modern video downloader**
- **Fast, concurrent, resumable**
- **HLS & DASH built-in**
- **Beautiful Bubble Tea TUI**
- **Cross-platform single binary**
- **Plugin ecosystem (future)**

---

## 10. README Sample

```
vget
----
A modern, blazing-fast video downloader for the command line.
Supports MP4, HLS (m3u8), DASH (mpd), multi-thread downloads,
resume, cookies, proxies, and a beautiful Bubble Tea-powered TUI.

Usage:
  vget <url>            # auto detect and download
  vget --ui <url>       # open interactive TUI
  vget -t 32 <url>      # 32-thread segmented download
  vget -q 1080p <url>   # choose quality (HLS/DASH)
  vget --cookies c.txt  # cookie support
```


================================================
FILE: docs/YOUTUBE_NOTES.md
================================================
# YouTube Support Notes

## Status: Delegated to yt-dlp (Docker Only)

After extensive research and failed attempts, we've concluded that building a native Go YouTube extractor is not viable.

## What We Tried (2025-12-04)

### The Go Implementation Worked... Briefly

- Browser automation (Rod + stealth) captured BotGuard tokens
- Innertube API with iOS client returned unencrypted stream URLs (no cipher)
- Separate video/audio streams downloaded and merged with ffmpeg

### Then It Broke

1. **BotGuard Detection** - YouTube's anti-bot (Error 153) detected rod/stealth automation
2. **IP Binding** - Stream URLs are bound to the requesting IP; VPNs/IPv6 cause 403s
3. **Rate Limiting** - Heavy testing flagged our IP/session; even new IPs didn't help
4. **Constant Changes** - YouTube updates anti-bot weekly; we can't keep up

## Why We Don't Build Our Own Extractor

### The Problems Are Real

1. **Aggressive Anti-Bot Detection**
   - PO Tokens (Proof of Origin) require JavaScript execution
   - N parameter challenge requires solving obfuscated JS functions
   - SAPISID hash authentication with rotating signatures
   - Client version checks that change frequently
   - Rate limiting that bans IPs quickly

2. **Constantly Moving Target**
   - YouTube updates their anti-bot mechanisms weekly
   - yt-dlp has 1000+ contributors constantly reverse-engineering changes
   - A solo developer cannot keep up with Google's anti-bot team

3. **IP Bans Are Inevitable**
   - Even with all the right tokens and signatures, YouTube rate-limits aggressively
   - Residential IPs get banned after moderate usage
   - Datacenter IPs are blocked almost immediately

4. **Resource Requirements**
   - Requires JavaScript runtime (Node.js/Deno) for challenge solving
   - Needs rotating residential proxies ($$$)
   - Cookie/session management is complex

## Our Solution

We delegate YouTube extraction to **yt-dlp** and **youtube-dl**, but only in Docker:

- **In Docker**: vget shells out to yt-dlp/youtube-dl
- **Outside Docker**: vget shows an error suggesting Docker usage

### Why Docker Only?

1. Windows/Mac users won't have Python installed
2. Bundling yt-dlp in the Go binary is impractical
3. Docker image includes all dependencies (Python, ffmpeg, Node.js)
4. NAS users (Synology, QNAP, Unraid) commonly use Docker

## User Responsibilities

**IMPORTANT**: Users must provide their own infrastructure:

1. **Residential Proxy / Rotating IPs** - YouTube will ban datacenter IPs and rate-limit residential IPs. Users need to configure their own proxy solution. **This is not optional for sustained usage.**

2. **Cookies (Optional)** - For age-restricted or premium content, users can mount a cookies file.

3. **Rate Limiting** - Users should use `--sleep-interval` with yt-dlp to avoid bans.

## Usage

```bash
# Basic usage (user handles proxy externally)
docker run -v ~/downloads:/downloads guiyumin/vget "https://youtube.com/watch?v=xxx"

# With proxy configured in environment
docker run -e HTTP_PROXY=http://proxy:port -v ~/downloads:/downloads guiyumin/vget "https://youtube.com/watch?v=xxx"

# With cookies file for premium/age-restricted content
docker run -v ~/downloads:/downloads -v ~/cookies.txt:/home/vget/cookies.txt guiyumin/vget "https://youtube.com/watch?v=xxx"
```

## Alternatives for Users

If Docker isn't an option, users should use yt-dlp directly:

```bash
# Install yt-dlp
pip install yt-dlp

# Download video
yt-dlp "https://youtube.com/watch?v=xxx"

# With proxy
yt-dlp --proxy http://proxy:port "https://youtube.com/watch?v=xxx"
```

## Old Troubleshooting (For Reference)

These were issues with our native Go implementation:

### 403 on download
1. Clear browser profile: `rm -rf ~/.config/vget/browser/`
2. Disable IPv6: `sudo networksetup -setv6off Wi-Fi`
3. Try a different network/IP
4. Wait for rate limiting to expire (24-48 hours)

### No POToken captured
- YouTube detecting automation (Error 153)
- go-rod/stealth needs constant updates

### IP mismatch
- VPN must tunnel ALL traffic (not just browser)
- Disable IPv6 to force IPv4
- Browser, API call, and download must use same IP

## References

- [yt-dlp GitHub](https://github.com/yt-dlp/yt-dlp)
- [youtube-dl GitHub](https://github.com/ytdl-org/youtube-dl)
- [yt-dlp Wiki: Rate Limiting](https://github.com/yt-dlp/yt-dlp/wiki/Extractors#this-content-isnt-available-try-again-later)

## Lessons Learned

1. Don't fight Google's anti-bot team alone
2. Leverage existing open-source solutions (yt-dlp has 1000+ contributors)
3. Make infrastructure (proxies, IPs) the user's responsibility
4. Docker is the right abstraction for complex dependencies
5. Know when to give up and delegate


================================================
FILE: docs/bilibili-port-plan.md
================================================
# BBDown Go Port Plan

This document outlines the plan for porting [BBDown](https://github.com/nilaoda/BBDown) (a C# Bilibili downloader) to Go, integrating it into vget.

## Overview

**BBDown** is a comprehensive Bilibili downloader with ~6,500 lines of C# code. Key capabilities:

- Download videos, anime (Bangumi), courses (Cheese), playlists
- Multiple quality levels (144P to 8K) and codecs (AVC, HEVC, AV1)
- Audio formats: AAC, FLAC, Dolby Atmos, E-AC-3
- Authentication via QR code or cookie/token
- Subtitles, danmaku (bullet comments), cover images
- FFmpeg/MP4Box muxing integration

## Architecture Comparison

### BBDown (C#)

```
BBDown/                          # CLI Application
├── Program.cs                   # Entry point (897 lines)
├── CommandLineInvoker.cs        # CLI parsing
├── BBDownUtil.cs                # URL parsing utilities
├── BBDownDownloadUtil.cs        # HTTP download logic
├── BBDownMuxer.cs               # Audio/video muxing
├── BBDownLoginUtil.cs           # QR code login
└── Model/                       # Data models

BBDown.Core/                     # Core library
├── Parser.cs                    # API response parsing (467 lines)
├── AppHelper.cs                 # gRPC/Protobuf for APP API
├── Config.cs                    # Global configuration
├── FetcherFactory.cs            # Content type routing
├── IFetcher.cs                  # Fetcher interface
├── Entity/                      # Data models
├── Fetcher/                     # Content type handlers
│   ├── NormalInfoFetcher.cs     # Regular videos
│   ├── BangumiInfoFetcher.cs    # Anime
│   ├── CheeseInfoFetcher.cs     # Courses
│   └── ...                      # Others
└── Util/                        # HTTP, subtitles, danmaku
```

### vget Target (Go)

```
internal/extractor/
├── bilibili.go                  # Main extractor + interface
├── bilibili_api.go              # API client (WEB/TV/APP/INTL)
├── bilibili_parser.go           # Stream parsing
├── bilibili_auth.go             # Authentication (QR, cookie)
├── bilibili_fetcher.go          # Fetcher interface + factory
├── bilibili_fetcher_normal.go   # Regular videos
├── bilibili_fetcher_bangumi.go  # Anime
├── bilibili_fetcher_cheese.go   # Courses
├── bilibili_fetcher_space.go    # User uploads
├── bilibili_fetcher_list.go     # Playlists/collections
├── bilibili_subtitle.go         # Subtitle processing
├── bilibili_danmaku.go          # Bullet comments
└── bilibili_proto/              # Generated protobuf (for APP API)
```

## Bilibili API Structure

BBDown supports 4 different APIs for accessing content:

| API  | Endpoint             | Auth Method         | Use Case              |
| ---- | -------------------- | ------------------- | --------------------- |
| WEB  | api.bilibili.com     | Cookie (SESSDATA)   | Standard access       |
| TV   | api.snm0516.aisee.tv | App key + signature | Unrestricted streams  |
| APP  | grpc.biliapi.net     | gRPC + Protobuf     | FLAC, Dolby, 8K       |
| INTL | api.biliintl.com     | Similar to WEB      | International content |

### WBI Signature (WEB API)

Bilibili uses a dynamic signature scheme called WBI:

1. Extract `img_key` and `sub_key` from website HTML
2. Combine and reorder using a fixed mapping table
3. Generate MD5 signature of parameters + key
4. Keys rotate periodically (need to refresh)

### APP API (gRPC)

The APP API uses Protocol Buffers with custom headers:

- Device info (Dalvik, Android version)
- Access key authentication
- Protobuf request/response encoding

## Implementation Phases

### Phase 1: Foundation (Priority: High)

**Goal**: Basic video download for regular Bilibili videos

#### 1.1 Data Models

```go
// internal/extractor/bilibili.go

type BilibiliVideoInfo struct {
    AID       int64    // av number
    BVID      string   // BV number
    CID       int64    // cid (for video stream)
    Title     string
    Desc      string
    Pic       string   // cover URL
    Duration  int64
    Pages     []Page   // multi-part videos
}

type Page struct {
    CID       int64
    Page      int
    Title     string
    Duration  int64
}

type VideoStream struct {
    Quality   int      // 127=8K, 120=4K, 116=1080P60, etc.
    Codec     string   // avc, hevc, av1
    URL       string
    Bandwidth int64
    Width     int
    Height    int
}

type AudioStream struct {
    Quality   int      // 30280=320kbps, 30232=128kbps, 30216=64kbps
    Codec     string   // mp4a, flac, ec-3
    URL       string
    Bandwidth int64
}
```

#### 1.2 URL Pattern Matching

```go
// Match patterns:
// - https://www.bilibili.com/video/BV1xx411c7mD
// - https://www.bilibili.com/video/av170001
// - https://b23.tv/BV1xx411c7mD (short URL)
// - bilibili://video/170001

func (e *BilibiliExtractor) Match(url string) bool {
    patterns := []string{
        `bilibili\.com/video/(BV[\w]+|av\d+)`,
        `b23\.tv/(BV[\w]+|av\d+|\w+)`,
        `bilibili://video/\d+`,
    }
    // ...
}
```

#### 1.3 BV/AV ID Conversion

```go
// AV to BV and vice versa (algorithm from BBDown)
// BV is base58-like encoding of AV number

const table = "fZodR9XQDSUm21yCkr6zBqiveYah8bt4xsWpHnJE7jL5VG3guMTKNPAwcF"
var s = []int{11, 10, 3, 8, 4, 6}
const xor = 177451812
const add = 8728348608

func BV2AV(bv string) int64 { ... }
func AV2BV(av int64) string { ... }
```

#### 1.4 WEB API Client

```go
// internal/extractor/bilibili_api.go

type BilibiliClient struct {
    httpClient *http.Client
    cookie     string
    wbi        *WBIKeys  // signature keys
}

func (c *BilibiliClient) GetVideoInfo(bvid string) (*BilibiliVideoInfo, error) {
    // GET https://api.bilibili.com/x/web-interface/view?bvid=xxx
}

func (c *BilibiliClient) GetPlayURL(bvid string, cid int64, qn int) (*PlayURLResponse, error) {
    // GET https://api.bilibili.com/x/player/wbi/playurl?bvid=xxx&cid=xxx&qn=xxx
    // Requires WBI signature
}
```

#### 1.5 Stream Extraction

```go
// Parse playurl API response to extract video/audio streams
func (c *BilibiliClient) ExtractStreams(resp *PlayURLResponse) ([]VideoStream, []AudioStream, error) {
    // Handle both DASH (video+audio separate) and legacy FLV formats
}
```

### Phase 2: Authentication (Priority: High)

**Goal**: Support both QR code login and manual cookie input, in both CLI and UI.

#### 2.1 Authentication Architecture

```
internal/
├── extractor/
│   └── bilibili_auth.go       # Core auth logic (API calls, token storage)
├── cli/
│   └── bilibili_login.go      # CLI: ASCII QR + cookie prompt
└── ui/                        # (existing React UI)
    └── components/
        └── BilibiliLogin.tsx  # UI: Image QR + cookie input field
```

#### 2.2 Core Auth Module

```go
// internal/extractor/bilibili_auth.go

type BilibiliAuth struct {
    configPath string  // ~/.config/vget/bilibili.json
}

type BilibiliCredentials struct {
    SESSDATA  string    `json:"sessdata"`
    BiliJCT   string    `json:"bili_jct"`
    DedeUserID string   `json:"dede_user_id"`
    ExpiresAt time.Time `json:"expires_at"`
}

// QR Code Login Flow
type QRLoginSession struct {
    URL       string  // QR code content URL
    QRCodeKey string  // Key for polling status
}

func (a *BilibiliAuth) GenerateQRCode() (*QRLoginSession, error) {
    // GET https://passport.bilibili.com/x/passport-login/web/qrcode/generate
    // Returns: { data: { url: "...", qrcode_key: "..." } }
}

type QRStatus int
const (
    QRWaiting   QRStatus = 86101  // Not scanned yet
    QRScanned   QRStatus = 86090  // Scanned, waiting confirm
    QRExpired   QRStatus = 86038  // QR code expired
    QRConfirmed QRStatus = 0      // Success
)

func (a *BilibiliAuth) PollQRStatus(qrcodeKey string) (QRStatus, *BilibiliCredentials, error) {
    // GET https://passport.bilibili.com/x/passport-login/web/qrcode/poll?qrcode_key=xxx
    // Returns status code and credentials on success
}

// Manual Cookie
func (a *BilibiliAuth) SetCookie(cookie string) (*BilibiliCredentials, error) {
    // Parse cookie string: "SESSDATA=xxx; bili_jct=xxx; DedeUserID=xxx"
    // Validate by calling user info API
    // Save to config file
}

// Token Storage
func (a *BilibiliAuth) SaveCredentials(creds *BilibiliCredentials) error {
    // Save to ~/.config/vget/bilibili.json
}

func (a *BilibiliAuth) LoadCredentials() (*BilibiliCredentials, error) {
    // Load from ~/.config/vget/bilibili.json
    // Return nil if not found or expired
}

func (a *BilibiliAuth) GetCookieString() string {
    // Return formatted cookie for HTTP requests
    // "SESSDATA=xxx; bili_jct=xxx; DedeUserID=xxx"
}
```

#### 2.3 CLI Login Interface

```go
// internal/cli/bilibili_login.go

// Command: vget login bilibili
func BilibiliLoginCmd() *cobra.Command {
    cmd := &cobra.Command{
        Use:   "bilibili",
        Short: "Login to Bilibili",
    }

    cmd.AddCommand(
        bilibiliQRLoginCmd(),     // vget login bilibili qr
        bilibiliCookieLoginCmd(), // vget login bilibili cookie
    )

    return cmd
}

// QR Login in Terminal
func bilibiliQRLogin() error {
    auth := extractor.NewBilibiliAuth()

    // 1. Generate QR code
    session, _ := auth.GenerateQRCode()

    // 2. Display ASCII QR in terminal
    qr, _ := qrcode.New(session.URL, qrcode.Medium)
    fmt.Println(qr.ToSmallString(false))
    fmt.Println("Scan with Bilibili app, or open:", session.URL)

    // 3. Poll for confirmation
    for {
        status, creds, _ := auth.PollQRStatus(session.QRCodeKey)
        switch status {
        case extractor.QRWaiting:
            // Show spinner
        case extractor.QRScanned:
            fmt.Println("Scanned! Please confirm in app...")
        case extractor.QRExpired:
            return errors.New("QR code expired")
        case extractor.QRConfirmed:
            auth.SaveCredentials(creds)
            fmt.Println("Login successful!")
            return nil
        }
        time.Sleep(time.Second)
    }
}

// Cookie Login in Terminal
func bilibiliCookieLogin() error {
    fmt.Println("Enter your Bilibili cookie (SESSDATA=xxx; bili_jct=xxx):")
    reader := bufio.NewReader(os.Stdin)
    cookie, _ := reader.ReadString('\n')

    auth := extractor.NewBilibiliAuth()
    creds, err := auth.SetCookie(strings.TrimSpace(cookie))
    if err != nil {
        return fmt.Errorf("invalid cookie: %w", err)
    }

    fmt.Printf("Login successful! User ID: %s\n", creds.DedeUserID)
    return nil
}
```

#### 2.4 UI Login Interface

```typescript
// ui/components/BilibiliLogin.tsx

interface QRLoginState {
  qrUrl: string;
  qrCodeKey: string;
  status: 'waiting' | 'scanned' | 'expired' | 'success';
}

export function BilibiliLogin() {
  const [mode, setMode] = useState<'qr' | 'cookie'>('qr');
  const [qrState, setQrState] = useState<QRLoginState | null>(null);
  const [cookie, setCookie] = useState('');

  // QR Code Login
  async function startQRLogin() {
    const session = await api.bilibili.generateQR();
    setQrState({ qrUrl: session.url, qrCodeKey: session.qrcode_key, status: 'waiting' });
    pollQRStatus(session.qrcode_key);
  }

  async function pollQRStatus(key: string) {
    const interval = setInterval(async () => {
      const result = await api.bilibili.pollQR(key);
      if (result.status === 'confirmed') {
        clearInterval(interval);
        setQrState(s => ({ ...s!, status: 'success' }));
      } else if (result.status === 'expired') {
        clearInterval(interval);
        setQrState(s => ({ ...s!, status: 'expired' }));
      } else if (result.status === 'scanned') {
        setQrState(s => ({ ...s!, status: 'scanned' }));
      }
    }, 1000);
  }

  // Cookie Login
  async function submitCookie() {
    await api.bilibili.setCookie(cookie);
  }

  return (
    <div>
      <Tabs value={mode} onChange={setMode}>
        <Tab value="qr">QR Code</Tab>
        <Tab value="cookie">Cookie</Tab>
      </Tabs>

      {mode === 'qr' && (
        <div>
          {qrState ? (
            <>
              <QRCodeImage value={qrState.qrUrl} />
              <StatusText status={qrState.status} />
            </>
          ) : (
            <Button onClick={startQRLogin}>Generate QR Code</Button>
          )}
        </div>
      )}

      {mode === 'cookie' && (
        <div>
          <p>Get cookie from browser DevTools → Application → Cookies</p>
          <TextArea
            placeholder="SESSDATA=xxx; bili_jct=xxx; DedeUserID=xxx"
            value={cookie}
            onChange={setCookie}
          />
          <Button onClick={submitCookie}>Save Cookie</Button>
        </div>
      )}
    </div>
  );
}
```

#### 2.5 API Endpoints for UI

```go
// internal/server/bilibili_routes.go

func RegisterBilibiliRoutes(r *mux.Router) {
    r.HandleFunc("/api/bilibili/qr/generate", handleGenerateQR).Methods("POST")
    r.HandleFunc("/api/bilibili/qr/poll", handlePollQR).Methods("GET")
    r.HandleFunc("/api/bilibili/cookie", handleSetCookie).Methods("POST")
    r.HandleFunc("/api/bilibili/status", handleAuthStatus).Methods("GET")
}

func handleGenerateQR(w http.ResponseWriter, r *http.Request) {
    auth := extractor.NewBilibiliAuth()
    session, _ := auth.GenerateQRCode()
    json.NewEncoder(w).Encode(session)
}

func handlePollQR(w http.ResponseWriter, r *http.Request) {
    key := r.URL.Query().Get("qrcode_key")
    auth := extractor.NewBilibiliAuth()
    status, creds, _ := auth.PollQRStatus(key)

    if status == extractor.QRConfirmed {
        auth.SaveCredentials(creds)
    }

    json.NewEncoder(w).Encode(map[string]interface{}{
        "status": statusToString(status),
    })
}

func handleSetCookie(w http.ResponseWriter, r *http.Request) {
    var req struct{ Cookie string }
    json.NewDecoder(r.Body).Decode(&req)

    auth := extractor.NewBilibiliAuth()
    creds, err := auth.SetCookie(req.Cookie)
    if err != nil {
        http.Error(w, err.Error(), 400)
        return
    }

    json.NewEncoder(w).Encode(map[string]string{
        "user_id": creds.DedeUserID,
    })
}
```

#### 2.6 CLI Commands Summary

```bash
# QR Code login (shows ASCII QR in terminal)
vget login bilibili qr

# Cookie login (interactive prompt)
vget login bilibili cookie

# Cookie login (non-interactive, for scripts)
vget login bilibili cookie --value "SESSDATA=xxx; bili_jct=xxx"

# Check login status
vget login bilibili status

# Logout (remove saved credentials)
vget login bilibili logout
```

#### 2.7 Credential Storage

```json
// ~/.config/vget/bilibili.json
{
  "sessdata": "abc123...",
  "bili_jct": "def456...",
  "dede_user_id": "12345678",
  "expires_at": "2024-06-01T00:00:00Z"
}
```

#### 2.8 Dependencies

| Package | Purpose |
|---------|---------|
| `github.com/skip2/go-qrcode` | Generate QR code for terminal (ASCII) |
| `github.com/mdp/qrterminal/v3` | Alternative: better terminal QR rendering |

### Phase 3: Extended Content Types (Priority: Medium)

#### 3.1 Fetcher Interface

```go
// internal/extractor/bilibili_fetcher.go

type BilibiliFetcher interface {
    Name() string
    Match(id string) bool
    Fetch(id string, client *BilibiliClient) ([]*BilibiliVideoInfo, error)
}

// Factory function
func CreateFetcher(id string) BilibiliFetcher {
    switch {
    case strings.HasPrefix(id, "ep"):
        return &BangumiFetcher{}
    case strings.HasPrefix(id, "ss"):
        return &BangumiFetcher{}
    case strings.HasPrefix(id, "cheese:"):
        return &CheeseFetcher{}
    case strings.HasPrefix(id, "mid"):
        return &SpaceFetcher{}
    default:
        return &NormalFetcher{}
    }
}
```

#### 3.2 Anime (Bangumi) Fetcher

```go
// internal/extractor/bilibili_fetcher_bangumi.go

// Handles:
// - https://www.bilibili.com/bangumi/play/ep123
// - https://www.bilibili.com/bangumi/play/ss456
// - https://www.bilibili.com/bangumi/media/md789

type BangumiFetcher struct{}

func (f *BangumiFetcher) Fetch(id string, client *BilibiliClient) ([]*BilibiliVideoInfo, error) {
    // GET https://api.bilibili.com/pgc/view/web/season?ep_id=xxx
    // or season_id for ss/md IDs
}
```

#### 3.3 Course (Cheese) Fetcher

```go
// internal/extractor/bilibili_fetcher_cheese.go

// Handles paid courses:
// - https://www.bilibili.com/cheese/play/ep123

type CheeseFetcher struct{}
```

#### 3.4 User Space Fetcher

```go
// internal/extractor/bilibili_fetcher_space.go

// Download all videos from a user:
// - mid123456 (user ID)

type SpaceFetcher struct{}
```

### Phase 4: APP API & Advanced Quality (Priority: Medium)

#### 4.1 Protobuf Setup

```bash
# Generate Go code from proto files
protoc --go_out=. --go-grpc_out=. bilibili_proto/*.proto
```

Proto files to port from BBDown:

- `bilibili.app.playurl.v1.proto`
- `bilibili.pgc.gateway.player.v2.proto`
- Various header and metadata protos

#### 4.2 APP API Client

```go
// internal/extractor/bilibili_api_app.go

type BilibiliAppClient struct {
    grpcClient *grpc.ClientConn
    accessKey  string
}

func (c *BilibiliAppClient) GetPlayView(aid, cid int64) (*PlayViewReply, error) {
    // gRPC call to grpc.biliapi.net
    // Required for FLAC, Dolby Atmos, 8K HDR
}
```

### Phase 5: Post-Processing (Priority: Low)

#### 5.1 Subtitle Processing

```go
// internal/extractor/bilibili_subtitle.go

// Download and convert subtitles
func (c *BilibiliClient) GetSubtitles(bvid string, cid int64) ([]Subtitle, error) {
    // Parse from video info API
    // Convert BCC (Bilibili) format to SRT
}
```

#### 5.2 Danmaku (Bullet Comments)

```go
// internal/extractor/bilibili_danmaku.go

// Download danmaku in various formats
func (c *BilibiliClient) GetDanmaku(cid int64, format string) ([]byte, error) {
    // Formats: xml, protobuf, ass
    // GET https://api.bilibili.com/x/v1/dm/list.so?oid=xxx
}
```

## Quality Access by Account Type

| 画质 | 未登录 | 已登录 | 大会员 |
|------|--------|--------|--------|
| 360P | ✅ | ✅ | ✅ |
| 480P | ✅ | ✅ | ✅ |
| 720P | ⚠️ | ✅ | ✅ |
| 1080P | ❌ | ✅ | ✅ |
| 1080P+ | ❌ | ❌ | ✅ |
| 4K | ❌ | ❌ | ✅ |
| 8K | ❌ | ❌ | ✅ |
| HDR | ❌ | ❌ | ✅ |
| 杜比视界 | ❌ | ❌ | ✅ |

## Quality Priority Mapping

```go
var QualityMap = map[int]string{
    127: "8K",
    126: "Dolby Vision",
    125: "HDR",
    120: "4K",
    116: "1080P60",
    112: "1080P+",
    80:  "1080P",
    74:  "720P60",
    64:  "720P",
    32:  "480P",
    16:  "360P",
    6:   "144P (for mobile)",
}

var AudioQualityMap = map[int]string{
    30280: "320kbps",
    30232: "128kbps",
    30216: "64kbps",
    30250: "Dolby Atmos",
    30251: "Hi-Res",
}
```

## Integration with vget

### CLI Commands

```bash
# Basic download
vget https://www.bilibili.com/video/BV1xx411c7mD

# With quality selection
vget -q 1080p https://www.bilibili.com/video/BV1xx411c7mD

# With authentication
vget --bilibili-cookie "SESSDATA=xxx" https://www.bilibili.com/video/BV1xx411c7mD

# Download specific pages (multi-part video)
vget -p 1-5 https://www.bilibili.com/video/BV1xx411c7mD

# Download anime series
vget https://www.bilibili.com/bangumi/play/ss12345
```

### Config Integration

```yaml
# ~/.config/vget/config.yml
bilibili:
  cookie: "SESSDATA=xxx; bili_jct=xxx"
  quality: "1080p"
  audio_quality: "320kbps"
  download_subtitle: true
  download_danmaku: false
  prefer_hevc: true
```

## Dependencies

| Package                      | Purpose                      |
| ---------------------------- | ---------------------------- |
| `google.golang.org/protobuf` | Protobuf for APP API         |
| `google.golang.org/grpc`     | gRPC client for APP API      |
| `github.com/skip2/go-qrcode` | QR code generation for login |

## Estimated Scope

| Phase                    | Go Lines (Est.) | Priority |
| ------------------------ | --------------- | -------- |
| Phase 1: Foundation      | ~1,500          | High     |
| Phase 2: Authentication  | ~400            | High     |
| Phase 3: Extended Types  | ~800            | Medium   |
| Phase 4: APP API         | ~1,200          | Medium   |
| Phase 5: Post-Processing | ~600            | Low      |
| **Total**                | **~4,500**      | -        |

## Implementation Order

1. **bilibili.go** - Extractor interface, URL matching, ID conversion
2. **bilibili_api.go** - WEB API client, WBI signature
3. **bilibili_parser.go** - Stream extraction from API responses
4. **bilibili_fetcher_normal.go** - Regular video support
5. **bilibili_auth.go** - Cookie parsing, token management
6. **bilibili_fetcher_bangumi.go** - Anime support
7. **bilibili_fetcher_cheese.go** - Course support
8. **bilibili_proto/** - Protobuf definitions (copy from BBDown)
9. **bilibili_api_app.go** - APP API for advanced quality
10. **bilibili_subtitle.go** - Subtitle download/conversion
11. **bilibili_danmaku.go** - Danmaku support

## 8K Video Download Technical Details

### Quality Codes (qn)

| qn  | Quality      | Requirements           |
| --- | ------------ | ---------------------- |
| 127 | 8K 超高清    | 大会员 + DASH + HEVC/AV1 |
| 126 | 杜比视界     | 大会员 + DASH          |
| 125 | HDR 真彩     | 大会员 + DASH          |
| 120 | 4K 超清      | 大会员 + DASH          |
| 116 | 1080P 60帧   | 大会员                 |
| 112 | 1080P 高码率 | 大会员                 |
| 80  | 1080P        | 登录                   |
| 64  | 720P         | 登录                   |
| 32  | 480P         | -                      |
| 16  | 360P         | -                      |

### 8K Requirements

| Requirement | Details |
|------------|---------|
| **Account** | 大会员 (Premium membership) required |
| **Format** | DASH only (no MP4/FLV for 8K) |
| **Codec** | HEVC or AV1 only (AVC not supported for 8K) |
| **fnval** | `1024` (8K flag) or `4048` (all DASH streams) |
| **fourk** | `1` (enable 4K/8K negotiation) |

### API Comparison for 8K

| API | Endpoint | 8K Support | Auth Method | Best For |
|-----|----------|-----------|-------------|----------|
| **APP API** | `grpc.biliapi.net` (gRPC) | ✅ Full | access_token | 8K, FLAC, Dolby |
| **TV API** | `api.snm0516.aisee.tv` | ✅ Yes | access_key + signature | Alternative |
| **WEB API** | `api.bilibili.com` | ⚠️ Limited | Cookie + WBI signature | Standard use |
| **INTL API** | `api.biliintl.com` | ❌ No | Cookie | International |

### APP API (Recommended for 8K)

The APP API uses gRPC with Protobuf, same as Bilibili mobile app:

```go
// Request structure
PlayViewReq {
    Aid:             int64,   // Video AV number
    Cid:             int64,   // Content ID
    Qn:              127,     // Always request 8K
    Fnval:           4048,    // DASH with all options
    Fourk:           true,    // Enable 4K/8K
    PreferCodecType: CodeAV1, // or CodeHEVC
}

// Endpoints
Regular:  grpc.biliapi.net/bilibili.app.playurl.v1.PlayURL/PlayView
Bangumi:  app.bilibili.com/bilibili.pgc.gateway.player.v2.PlayURL/PlayView
```

### fnval Bitmask

```go
const (
    FnvalMP4     = 1     // MP4 format
    FnvalDASH    = 16    // DASH format
    FnvalHDR     = 64    // HDR support
    FnvalDolby   = 256   // Dolby audio
    FnvalDolbyVision = 512  // Dolby Vision
    Fnval8K      = 1024  // 8K resolution
    FnvalAV1     = 2048  // AV1 codec
)

// Common combinations
FnvalAll = 4048  // 16 | 64 | 256 | 512 | 1024 | 2048
```

### Codec Support

| Codec | Code | 8K Support | Notes |
|-------|------|------------|-------|
| AV1   | 13   | ✅ Yes | Best compression, newer |
| HEVC  | 12   | ✅ Yes | Default for bangumi |
| AVC   | 7    | ❌ No | Legacy, max 4K |

### Authentication for 8K

```bash
# Method 1: APP API with TV login token
bbdown logintv                    # Get access_token
bbdown <url> -app                 # Use APP API

# Method 2: Cookie (WEB API, limited)
bbdown login                      # QR code login
bbdown <url> --cookie <cookie>    # Use WEB API
```

### Implementation Priority

For vget Bilibili support:

1. **Phase 1**: WEB API with Cookie (covers most content up to 1080P)
2. **Phase 2**: TV API for 4K content
3. **Phase 3**: APP API (gRPC) for 8K, FLAC, Dolby

## References

- [BBDown Source](https://github.com/nilaoda/BBDown)
- [Bilibili API Documentation](https://github.com/SocialSisterYi/bilibili-API-collect) (unofficial)
- [Video Stream URL API](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/video/videostream_url.md)
- [BV/AV Conversion Algorithm](https://www.zhihu.com/question/381784377)
- [WBI Signature Mechanism](https://github.com/SocialSisterYi/bilibili-API-collect/blob/master/docs/misc/sign/wbi.md)


================================================
FILE: docs/bugfix/docker-browser-launch.md
================================================
# Docker Browser Launch Hang Bug

## Problem

Browser-based extractors (m3u8 detection, XHS, etc.) would hang indefinitely in Docker while working fine on macOS CLI.

**Symptoms:**
- CLI on Mac: Works
- CLI in Docker: Hangs at "Trying to detecting m3u8 stream..."
- Server in Docker: Same hang

## Root Cause

The `go-rod` library wasn't using the `ROD_BROWSER` environment variable to locate the system Chromium. Instead, it was attempting to download its own browser binary, which would hang in the containerized environment.

Even though `ROD_BROWSER=/usr/bin/chromium` was set in the Dockerfile, rod's `launcher.New()` doesn't automatically use this env var - it needs explicit `Bin()` call.

## Solution

In `internal/extractor/browser.go`, explicitly set the browser binary path:

```go
func (e *BrowserExtractor) createLauncher(headless bool) *launcher.Launcher {
    // Check for ROD_BROWSER env var (set in Docker)
    browserPath := os.Getenv("ROD_BROWSER")

    l := launcher.New().
        Headless(headless).
        // ... other options

    // Explicitly set browser path if provided (required for Docker)
    if browserPath != "" {
        l = l.Bin(browserPath)
    }

    return l
}
```

## Additional Changes

1. **Switched from Alpine to Debian** - Alpine's musl libc and Chromium package caused compatibility issues. Debian's glibc-based Chromium is more stable.

2. **Added Chrome flags** for better headless stability:
   - `disable-software-rasterizer`
   - `disable-extensions`
   - `disable-background-networking`
   - `window-size=1920,1080`
   - Custom user-agent to avoid bot detection

## Files Changed

- `internal/extractor/browser.go` - Added explicit `Bin()` call and Chrome flags
- `Dockerfile` - Switched to Debian bookworm-slim
- `docker/entrypoint.sh` - Changed from `su-exec` to `gosu` (Debian equivalent)


================================================
FILE: docs/homebrew-distribution.md
================================================
# Homebrew Distribution

This document explains how to distribute vget via Homebrew.

## Option 1: Own Tap (Recommended)

Use your own Homebrew tap for instant updates with no PR reviews.

### Setup

1. Create GitHub repo: `guiyumin/homebrew-tap`

2. Add `Formula/vget.rb`:

```ruby
class Vget < Formula
  desc "Media downloader CLI for various platforms"
  homepage "https://github.com/guiyumin/vget"
  url "https://github.com/guiyumin/vget/archive/refs/tags/v0.9.2.tar.gz"
  sha256 "bf5228673cfd080ac8f0e9d0ee05e875fc5bfcde342ae5fd615c5d2a23181ab3"
  license "Apache-2.0"

  depends_on "go" => :build

  def install
    ldflags = "-s -w -X github.com/guiyumin/vget/internal/version.Version=#{version}"
    system "go", "build", *std_go_args(ldflags: ldflags), "./cmd/vget"
  end

  test do
    assert_match version.to_s, shell_output("#{bin}/vget --version")
  end
end
```

3. Users install with:

```bash
brew tap guiyumin/tap
brew install vget
```

Or in one command:

```bash
brew install guiyumin/tap/vget
```

### Automate Updates with GoReleaser

Add to `.goreleaser.yaml`:

```yaml
brews:
  - repository:
      owner: guiyumin
      name: homebrew-tap
    homepage: "https://github.com/guiyumin/vget"
    description: "Media downloader CLI for various platforms"
    license: "Apache-2.0"
    directory: Formula
```

This automatically updates your tap on every GitHub release.

## Option 2: homebrew-core (Official)

Submit to the official Homebrew repository for `brew install vget` (no tap needed).

### Requirements

- 30+ GitHub stars (vget has 300+ ✓)
- Stable versioned releases ✓
- Open source license ✓

### Submission Steps

```bash
# 1. Fork homebrew-core on GitHub, then clone
git clone https://github.com/YOUR_USERNAME/homebrew-core.git
cd homebrew-core

# 2. Create the formula
mkdir -p Formula/v
# Add Formula/v/vget.rb (same content as above)

# 3. Test locally
brew install --build-from-source ./Formula/v/vget.rb
brew test vget
brew audit --strict --new vget

# 4. Commit and push
git checkout -b vget
git add Formula/v/vget.rb
git commit -m "vget: new formula"
git push origin vget

# 5. Open PR to Homebrew/homebrew-core
```

PR title: `vget 0.9.2 (new formula)`

### Updating Versions

After initial approval, update with:

```bash
brew bump-formula-pr --url https://github.com/guiyumin/vget/archive/refs/tags/vX.Y.Z.tar.gz vget
```

Or automate with GitHub Actions (`.github/workflows/homebrew.yml`):

```yaml
name: Bump Homebrew Formula

on:
  release:
    types: [published]

jobs:
  bump-formula:
    runs-on: macos-latest
    steps:
      - name: Bump formula
        uses: mislav/bump-homebrew-formula-action@v3
        with:
          formula-name: vget
        env:
          COMMITTER_TOKEN: ${{ secrets.HOMEBREW_GITHUB_TOKEN }}
```

Setup: Create a GitHub PAT with `public_repo` scope and add as `HOMEBREW_GITHUB_TOKEN` secret.

Version bump PRs are auto-merged by BrewTestBot in ~5-15 minutes (no human review).

## Comparison

| Approach | Review Time | User Command |
|----------|-------------|--------------|
| Own tap | Instant | `brew install guiyumin/tap/vget` |
| homebrew-core | ~5-15 min (auto-merge) | `brew install vget` |


================================================
FILE: docs/http-server-mode.md
================================================
# HTTP Server Mode (`vget server`)

## Overview

HTTP server mode that accepts download requests via API, with an embedded WebUI for job monitoring.

**Commands:**

```bash
vget server start              # Foreground, port 8080
vget server start -d           # Background daemon, port 8080
vget server start -p 9000      # Custom port
vget server start -d -p 9000 -o ~/downloads
vget server stop               # Stop daemon
vget server restart            # Restart server
vget server status             # Check if running
vget server logs               # View recent logs
vget server logs -f            # Follow logs (tail -f)
```

- Listens on port 8080 by default (override with `-p`)
- `-d` runs as background daemon
- WebUI available at `http://localhost:8080/`
- API accepts URLs via HTTP POST
- Supports video, audio, and image downloads via extractors

## WebUI

The server includes an embedded React SPA for job monitoring:

- Real-time job status updates (polling)
- Download form to submit URLs
- Progress bars for active downloads
- Cancel button for queued/downloading jobs
- Configuration panel for output directory
- i18n support (zh, en, jp, kr, es, fr, de)
- Dark theme

Access at `http://localhost:8080/` when server is running.

## Configuration

**CLI Flags:**
| Flag | Default | Description |
|------|---------|-------------|
| `-p, --port` | 8080 | HTTP listen port |
| `-o, --output` | config default or `~/Downloads` | Output directory |
| `-d, --daemon` | false | Run in background |

**Config file (`~/.config/vget/config.yml`):**

```yaml
output_dir: ~/Downloads/vget

server:
  port: 8080
  max_concurrent: 10
  api_key: "optional-secret-key"
```

**Priority order for output directory:** CLI flag `-o` > `output_dir` in config > default (`~/Downloads/vget`)

## API Reference

### Response Structure

All endpoints return a consistent JSON structure:

```json
{
  "code": 200,
  "data": { ... },
  "message": "description"
}
```

### Endpoints

#### `GET /health`

```json
{
  "code": 200,
  "data": {
    "status": "ok",
    "version": "v0.7.1"
  },
  "message": "everything is good"
}
```

#### `POST /download`

```json
// Request
{
  "url": "https://twitter.com/...",
  "filename": "optional.mp4",
  "return_file": false
}

// Response (return_file=false)
{
  "code": 200,
  "data": {
    "id": "abc123",
    "status": "queued"
  },
  "message": "download started"
}

// Response (return_file=true)
// Returns file directly with Content-Disposition header
```

#### `GET /status/:id`

```json
{
  "code": 200,
  "data": {
    "id": "abc123",
    "status": "downloading",
    "progress": 45.5,
    "filename": "video.mp4"
  },
  "message": "downloading"
}
```

#### `GET /jobs`

```json
{
  "code": 200,
  "data": {
    "jobs": [
      { "id": "abc123", "url": "...", "status": "completed" },
      {
        "id": "def456",
        "url": "...",
        "status": "downloading",
        "progress": 67.2
      }
    ]
  },
  "message": "2 jobs found"
}
```

#### `DELETE /jobs/:id`

```json
{
  "code": 200,
  "data": { "id": "def456" },
  "message": "job cancelled"
}
```

#### `GET /config`

```json
{
  "code": 200,
  "data": {
    "output_dir": "/path/to/downloads"
  },
  "message": "config retrieved"
}
```

#### `PUT /config`

Update server configuration at runtime.

```json
// Request
{
  "output_dir": "/new/path/to/downloads"
}

// Response
{
  "code": 200,
  "data": {
    "output_dir": "/new/path/to/downloads"
  },
  "message": "config updated"
}
```

#### `GET /i18n`

Get UI translations for the configured language.

```json
{
  "code": 200,
  "data": {
    "language": "zh",
    "ui": { ... },
    "server": { ... },
    "config_exists": true
  },
  "message": "translations retrieved"
}
```

### Authentication

Optional API key authentication via header `X-API-Key`. If `api_key` is set in config, all API requests must include it. The WebUI and `/health` endpoint are accessible without authentication.

## Daemon Mode

```bash
vget server start -d       # Start daemon
vget server stop           # Stop daemon
vget server restart        # Restart daemon
vget server status         # Check if running
vget server logs -f        # Follow logs
```

- PID stored in `~/.config/vget/serve.pid`
- Logs written to `~/.config/vget/serve.log`

## Development

### Running in Dev Mode

For UI development with hot reload:

**Terminal 1 - Go server (API on :8080):**

```bash
go run ./cmd/vget server start
```

**Terminal 2 - Vite dev server (UI on :5173):**

```bash
cd ui && npm run dev
```

Open `http://localhost:5173` - Vite proxies API calls to the Go server.

### Building

```bash
# Build UI and Go binary
make build

# Or manually:
cd ui && npm install && npm run build
cp -r ui/dist/* internal/server/dist/
go build -o build/vget ./cmd/vget
```

## Architecture

```
ui/                          # React SPA source
internal/server/
├── server.go                # HTTP server, handlers, download logic
├── job.go                   # Job queue, worker pool
├── embed.go                 # go:embed for UI
└── dist/                    # Built UI (embedded)
internal/cli/server.go       # Cobra commands, daemon management, service install
internal/config/config.go    # ServerConfig struct
```

### Internal Flow

```
HTTP Request
    ↓
Logging middleware
    ↓
Auth middleware (check X-API-Key if configured, skip for /health and UI)
    ↓
Route to handler
    ↓
POST /download → Add job to queue → Return job ID
    ↓
Worker pool (max_concurrent workers, default 10)
    ↓
Worker picks job → extractor.Match(url) → ext.Extract(url) → download with progress
    ↓
Update job status (queued → downloading → completed/failed/cancelled)
    ↓
Auto-cleanup completed/failed/cancelled jobs after 1 hour (runs every 10 minutes)
```

### Supported Media Types

The server uses the extractor system to handle different media:

- **Video** (Twitter, YouTube, etc.) - Selects best format (prefers with audio, then highest bitrate)
- **Audio** (podcasts, music)
- **Images** (downloads all images from multi-image posts)

For unsupported URLs, falls back to `sites.yml` config or generic browser extractor.

## Usage Examples

**Start server:**

```bash
vget server start -p 9000 -o ~/Downloads/vget
vget server start -d  # Run in background
```

**Download via API:**

```bash
# Queue download
curl -X POST http://localhost:8080/download \
  -H "Content-Type: application/json" \
  -d '{"url": "https://twitter.com/user/status/123"}'

# Download and return file directly
curl -X POST http://localhost:8080/download \
  -H "Content-Type: application/json" \
  -d '{"url": "https://...", "return_file": true}' \
  -o video.mp4

# Check status
curl http://localhost:8080/status/abc123

# List all jobs
curl http://localhost:8080/jobs

# Cancel job
curl -X DELETE http://localhost:8080/jobs/abc123

# Get/update config
curl http://localhost:8080/config
curl -X PUT http://localhost:8080/config \
  -H "Content-Type: application/json" \
  -d '{"output_dir": "/new/path"}'
```

## Job Queue Details

- Job queue buffer size: 100 jobs
- Jobs have unique 16-character hex IDs
- Job statuses: `queued`, `downloading`, `completed`, `failed`, `cancelled`
- Progress tracking via callback during download
- Context-based cancellation support

---

## Future Enhancements

- WebSocket for real-time progress updates (currently uses polling)
- Webhook notifications on completion
- Multi-user support with separate queues
- Download scheduling (see below)

---

## Download Scheduling (Planned)

Schedule downloads to run at specific times or on recurring intervals.

### Features

**One-time scheduled downloads:**

- Schedule a download to start at a specific datetime
- Use case: Queue large downloads for off-peak hours

**Recurring downloads (cron-style):**

- Standard cron expressions for repeat scheduling
- Use case: Automatically fetch new podcast episodes, YouTube channel updates

**Time-window restrictions:**

- Limit downloads to specific time windows
- Use case: Bandwidth management, only download during night hours

### API Endpoints

#### `POST /api/v1/schedules`

Create a new schedule.

```json
// Request
{
  "url": "https://example.com/video",
  "schedule": "0 2 * * *",          // cron expression (2 AM daily)
  "name": "Daily backup video",      // optional, human-readable name
  "enabled": true,
  "options": {
    "format": "best",
    "output_dir": "/downloads/scheduled"
  }
}

// Response
{
  "code": 200,
  "data": {
    "id": "sch_abc123",
    "url": "https://example.com/video",
    "schedule": "0 2 * * *",
    "next_run": "2025-01-15T02:00:00Z",
    "enabled": true
  },
  "message": "schedule created"
}
```

**Cron expression format:** `minute hour day month weekday`
| Expression | Description |
|------------|-------------|
| `0 2 * * *` | Every day at 2:00 AM |
| `0 */6 * * *` | Every 6 hours |
| `0 8 * * 1` | Every Monday at 8:00 AM |
| `30 22 * * 5` | Every Friday at 10:30 PM |

**One-time schedule:** Use `run_at` instead of `schedule`:

```json
{
  "url": "https://example.com/large-file",
  "run_at": "2025-01-15T03:00:00Z"
}
```

#### `GET /api/v1/schedules`

List all schedules.

```json
{
  "code": 200,
  "data": {
    "schedules": [
      {
        "id": "sch_abc123",
        "name": "Daily podcast",
        "url": "https://...",
        "schedule": "0 6 * * *",
        "next_run": "2025-01-15T06:00:00Z",
        "last_run": "2025-01-14T06:00:00Z",
        "last_status": "completed",
        "enabled": true
      }
    ]
  },
  "message": "1 schedule found"
}
```

#### `GET /api/v1/schedules/:id`

Get schedule details and history.

```json
{
  "code": 200,
  "data": {
    "id": "sch_abc123",
    "name": "Daily podcast",
    "url": "https://...",
    "schedule": "0 6 * * *",
    "enabled": true,
    "next_run": "2025-01-15T06:00:00Z",
    "history": [
      {
        "run_at": "2025-01-14T06:00:00Z",
        "status": "completed",
        "job_id": "job_xyz"
      },
      {
        "run_at": "2025-01-13T06:00:00Z",
        "status": "completed",
        "job_id": "job_abc"
      }
    ]
  },
  "message": "schedule found"
}
```

#### `PUT /api/v1/schedules/:id`

Update a schedule.

```json
// Request
{
  "schedule": "0 3 * * *",
  "enabled": false
}

// Response
{
  "code": 200,
  "data": {"id": "sch_abc123"},
  "message": "schedule updated"
}
```

#### `DELETE /api/v1/schedules/:id`

Delete a schedule.

```json
{
  "code": 200,
  "data": { "id": "sch_abc123" },
  "message": "schedule deleted"
}
```

#### `POST /api/v1/schedules/:id/run`

Trigger a scheduled download immediately (outside of schedule).

```json
{
  "code": 200,
  "data": {
    "id": "sch_abc123",
    "job_id": "job_xyz789"
  },
  "message": "schedule triggered"
}
```

### Configuration

```yaml
# ~/.config/vget/config.yml
server:
  scheduling:
    enabled: true
    max_schedules: 50 # max number of schedules
    history_retention: 30 # days to keep run history
    time_window: # optional global restriction
      start: "01:00" # downloads only between 1 AM
      end: "06:00" # and 6 AM
```

### Persistence

- Schedules stored in `~/.config/vget/schedules.json`
- Survives server restarts
- Run history kept for configured retention period

### WebUI Integration

- New "Schedules" tab in the dashboard
- Create/edit/delete schedules via UI
- View upcoming runs and execution history
- Toggle schedules on/off

### Implementation Notes

- Uses `robfig/cron/v3` library for cron parsing and scheduling
- Schedules are evaluated on server startup and when modified
- Scheduled jobs enter the same job queue as manual downloads
- If server is stopped during scheduled time, missed runs are skipped (no catch-up)

---

## Service Installation

One-command installation for NAS and Linux servers.

### Commands

```bash
sudo vget server install      # Install as systemd service (interactive)
sudo vget server install -y   # Install with defaults (non-interactive)
sudo vget server uninstall    # Remove service
vget server install --help    # Show options
```

### Interactive TUI Flow

When running `sudo vget server install`, a Bubbletea TUI guides the user:

```
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   vget service installer                                    │
│                                                             │
│   This will install vget as a system service:               │
│                                                             │
│   ✓ Copy binary to /usr/local/bin/vget                      │
│   ✓ Create systemd service at /etc/systemd/system/          │
│   ✓ Enable auto-start on boot                               │
│   ✓ Start the vget server                                   │
│                                                             │
│   Service configuration:                                    │
│   ┌─────────────────────────────────────────────────────┐   │
│   │  Port:        8080                                  │   │
│   │  Output dir:  /var/lib/vget/downloads               │   │
│   │  Run as user: vget                                  │   │
│   └─────────────────────────────────────────────────────┘   │
│                                                             │
│   [ Configure ]    [ Install ]    [ Cancel ]                │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### Configuration Screen (if "Configure" selected)

```
┌─────────────────────────────────────────────────────────────┐
│                                                             │
│   Service Configuration                                     │
│                                                             │
│   Port:              8080                                   │
│   Output directory:  /var/lib/vget/downloads                │
│   Run as user:       vget  (will be created if needed)      │
│   API key:           (none)                                 │
│                                                             │
│   Use arrow keys to navigate, Enter to edit                 │
│                                                             │
│   [ Back ]    [ Save & Install ]                            │
│                                                             │
└─────────────────────────────────────────────────────────────┘
```

### What `vget server install` Does

1. **Pre-flight checks**
   - Verify running as root (prompt for sudo if not)
   - Check if systemd is available
   - Check if service already exists (offer reinstall/update)

2. **Setup**
   - Create `vget` system user (if configured to run as non-root)
   - Create output directory with proper permissions
   - Copy binary to `/usr/local/bin/vget`

3. **Service installation**
   - Write service file to `/etc/systemd/system/vget.service`
   - Write config to `/etc/vget/config.yml`
   - Run `systemctl daemon-reload`
   - Run `systemctl enable vget`
   - Run `systemctl start vget`

4. **Success screen**
   ```
   ┌─────────────────────────────────────────────────────────────┐
   │                                                             │
   │   ✓ vget service installed successfully!                   │
   │                                                             │
   │   WebUI:    http://localhost:8080                           │
   │   Status:   sudo systemctl status vget                      │
   │   Logs:     sudo journalctl -u vget -f                      │
   │   Stop:     sudo systemctl stop vget                        │
   │   Remove:   sudo vget server uninstall                      │
   │                                                             │
   └─────────────────────────────────────────────────────────────┘
   ```

### Generated systemd Service File

```ini
# /etc/systemd/system/vget.service
[Unit]
Description=vget media downloader server
After=network.target

[Service]
Type=simple
User=vget
Group=vget
ExecStart=/usr/local/bin/vget server start --config /etc/vget/config.yml
Restart=always
RestartSec=5

[Install]
WantedBy=multi-user.target
```

### CLI Flags (non-interactive mode)

```bash
# Skip TUI, use defaults
sudo vget server install -y

# Custom configuration
sudo vget server install -p 9000 -o /data/downloads -u root

# Uninstall
sudo vget server uninstall
```

### Platform Support

Currently only Linux with systemd is supported.

On unsupported platforms, the command exits immediately with a helpful message:

```
$ vget server install

vget server install is only supported on Linux with systemd.

To run vget as a service on macOS, see:
https://github.com/guiyumin/vget/blob/main/docs/manual-service-setup.md

$ echo $?
0
```

No TUI is shown - just a clear message and clean exit.


================================================
FILE: docs/multi-binary-architecture.md
================================================
# Multi-Binary Architecture

## Overview

vget is split into separate binaries with a shared core module:

| Binary | Purpose | Distribution |
|--------|---------|--------------|
| `vget` | CLI tool | GitHub Releases (all platforms) |
| `vget-server` | HTTP server + Web UI | GitHub Releases + Docker Image |
| `vget-desktop` | Desktop GUI (PySide6) | Separate private repo |

## Current Structure

```
cmd/
  vget/main.go              # CLI entry point
  vget-server/main.go       # Server entry point

internal/
  core/                     # Shared by all binaries
    config/                 # Config file management
    downloader/             # Download logic, progress callbacks
    extractor/              # URL matching, media extraction
    i18n/                   # Translations
    tracker/                # Package tracking (kuaidi100)
    version/                # Version info
    webdav/                 # WebDAV client

  cli/                      # CLI-specific (Cobra + Bubbletea TUI)
  server/                   # Server-specific (HTTP + job queue + embedded UI)
  updater/                  # Self-update (CLI only)
```

## Build Commands

```bash
# CLI only
go build -o build/vget ./cmd/vget

# Server (works on all platforms)
go build -o build/vget-server ./cmd/vget-server

# Both
go build ./cmd/...
```

## Binary Comparison

| Binary | Size | Contains |
|--------|------|----------|
| `vget` | ~28 MB | CLI commands, Bubbletea TUI, extractors, downloaders |
| `vget-server` | ~25 MB | HTTP server, embedded Web UI, extractors, downloaders |

The server binary is smaller because it doesn't include CLI components (Cobra commands, Bubbletea TUI).

## Docker

The Docker image uses `vget-server` directly:

```dockerfile
# Build
RUN go build -ldflags="-s -w" -o /vget-server ./cmd/vget-server

# Run
ENTRYPOINT ["entrypoint.sh"]  # Runs vget-server
```

## vget-server CLI

```bash
# Start server with defaults (port 8080)
vget-server

# Custom port
vget-server -port 9000

# Custom output directory
vget-server -output /path/to/downloads

# Show version
vget-server -version
```

Configuration is read from `~/.config/vget/config.yml` (same as CLI).

## Release Artifacts

| Platform | CLI | Server |
|----------|-----|--------|
| Linux amd64 | vget-linux-amd64 | vget-server-linux-amd64 |
| Linux arm64 | vget-linux-arm64 | vget-server-linux-arm64 |
| macOS amd64 | vget-darwin-amd64 | vget-server-darwin-amd64 |
| macOS arm64 | vget-darwin-arm64 | vget-server-darwin-arm64 |
| Windows | vget-windows-amd64.exe | vget-server-windows-amd64.exe |
| Docker | - | guiyumin/vget |

## Desktop App

The desktop app (`vget-desktop`) is maintained in a separate private repository. It is built with PySide6, the official Python binding for Qt 6.


================================================
FILE: docs/seedbox.md
================================================
# Seedbox Support

## Overview

Extend vget to support seedboxes as remote torrent clients. Unlike NAS mode (dispatch only), seedbox mode includes:
1. **Dispatch** - Send magnet/torrent to seedbox
2. **Browse** - List files on seedbox
3. **Download** - Fetch completed files to NAS/local via HTTP/HTTPS or SFTP

## Motivation

Seedboxes are remote servers with high-bandwidth connections, commonly used for:
- Fast torrent downloads (datacenter speeds)
- Maintaining seed ratios on private trackers
- Avoiding ISP throttling/detection of P2P traffic

Users want to:
1. Send torrents to seedbox from vget
2. Browse what's on the seedbox
3. Download completed files via HTTP/HTTPS (looks like normal web traffic to ISP)

## Architecture

```
┌─────────────────────────────────────────────────────────────────────┐
│  vget (Docker or CLI)                                               │
│                                                                     │
│  ┌─────────────┐    ┌─────────────┐    ┌─────────────┐             │
│  │   Dispatch  │    │   Browse    │    │  Download   │             │
│  │   Torrent   │    │   Files     │    │   Files     │             │
│  └──────┬──────┘    └──────┬──────┘    └──────┬──────┘             │
└─────────┼──────────────────┼──────────────────┼─────────────────────┘
          │                  │                  │
          │ RPC/API          │ SFTP/HTTP        │ HTTP/SFTP
          ▼                  ▼                  ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Seedbox                                                            │
│                                                                     │
│  ┌─────────────────┐    ┌─────────────────┐                        │
│  │ Torrent Client  │    │   File Server   │                        │
│  │ - rTorrent      │    │ - nginx/HTTP    │                        │
│  │ - Deluge        │    │ - SFTP (SSH)    │                        │
│  │ - qBittorrent   │    │                 │                        │
│  │ - Transmission  │    │                 │                        │
│  └─────────────────┘    └─────────────────┘                        │
│           │                     │                                   │
│           ▼                     ▼                                   │
│  ┌─────────────────────────────────────────┐                       │
│  │           /downloads/                    │                       │
│  │  ├── movie.mkv                          │                       │
│  │  ├── album/                             │                       │
│  │  │   ├── 01-track.flac                  │                       │
│  │  │   └── 02-track.flac                  │                       │
│  │  └── series/                            │                       │
│  └─────────────────────────────────────────┘                       │
└─────────────────────────────────────────────────────────────────────┘
          │
          │ HTTP/HTTPS or SFTP
          ▼
┌─────────────────────────────────────────────────────────────────────┐
│  Destination (NAS or Local)                                         │
│  /volume1/downloads/ or ~/Downloads/                                │
└─────────────────────────────────────────────────────────────────────┘
```

## Supported Torrent Clients

### Existing (from NAS mode)
| Client | Protocol | Default Port | Status |
|--------|----------|--------------|--------|
| Transmission | JSON-RPC | 9091 | Done |
| qBittorrent | REST API | 8080 | Done |
| Synology DS | REST API | 5000/5001 | Done |

### New (for seedbox)
| Client | Protocol | Default Port | Status |
|--------|----------|--------------|--------|
| rTorrent | XML-RPC | 8080 (via ruTorrent) | TODO |
| Deluge | JSON-RPC | 8112 | TODO |

### rTorrent (XML-RPC)

Most common on seedboxes. Usually accessed via:
- ruTorrent web UI (PHP frontend)
- Direct XML-RPC endpoint (often `/RPC2` or `/rutorrent/plugins/httprpc/action.php`)

```go
// internal/torrent/rtorrent.go
type RTorrentClient struct {
    endpoint string  // e.g., "https://seedbox.example.com/RPC2"
    username string
    password string
}

// XML-RPC methods:
// - load.raw_start (add torrent from base64 data)
// - load.start (add torrent from URL/magnet)
// - d.multicall2 (list torrents)
// - d.name, d.size_bytes, d.completed_bytes, d.ratio, etc.
```

### Deluge (JSON-RPC)

Popular alternative with web UI.

```go
// internal/torrent/deluge.go
type DelugeClient struct {
    host     string  // e.g., "seedbox.example.com:8112"
    password string  // Deluge uses single password, no username
    useTLS   bool
}

// JSON-RPC methods (via /json endpoint):
// - auth.login
// - core.add_torrent_magnet
// - core.add_torrent_url
// - core.get_torrents_status
```

## File Access Methods

### HTTP/HTTPS (Primary)

Most seedboxes run nginx/apache serving the downloads directory. This is ideal because:
- Looks like normal web traffic to ISP
- No P2P protocol detection
- Often faster than SFTP for large files
- Resume support via Range headers

```
Seedbox URL: https://user.seedbox.io/downloads/
            https://user.seedbox.io/downloads/movie.mkv
            https://user.seedbox.io/downloads/album/01-track.flac
```

**Implementation:**
```go
// internal/seedbox/http.go
type HTTPFileServer struct {
    baseURL  string  // e.g., "https://user.seedbox.io/downloads/"
    username string  // HTTP Basic Auth
    password string
}

func (h *HTTPFileServer) List(path string) ([]FileInfo, error)
func (h *HTTPFileServer) Download(remotePath, localPath string, progress func(int64, int64)) error
```

**Directory listing:** Parse HTML index page or use JSON index if available.

### SFTP (Alternative)

Universal fallback - every seedbox has SSH access.

```go
// internal/seedbox/sftp.go
type SFTPFileServer struct {
    host       string  // e.g., "seedbox.example.com:22"
    username   string
    password   string  // or privateKey
    privateKey string  // path to SSH key
    basePath   string  // e.g., "/home/user/downloads"
}

func (s *SFTPFileServer) List(path string) ([]FileInfo, error)
func (s *SFTPFileServer) Download(remotePath, localPath string, progress func(int64, int64)) error
```

**Library:** Use `github.com/pkg/sftp` with `golang.org/x/crypto/ssh`

### FileInfo Structure

```go
// internal/seedbox/types.go
type FileInfo struct {
    Name    string    `json:"name"`
    Path    string    `json:"path"`     // relative path from base
    Size    int64     `json:"size"`
    IsDir   bool      `json:"isDir"`
    ModTime time.Time `json:"modTime"`
}
```

## UI Design

### Seedbox Page (Web UI)

```
┌─────────────────────────────────────────────────────────────────┐
│  Seedbox                                              [Settings]│
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  ┌─── Add Torrent ───────────────────────────────────────────┐ │
│  │                                                           │ │
│  │  [Magnet link or .torrent URL                        ]   │ │
│  │                                                           │ │
│  │  [ ] Start paused                      [Send to Seedbox] │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  ┌─── Browse Files ──────────────────────────────────────────┐ │
│  │                                                           │ │
│  │  Path: /downloads/                            [Refresh]   │ │
│  │  ─────────────────────────────────────────────────────── │ │
│  │  [ ] Name                          Size        Modified   │ │
│  │  ─────────────────────────────────────────────────────── │ │
│  │  [ ] 📁 movies/                     -          2024-01-15│ │
│  │  [ ] 📁 music/                      -          2024-01-14│ │
│  │  [x] 📄 ubuntu-24.04.iso           4.7 GB     2024-01-13│ │
│  │  [ ] 📄 document.pdf               2.3 MB     2024-01-12│ │
│  │  ─────────────────────────────────────────────────────── │ │
│  │                                                           │ │
│  │  Selected: 1 file (4.7 GB)                               │ │
│  │                                                           │ │
│  │  Download to: [/volume1/downloads     ▼]  [Download]     │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  ┌─── Active Torrents ───────────────────────────────────────┐ │
│  │                                                           │ │
│  │  ubuntu-24.04.iso                                        │ │
│  │  ████████████████████████████░░░░░░  85%  12.3 MB/s      │ │
│  │                                                           │ │
│  │  archlinux-2024.01.01.iso                                │ │
│  │  ████████████████████████████████████  100%  Seeding     │ │
│  │                                                           │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### Settings Modal

```
┌─────────────────────────────────────────────────────────────────┐
│  Seedbox Settings                                          [X] │
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Torrent Client                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Client:    [rTorrent (ruTorrent)  ▼]                   │   │
│  │  RPC URL:   [https://my.seedbox.io/rutorrent/plugins/ht]│   │
│  │  Username:  [myuser                  ]                   │   │
│  │  Password:  [••••••••                ]                   │   │
│  │                                    [Test Connection]     │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  File Access                                                    │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Method:    (•) HTTP/HTTPS    ( ) SFTP                  │   │
│  │                                                         │   │
│  │  ── HTTP/HTTPS Settings ──                              │   │
│  │  Base URL:  [https://my.seedbox.io/downloads/       ]   │   │
│  │  Username:  [myuser                  ]                   │   │
│  │  Password:  [••••••••                ]                   │   │
│  │                                                         │   │
│  │  ── SFTP Settings (if selected) ──                      │   │
│  │  Host:      [my.seedbox.io:22        ]                   │   │
│  │  Username:  [myuser                  ]                   │   │
│  │  Auth:      (•) Password  ( ) SSH Key                   │   │
│  │  Password:  [••••••••                ]                   │   │
│  │  Base Path: [/home/myuser/downloads  ]                   │   │
│  │                                    [Test Connection]     │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│  Default Download Location                                      │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  Path:      [/volume1/downloads      ]                   │   │
│  └─────────────────────────────────────────────────────────┘   │
│                                                                 │
│                              [Cancel]  [Save]                   │
└─────────────────────────────────────────────────────────────────┘
```

## API Endpoints

### Torrent Dispatch
```
POST /api/seedbox/torrent
{
  "magnet": "magnet:?xt=urn:btih:...",
  // or
  "torrentUrl": "https://example.com/file.torrent",
  "startPaused": false
}
```

### List Torrents
```
GET /api/seedbox/torrents

Response:
{
  "torrents": [
    {
      "id": "abc123",
      "name": "ubuntu-24.04.iso",
      "size": 5000000000,
      "downloaded": 4250000000,
      "progress": 85,
      "speed": 12900000,
      "status": "downloading",  // downloading, seeding, paused, error
      "ratio": 0.5
    }
  ]
}
```

### Browse Files
```
GET /api/seedbox/files?path=/downloads/

Response:
{
  "path": "/downloads/",
  "files": [
    {"name": "movies", "path": "/downloads/movies/", "isDir": true, "size": 0, "modTime": "..."},
    {"name": "ubuntu.iso", "path": "/downloads/ubuntu.iso", "isDir": false, "size": 5000000000, "modTime": "..."}
  ]
}
```

### Download Files
```
POST /api/seedbox/download
{
  "files": [
    "/downloads/ubuntu.iso",
    "/downloads/movies/"
  ],
  "destination": "/volume1/downloads"
}

Response:
{
  "taskId": "download-123",
  "status": "started"
}
```

### Download Progress
```
GET /api/seedbox/download/download-123

Response:
{
  "taskId": "download-123",
  "status": "in_progress",  // in_progress, completed, failed
  "files": [
    {"path": "/downloads/ubuntu.iso", "progress": 45, "speed": 50000000}
  ],
  "totalSize": 5000000000,
  "downloaded": 2250000000
}
```

## Configuration

### Config File Structure

```yaml
# ~/.config/vget/config.yml

seedbox:
  enabled: true

  # Torrent client settings
  client: rtorrent  # rtorrent, deluge, qbittorrent, transmission
  clientHost: "https://my.seedbox.io/rutorrent/plugins/httprpc/action.php"
  clientUsername: "myuser"
  clientPassword: "secret"

  # File access settings
  fileAccess: http  # http or sftp

  # HTTP settings (when fileAccess: http)
  httpBaseURL: "https://my.seedbox.io/downloads/"
  httpUsername: "myuser"
  httpPassword: "secret"

  # SFTP settings (when fileAccess: sftp)
  sftpHost: "my.seedbox.io:22"
  sftpUsername: "myuser"
  sftpPassword: "secret"
  sftpPrivateKey: ""  # path to SSH key, alternative to password
  sftpBasePath: "/home/myuser/downloads"

  # Download settings
  defaultDownloadPath: "/volume1/downloads"
```

### Go Config Struct

```go
// internal/core/config/config.go

type SeedboxConfig struct {
    Enabled bool `yaml:"enabled"`

    // Torrent client
    Client         string `yaml:"client"`          // rtorrent, deluge, qbittorrent, transmission
    ClientHost     string `yaml:"clientHost"`
    ClientUsername string `yaml:"clientUsername"`
    ClientPassword string `yaml:"clientPassword"`

    // File access
    FileAccess string `yaml:"fileAccess"`  // http or sftp

    // HTTP settings
    HTTPBaseURL  string `yaml:"httpBaseURL"`
    HTTPUsername string `yaml:"httpUsername"`
    HTTPPassword string `yaml:"httpPassword"`

    // SFTP settings
    SFTPHost       string `yaml:"sftpHost"`
    SFTPUsername   string `yaml:"sftpUsername"`
    SFTPPassword   string `yaml:"sftpPassword"`
    SFTPPrivateKey string `yaml:"sftpPrivateKey"`
    SFTPBasePath   string `yaml:"sftpBasePath"`

    // Download settings
    DefaultDownloadPath string `yaml:"defaultDownloadPath"`
}
```

## Implementation Plan

### Phase 1: New Torrent Clients
- [ ] `internal/torrent/rtorrent.go` - rTorrent XML-RPC client
- [ ] `internal/torrent/deluge.go` - Deluge JSON-RPC client
- [ ] Add to client factory in `internal/torrent/client.go`
- [ ] Test with Docker containers

### Phase 2: File Access Layer
- [ ] `internal/seedbox/types.go` - FileInfo, interfaces
- [ ] `internal/seedbox/http.go` - HTTP/HTTPS file browser/downloader
- [ ] `internal/seedbox/sftp.go` - SFTP file browser/downloader
- [ ] `internal/seedbox/manager.go` - Factory and download task management

### Phase 3: Backend API
- [ ] Add SeedboxConfig to `internal/core/config/config.go`
- [ ] `internal/server/seedbox.go` - API handlers
- [ ] Register routes in `internal/server/server.go`
- [ ] Download task queue with progress tracking

### Phase 4: Frontend UI
- [ ] `ui/src/pages/SeedboxPage.tsx` - Main page component
- [ ] `ui/src/components/SeedboxTorrent.tsx` - Add torrent form
- [ ] `ui/src/components/SeedboxBrowser.tsx` - File browser
- [ ] `ui/src/components/SeedboxDownloads.tsx` - Download progress
- [ ] `ui/src/components/SeedboxSettings.tsx` - Settings modal
- [ ] Add to sidebar, routes, translations

### Phase 5: Polish
- [ ] i18n translations (all locales)
- [ ] Error handling and retry logic
- [ ] Connection testing in settings
- [ ] Documentation

## Testing

### Local Testing with Docker

```bash
# rTorrent with ruTorrent
docker run -d --name rutorrent \
  -p 8080:8080 \
  -p 45000:45000 \
  crazymax/rtorrent-rutorrent

# Deluge
docker run -d --name deluge \
  -p 8112:8112 \
  linuxserver/deluge

# nginx for HTTP file serving (simulate seedbox HTTP)
docker run -d --name nginx-files \
  -p 8081:80 \
  -v /tmp/downloads:/usr/share/nginx/html:ro \
  nginx
```

### Test Files
Use legal test torrents (Ubuntu, Arch Linux ISOs, etc.)

## Security Considerations

- Credentials stored in config file (same as other sensitive data)
- HTTPS strongly recommended for HTTP file access
- SSH key authentication preferred over password for SFTP
- No auto-discovery to avoid network scanning

## Differences from NAS Mode

| Aspect | NAS Mode | Seedbox Mode |
|--------|----------|--------------|
| Location | Local network | Remote server |
| Dispatch | Yes | Yes |
| Browse files | No (use NAS UI) | Yes |
| Download back | No (already local) | Yes (HTTP/SFTP) |
| ISP visibility | N/A | Hidden (HTTP looks normal) |
| Speed | LAN speed | Internet speed |
| Use case | Home NAS | Remote seedbox |

## Future Enhancements (Not Planned)

- Automatic sync (watch folder)
- Webhook notifications on completion
- Multiple seedbox profiles
- Bandwidth scheduling
- Integration with Plex/Jellyfin for auto-scan


================================================
FILE: docs/tauri.md
================================================
# vget Desktop App - Tauri Implementation Plan

## Goal
Build a desktop app version of vget using Tauri 2.0 with React frontend and Rust backend, matching the tech stack from maily.

---

## Tech Stack (Aligned with maily)

### Frontend
- **React** 19.x
- **Vite** 7.x with `@vitejs/plugin-react`
- **TanStack Router** (file-based routing)
- **Tailwind CSS** 4.x via `@tailwindcss/vite`
- **shadcn/ui** (new-york style) with Radix primitives
- **Zustand** 5.x for state management
- **lucide-react** for icons
- **sonner** for toasts
- **bun** as package manager

### Backend (Rust)
- **Tauri** 2.0
- **tokio** for async runtime
- **reqwest** for HTTP client
- **serde** + serde_json + serde_yaml
- **rusqlite** for local database (download history)
- **dirs** for config paths
- **tauri-plugin-dialog** for file dialogs
- **tauri-plugin-updater** for auto-updates
- **tauri-plugin-process** for app control

### vget-specific Rust crates
- **m3u8-rs** for HLS parsing
- **aes** + **cbc** for HLS decryption
- **chromiumoxide** for browser automation (Xiaohongshu)
- **ffmpeg-sidecar** for video/audio merging

---

## Project Structure

```
vget-desktop/
├── package.json
├── bun.lock
├── vite.config.ts
├── tsconfig.json
├── components.json          # shadcn/ui config
├── index.html
├── Makefile
│
├── src/                     # React frontend
│   ├── main.tsx
│   ├── index.css
│   ├── routeTree.gen.ts    # Auto-generated by TanStack
│   ├── routes/
│   │   ├── __root.tsx
│   │   ├── index.tsx       # Main download page
│   │   ├── history.tsx     # Download history
│   │   └── settings.tsx    # Configuration
│   ├── components/
│   │   ├── ui/             # shadcn/ui components
│   │   ├── URLInput.tsx
│   │   ├── DownloadCard.tsx
│   │   ├── ProgressBar.tsx
│   │   ├── FormatSelector.tsx
│   │   └── UpdateNotification.tsx
│   ├── stores/
│   │   ├── downloads.ts    # Download queue state
│   │   └── config.ts       # App config state
│   ├── hooks/
│   │   └── useDownload.ts
│   └── lib/
│       └── utils.ts
│
├── src-tauri/
│   ├── Cargo.toml
│   ├── tauri.conf.json
│   ├── build.rs
│   ├── icons/
│   └── src/
│       ├── main.rs
│       ├── lib.rs          # Tauri commands registration
│       ├── config.rs       # Config management
│       ├── db.rs           # SQLite for history
│       ├── extractor/      # Site extractors
│       │   ├── mod.rs      # Extractor trait + registry
│       │   ├── types.rs    # Media types
│       │   ├── direct.rs
│       │   ├── m3u8.rs
│       │   ├── twitter.rs
│       │   ├── bilibili.rs
│       │   ├── xiaoyuzhou.rs
│       │   ├── itunes.rs
│       │   ├── xiaohongshu.rs
│       │   └── youtube.rs
│       ├── downloader/
│       │   ├── mod.rs
│       │   ├── simple.rs
│       │   ├── multistream.rs
│       │   └── hls.rs
│       └── ffmpeg.rs
│
└── public/
    └── favicon.ico
```

---

## Implementation Phases

### Phase 1: Project Setup
1. Create new Tauri 2.0 project with React + TypeScript template
2. Configure Vite with TanStack Router plugin and Tailwind
3. Setup shadcn/ui with new-york style
4. Create basic Rust module structure
5. Configure tauri.conf.json with app metadata

**Files to create:**
- `package.json`, `vite.config.ts`, `tsconfig.json`
- `components.json`, `src/index.css`
- `src-tauri/Cargo.toml`, `src-tauri/tauri.conf.json`
- Basic route files and lib.rs

### Phase 2: Core Types & Direct Downloads
1. Define Rust types: `Media`, `VideoMedia`, `AudioMedia`, `ImageMedia`, `Format`
2. Implement `Extractor` trait and registry pattern
3. Implement `DirectExtractor` (file URL detection)
4. Implement simple HTTP downloader with progress events
5. Create frontend: URL input, progress display, basic settings

**Tauri commands:**
```rust
#[tauri::command]
async fn extract_media(url: String) -> Result<MediaInfo, String>

#[tauri::command]
async fn start_download(url: String, output_path: String, format_id: Option<String>) -> Result<String, String>

#[tauri::command]
fn cancel_download(job_id: String) -> Result<(), String>

#[tauri::command]
fn get_config() -> Result<Config, String>

#[tauri::command]
fn save_config(config: Config) -> Result<(), String>
```

### Phase 3: Core Extractors
1. **M3U8Extractor** - HLS stream detection
2. **TwitterExtractor** - Syndication API + GraphQL fallback
3. **BilibiliExtractor** - WBI signing, DASH streams
4. **XiaoyuzhouExtractor** - Podcast episodes
5. **iTunesExtractor** - Apple Podcasts

### Phase 4: Advanced Downloaders
1. **Multi-stream downloader** - 12 parallel streams with resume
2. **HLS downloader** - M3U8 parsing, segment download, AES decryption
3. **FFmpeg integration** - Bundle ffmpeg-sidecar for merging

### Phase 5: Browser Automation
1. Integrate chromiumoxide for browser automation
2. Implement stealth techniques (anti-bot detection)
3. **XiaohongshuExtractor** - Browser-based extraction
4. Cookie persistence for authenticated sites

### Phase 6: Polish & Distribution
1. Download history with SQLite
2. Auto-updater configuration
3. i18n support (start with en, zh)
4. Cross-platform testing and builds
5. GitHub releases setup

---

## Key Tauri Commands (Full List)

```rust
// Extraction
extract_media(url: String) -> Result<MediaInfo, String>

// Downloads
start_download(url: String, output_path: String, format_id: Option<String>) -> Result<String, String>
cancel_download(job_id: String) -> Result<(), String>
get_download_status(job_id: String) -> Result<DownloadStatus, String>

// Config
get_config() -> Result<Config, String>
save_config(config: Config) -> Result<(), String>
select_output_directory() -> Result<Option<String>, String>

// History
list_downloads(limit: usize, offset: usize) -> Result<Vec<DownloadRecord>, String>
clear_history() -> Result<(), String>
```

**Events (Rust → Frontend):**
- `download-progress` - Progress updates
- `download-complete` - Download finished
- `download-error` - Download failed

---

## Rust Dependencies (Cargo.toml)

```toml
[dependencies]
tauri = { version = "2", features = [] }
tauri-plugin-dialog = "2"
tauri-plugin-updater = "2"
tauri-plugin-process = "2"
tauri-plugin-opener = "2"

serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_yaml = "0.9"

tokio = { version = "1", features = ["rt-multi-thread", "sync", "time", "fs"] }
reqwest = { version = "0.12", features = ["json", "cookies", "gzip", "stream"] }

rusqlite = { version = "0.32", features = ["bundled"] }
dirs = "6"
uuid = { version = "1", features = ["v4", "serde"] }
chrono = { version = "0.4", features = ["serde"] }

# HLS
m3u8-rs = "6"
aes = "0.8"
cbc = "0.1"
hex = "0.4"

# FFmpeg
ffmpeg-sidecar = "2"

# Browser automation (for Xiaohongshu)
chromiumoxide = { version = "0.7", optional = true }

[features]
default = []
browser = ["chromiumoxide"]
```

---

## Frontend Dependencies (package.json)

```json
{
  "dependencies": {
    "@tauri-apps/api": "^2",
    "@tauri-apps/plugin-dialog": "^2",
    "@tauri-apps/plugin-updater": "^2",
    "@tauri-apps/plugin-process": "^2",
    "@tanstack/react-router": "^1.147",
    "zustand": "^5",
    "react": "^19",
    "react-dom": "^19",
    "lucide-react": "^0.562",
    "sonner": "^2",
    "tailwindcss": "^4.1",
    "class-variance-authority": "^0.7",
    "clsx": "^2.1",
    "tailwind-merge": "^3.4"
  },
  "devDependencies": {
    "@tanstack/router-plugin": "^1.149",
    "@tauri-apps/cli": "^2",
    "@tailwindcss/vite": "^4.1",
    "@vitejs/plugin-react": "^4.6",
    "typescript": "~5.8",
    "vite": "^7"
  }
}
```

---

## Extractor Priority (What to Build First)

| Order | Extractor | Effort | Notes |
|-------|-----------|--------|-------|
| 1 | Direct | Low | Foundation - file URL detection |
| 2 | M3U8 | Medium | HLS support, needed by others |
| 3 | Twitter | Medium | Popular, API-based |
| 4 | Bilibili | Medium | WBI signing complexity |
| 5 | Xiaoyuzhou | Low | Simple HTML parsing |
| 6 | iTunes | Low | RSS/JSON API |
| 7 | YouTube | Medium | yt-dlp subprocess wrapper |
| 8 | Xiaohongshu | High | Browser automation required |

---

## Verification

1. **Build test:** `cd src-tauri && cargo build`
2. **Dev mode:** `bun tauri dev`
3. **Test download:** Paste a Twitter/Bilibili URL and verify download completes
4. **Test progress:** Verify real-time progress updates in UI
5. **Test config:** Change output directory, verify persistence
6. **Production build:** `bun tauri build`

---

## Release Strategy

### Separate Release Cycles

CLI and Desktop have independent release cycles to allow focused development on either without forcing synchronized releases.

### Git Tags

| Product | Version Tags | Latest Tag | Example |
|---------|--------------|------------|---------|
| CLI | `v0.x.x` | `latest` | `v0.12.13` |
| Desktop | `desktop-v0.x.x` | `desktop-latest` | `desktop-v0.1.0` |

### Auto-Updater Endpoints

- **CLI**: Checks `https://github.com/guiyumin/vget/releases/download/latest/...`
- **Desktop**: Checks `https://github.com/guiyumin/vget/releases/download/desktop-latest/latest.json`

### Release Workflow

**CLI Release:**
```bash
# 1. Update version in internal/version/version.go
# 2. Commit and tag
git tag v0.12.13
git push origin v0.12.13

# 3. Update 'latest' tag
git tag -f latest
git push -f origin latest
```

**Desktop Release:**
```bash
# 1. Update version in tauri/src-tauri/tauri.conf.json
# 2. Commit and tag
git tag desktop-v0.1.0
git push origin desktop-v0.1.0

# 3. Update 'desktop-latest' tag
git tag -f desktop-latest
git push -f origin desktop-latest
```

### Benefits

1. **Independent cycles** - Release CLI bug fixes without waiting for Desktop features
2. **Focused development** - Work on one product at a time without pressure
3. **Backward compatibility** - Existing CLI users continue getting updates from `latest`
4. **Clear separation** - Easy to track which version is which
5. **GitHub Actions** - Can have separate workflows triggered by tag patterns:
   - `v*` → Build CLI binaries
   - `desktop-v*` → Build Desktop app bundles

### GitHub Actions Setup (Recommended)

```yaml
# .github/workflows/release-cli.yml
on:
  push:
    tags:
      - 'v*'

# .github/workflows/release-desktop.yml
on:
  push:
    tags:
      - 'desktop-v*'
```

---

## Shared Config Directory

CLI and Desktop share the same config directory at `~/.config/vget/`. This ensures settings sync between both apps.

### Directory Structure

```
~/.config/vget/
├── config.yml              # Main configuration (shared by CLI & Desktop)
├── update.key              # Desktop auto-updater signing key (private)
├── update.key.pub          # Desktop auto-updater signing key (public)
├── auth.json               # Authentication tokens
├── xhs_cookies.json        # Xiaohongshu browser cookies
├── youtube_session.json    # YouTube session data
└── telegram/               # Telegram MTProto session
    └── session
```

### config.yml Format

```yaml
# vget configuration file
# Shared between CLI and Desktop

# Basic settings
language: zh                          # en, zh, jp, kr, es, fr, de
output_dir: /Users/yumin/Downloads/vget
format: mp4                           # mp4, webm, best
quality: best                         # best, 1080p, 720p, 480p

# WebDAV servers (for cloud storage like PikPak)
webdavServers:
    pikpak:
        url: https://dav.mypikpak.com
        username: your_username
        password: your_password

# Twitter/X authentication
twitter:
    auth_token: your_auth_token       # Required for NSFW content

# Bilibili authentication
bilibili:
    cookie: SESSDATA=xxx; bili_jct=xxx; DedeUserID=xxx

# Server mode settings (CLI only)
server:
    max_concurrent: 10

# Express tracking (CLI only)
express:
    kuaidi100:
        customer: xxx
        key: xxx
```

### Config File Locations by Platform

| Platform | Path |
|----------|------|
| macOS | `~/.config/vget/config.yml` |
| Linux | `~/.config/vget/config.yml` |
| Windows | `%USERPROFILE%\.config\vget\config.yml` |

**Note:** Desktop does NOT use `~/Library/Application Support/` on macOS to maintain compatibility with CLI.

### Rust Config Struct

```rust
// src-tauri/src/config.rs

pub struct Config {
    pub language: String,
    pub output_dir: String,
    pub format: String,
    pub quality: String,
    #[serde(rename = "webdavServers")]
    pub webdav_servers: HashMap<String, WebDAVServer>,
    pub twitter: TwitterConfig,
    pub bilibili: BilibiliConfig,
    pub server: ServerConfig,
    pub express: ExpressConfig,
}

pub struct TwitterConfig {
    pub auth_token: Option<String>,
}

pub struct BilibiliConfig {
    pub cookie: Option<String>,
}

pub struct WebDAVServer {
    pub url: String,
    pub username: String,
    pub password: String,
}
```

### TypeScript Config Interface

```typescript
// src/components/settings/types.ts

interface Config {
  language: string;
  output_dir: string;
  format: string;
  quality: string;
  webdav_servers: Record<string, WebDAVServer>;
  twitter: { auth_token: string | null };
  bilibili: { cookie: string | null };
  server: { max_concurrent: number };
  express: { kuaidi100: Kuaidi100Config | null };
}
```


================================================
FILE: docs/telegram.md
================================================
# Telegram Support

Implementation plan for Telegram media download support in vget.

## Overview

vget aims to be an all-in-one media downloader. Telegram support is part of this vision, even though `tdl` (6k+ stars) exists as a dedicated tool.

**Current**: Desktop session import using Telegram Desktop's API credentials.

**Future**: Full CLI Telegram client capabilities (phone login, QR login, etc.).

## Technical Background

### How Telegram Auth Works

```
api_id + api_hash  =  identifies THE APP (vget)
user session       =  identifies THE USER's account
```

- Sessions are tied to the `api_id` they were created with
- Desktop session import reuses existing login from Telegram Desktop
- No phone/SMS verification needed if user has Desktop installed

### API Credentials

Currently using Telegram Desktop's public credentials:

```go
const (
    TelegramDesktopAppID   = 2040
    TelegramDesktopAppHash = "b18441a1ff607e10a989891a5462e627"
)
```

These are safe to use:
- Already public (used by Telegram Desktop itself)
- Used by many third-party tools (tdl, etc.)
- Telegram cannot revoke without breaking Desktop app

Future: Register vget's own credentials for `--phone` login method.

### Login Methods & Ban Risk

| Method | API Credentials | Ban Risk | Why |
|--------|-----------------|----------|-----|
| `--import-desktop` | Desktop's (2040) | Low | Reusing session, same app identity |
| `--phone` (future) | vget's own | **Zero** | Fresh session with registered app |
| `--qr` (future) | vget's own | **Zero** | Fresh session with registered app |
| `--bot-token` (future) | N/A | **Zero** | Bot tokens are inherently safe |

## Dependencies

```go
github.com/gotd/td                    // Pure Go MTProto 2.0 implementation
github.com/gotd/td/session/tdesktop   // Desktop session import
```

## Implementation Status

### Phase 1: MVP (Implemented)

#### 1. Session Management Commands

```bash
vget telegram login                  # Shows available login methods
vget telegram login --import-desktop # Import from Telegram Desktop
vget telegram logout                 # Clear stored session
vget telegram status                 # Show login state
```

**Desktop import flow (`--import-desktop`):**
- Reads Desktop's `tdata/` directory
  - macOS: `~/Library/Application Support/Telegram Desktop/tdata/`
  - Linux: `~/.local/share/TelegramDesktop/tdata/`
  - Windows: `%APPDATA%/Telegram Desktop/tdata/`
  - **Custom path**: Set via `vget config set telegram.tdata_path /path/to/tdata`
- Imports session using Desktop's API credentials (2040)
- Session stored in `~/.config/vget/telegram/desktop-session.json`

#### Session Storage & Multi-Account

**Session file layout:**
```
~/.config/vget/telegram/
├── desktop-session.json        # Imported from Telegram Desktop (current)
└── cli-sessions/               # Future: phone/QR login sessions
    ├── account1.json
    └── account2.json
```

**Current behavior:**
- Desktop import stores session at `desktop-session.json`
- If Desktop has multiple accounts, vget imports the **first/primary** account
- Re-importing **overwrites** the previous session

**Multi-account workflow (current):**
1. Switch to desired account in Telegram Desktop
2. Run `vget telegram login --import-desktop`
3. vget now uses that account
4. To switch: repeat steps 1-2

**Future (full CLI client):**
```bash
# Phone login creates named session in cli-sessions/
vget telegram login --phone --name work
vget telegram login --phone --name personal

# Use specific account
vget --account work https://t.me/channel/123
```

For now, Telegram Desktop manages multi-account; vget imports whichever is active.

#### Future Login Methods

| Flag | Description | Status |
|------|-------------|--------|
| `--import-desktop` | Import from Telegram Desktop | Implemented |
| `--phone` | Phone + SMS/code verification | Planned |
| `--qr` | QR code login (scan with mobile) | Planned |
| `--bot-token` | Bot authentication | Planned |

**Phone login flow (`--phone`):**
1. User enters phone number
2. Telegram sends verification code:
   - **Primary**: In-app message to existing Telegram sessions (Desktop/mobile)
   - **Fallback**: SMS (if no active sessions or user requests it)
3. User enters code
4. (Optional) Enters 2FA password if enabled
5. Session created with vget's API credentials

**QR login flow (`--qr`):**
1. vget displays QR code in terminal
2. User scans with Telegram mobile app
3. Session created automatically
4. No phone number or code needed

**Bot token flow (`--bot-token`):**
1. User provides bot token from @BotFather
2. Authenticate as bot (limited permissions)
3. Useful for downloading from public channels only

#### 2. URL Parsing

Support these `t.me` formats:

| Format | Example | Type |
|--------|---------|------|
| Public channel | `https://t.me/channel/123` | Public |
| Private channel | `https://t.me/c/123456789/123` | Private |
| User/bot post | `https://t.me/username/123` | Public |
| Single from album | `https://t.me/channel/123?single` | Public |

#### 3. Single Message Download

```bash
vget https://t.me/somechannel/456
```

- Extract media (video/audio/document) from one message
- Download with progress bar (existing Bubbletea infrastructure)
- Save to current directory or `-o` path

#### 4. Media Type Detection

```go
MediaTypeVideo     // .mp4, .mov
MediaTypeAudio     // .mp3, .ogg voice messages
MediaTypeDocument  // .pdf, .zip, etc.
MediaTypePhoto     // .jpg (lower priority)
```

### Phase 2: Nice-to-Have

| Feature | Description |
|---------|-------------|
| Batch download | `vget https://t.me/channel/100-200` (range) |
| Resume | Continue interrupted downloads |
| Album support | Download all media from grouped messages |
| Channel dump | `vget https://t.me/channel --all` |

## File Structure

```
internal/core/extractor/
├── telegram.go              # Thin wrapper, registers extractor, re-exports
├── telegram/
│   ├── constants.go         # API credentials (DesktopAppID, DesktopAppHash)
│   ├── parser.go            # URL parsing
│   ├── session.go           # Session path/exists helpers
│   ├── media.go             # Media extraction helpers
│   ├── extractor.go         # Extractor implementation
│   ├── download.go          # Download functionality + DownloadWithOptions
│   └── takeout.go           # TakeoutSession for bulk downloads

internal/cli/
├── telegram.go              # login/logout/status commands
├── batch.go                 # Batch download with auto-takeout for Telegram
```

## vget vs tdl

| Aspect | tdl | vget |
|--------|-----|------|
| Scope | Telegram-only | Multi-platform |
| Features | Many advanced (batch, resume, takeout) | Simple + auto-takeout for batch |
| Philosophy | Power tool | All-in-one simplicity |

## Reference Implementation

The `tdl` project (github.com/iyear/tdl) was analyzed for patterns:

### Worth Borrowing

1. **URL Parsing** (`pkg/tmessage/parse.go`) - handles various t.me formats
2. **Media Extraction** (`core/tmedia/media.go`) - unified media type abstraction
3. **Middleware Pattern** - retry, recovery, flood-wait as composable layers

### Skip for MVP

- Iterator + Resume pattern (Phase 2)
- Data Center pooling (overkill for single downloads)

### Implemented from tdl

- **Takeout mode** - auto-enabled for batch downloads (2+ Telegram URLs)

## Protected/Restricted Content

### Understanding `noforwards` Flag

Channel owners can enable "Restrict saving content" which sets the `noforwards` flag on the channel or individual messages. This:
- Disables "Forward" button in official apps
- Disables "Save" button for media in official apps
- Shows "Saving content is restricted" message

### Why Downloads Still Work

**Key insight**: `noforwards` is a **client-side UI restriction**, not an API-level restriction.

The official Telegram apps *choose* to respect this flag by hiding UI buttons. But at the API level:
- If you have access to a message, you can read its content
- If you can read the content, you can download attached media
- The API does not block file downloads based on `noforwards`

This is how `tdl` and similar tools work - they use the Telegram Client API (MTProto) directly, bypassing the UI restrictions that official apps enforce.

### How tdl Detects Protected Content

From `tdl/core/forwarder/forwarder.go`:

```go
func protectedDialog(peer peers.Peer) bool {
    switch p := peer.(type) {
    case peers.Chat:
        return p.Raw().GetNoforwards()
    case peers.Channel:
        return p.Raw().GetNoforwards()
    }
    return false
}

func protectedMessage(msg *tg.Message) bool {
    return msg.GetNoforwards()
}
```

### Operation Differences

| Operation | Protected Content | How It Works |
|-----------|-------------------|--------------|
| **Download** | ✅ Works | Direct file access via API - `noforwards` doesn't apply |
| **Forward** | ⚠️ Blocked by API | Must use "clone" mode (re-upload as new message) |

### Takeout Mode for Bulk Downloads

For downloading many files, use Telegram's official "Data Export" feature via API:

```go
// From tdl/core/middlewares/takeout/takeout.go
req := &tg.AccountInitTakeoutSessionRequest{
    MessageChannels:   true,
    Files:             true,
    FileMaxSize:       4000 * 1024 * 1024,  // 4GB limit
}
```

Takeout sessions have **lower flood wait limits**, making bulk downloads faster and less likely to trigger rate limiting.

### vget Implementation Notes

For vget's Telegram support:
1. Desktop session import works for protected content - same API access as tdl
2. No special handling needed - just download the file if user has message access
3. Takeout mode is **auto-enabled** for batch downloads (2+ Telegram URLs)

## Takeout Mode (Implemented)

### What is Takeout?

Takeout is Telegram's official "Data Export" API feature (`AccountInitTakeoutSession`). It's designed for users to export their own data with **relaxed rate limits**.

| Without Takeout | With Takeout |
|-----------------|--------------|
| Normal flood wait limits | Lower flood wait limits |
| More likely to get rate-limited on bulk downloads | Designed for bulk export |
| Faster to hit `FLOOD_WAIT` errors | Can download more before limits |

### vget Implementation

Takeout is automatically enabled when batch downloading multiple Telegram URLs. No user flags needed.

**Usage:**

```bash
# Single file - no takeout (not needed)
vget https://t.me/channel/123

# Batch mode with 2+ Telegram URLs - takeout auto-enabled
vget -f urls.txt
```

**Implementation files:**

| File | Purpose |
|------|---------|
| `telegram/takeout.go` | `TakeoutSession` struct with `Start()`, `Finish()`, `Middleware()` |
| `telegram/download.go` | `DownloadWithOptions()` accepts `Takeout` bool |
| `cli/root.go` | `runTelegramBatchDownload()` uses takeout internally |
| `cli/batch.go` | Detects 2+ Telegram URLs → calls batch function |

**Core takeout logic:**

```go
// internal/extractor/telegram/takeout.go

type TakeoutSession struct {
    api       *tg.Client
    takeoutID int64
}

func (t *TakeoutSession) Start(ctx context.Context) error {
    req := &tg.AccountInitTakeoutSessionRequest{
        Files:       true,
        FileMaxSize: 4 * 1024 * 1024 * 1024, // 4GB
    }
    session, err := t.api.AccountInitTakeoutSession(ctx, req)
    if err != nil {
        return err
    }
    t.takeoutID = session.ID
    return nil
}

func (t *TakeoutSession) Finish(ctx context.Context) error {
    if t.takeoutID == 0 {
        return nil
    }
    req := &tg.AccountFinishTakeoutSessionRequest{Success: true}
    _, err := t.api.AccountFinishTakeoutSession(ctx, req)
    return err
}
```

**Batch download flow:**

```
urls.txt contains:
  https://t.me/channel/1
  https://t.me/channel/2
  https://twitter.com/user/status/123

vget -f urls.txt
  ↓
batch.go separates URLs:
  - telegramURLs: [t.me/1, t.me/2]  (2 URLs → use takeout)
  - otherURLs: [twitter.com/...]
  ↓
runTelegramBatchDownload(telegramURLs)
  → Each download uses takeout session
  ↓
runDownload() for other URLs
```

### Reference: How tdl Does It

tdl uses a similar pattern with middleware wrapping:

```go
// Wrap all API calls with takeout session ID
func (t takeout) Handle(next tg.Invoker) telegram.InvokeFunc {
    return func(ctx context.Context, input bin.Encoder, output bin.Decoder) error {
        return next.Invoke(ctx, &tg.InvokeWithTakeoutRequest{
            TakeoutID: t.id,
            Query:     nopDecoder{input},
        }, output)
    }
}
```

## References

- tdl source: https://github.com/iyear/tdl
- gotd/td (MTProto library): https://github.com/gotd/td
- Telegram Desktop session format: https://github.com/nickoala/tdesktop-session


================================================
FILE: docs/torrent-dispatch.md
================================================
# Torrent Dispatch Feature

## Overview

Allow vget to dispatch magnet links and .torrent files to remote torrent clients running on NAS devices or servers. vget does NOT download torrents itself - it only manages/dispatches jobs to existing torrent clients.

## Motivation

Chinese users requested BitTorrent support. Many use private trackers (PT sites) and have NAS devices (Synology, QNAP, etc.) running 24/7 with torrent clients. They want to quickly send magnets to their NAS without opening the web UI.

## Scope

### In Scope
- Send magnet links to remote torrent client
- Send .torrent file URLs to remote torrent client
- List active torrents (optional, for status checking)
- Support multiple torrent clients: Transmission, qBittorrent, Synology Download Station

### Out of Scope
- Actually downloading torrents (use existing clients)
- Torrent search/discovery
- Auto-detection of NAS devices (unreliable)
- Torrent client running inside vget Docker container

## Architecture

```
┌─────────────────────────────────────────────────────────┐
│  vget Docker (Web UI)                                   │
│                                                         │
│    Browser  →  POST /api/torrent  →  Torrent Library    │
│                                                         │
└─────────────────────┬───────────────────────────────────┘
                      │ HTTP/RPC
                      ▼
┌─────────────────────────────────────────────────────────┐
│  Remote Torrent Client (on NAS or server)               │
│  - Transmission (RPC on port 9091)                      │
│  - qBittorrent (Web API on port 8080)                   │
│  - Synology Download Station (API on port 5000/5001)    │
└─────────────────────────────────────────────────────────┘
```

## Implementation Status

### Completed
- [x] `internal/torrent/client.go` - Interface definition
- [x] `internal/torrent/transmission.go` - Transmission RPC client
- [x] `internal/torrent/qbittorrent.go` - qBittorrent Web API client
- [x] `internal/torrent/synology.go` - Synology Download Station client
- [x] `internal/core/config/config.go` - TorrentConfig struct added
- [x] `internal/server/server.go` - API endpoints added
- [x] `ui/src/routes/torrent.tsx` - Route file
- [x] `ui/src/pages/TorrentPage.tsx` - Page component
- [x] `ui/src/components/Torrent.tsx` - BT/Magnet submit component
- [x] `ui/src/components/TorrentSettings.tsx` - Settings component
- [x] `ui/src/components/Sidebar.tsx` - Menu item added
- [x] `ui/src/utils/translations.ts` - Translation strings added
- [x] `ui/src/utils/apis.ts` - API functions added
- [x] `ui/src/context/AppContext.tsx` - torrentEnabled state added

### TODO (Future)

- [x] Add backend i18n translations (internal/i18n/locales/*.yml)
- [ ] CLI support if users request it (vget bittorrent / bt / magnet / cili)

## Configuration

### Config File (~/.config/vget/config.yml)
```yaml
torrent:
  enabled: true
  client: transmission
  host: "192.168.1.100:9091"
  username: "admin"
  password: "secret"
```

### NOT in `vget init`
Torrent config is optional and should NOT be part of the initial setup wizard. Users configure it through the Web UI settings page.

## Supported Clients

| Client | Default Port | Protocol | Notes |
|--------|-------------|----------|-------|
| Transmission | 9091 | JSON-RPC | Most common on Linux NAS |
| qBittorrent | 8080 | REST API | Popular alternative |
| Synology DS | 5000/5001 | REST API | Built into Synology NAS |

## Testing

### Local Testing (without NAS)
```bash
# Run Transmission in Docker
docker run -d --name transmission \
  -p 9091:9091 \
  -e USER=admin \
  -e PASS=admin \
  linuxserver/transmission

# Run qBittorrent in Docker
docker run -d --name qbittorrent \
  -p 8080:8080 \
  linuxserver/qbittorrent
```

### Test Magnets
Use legal test torrents:
- Ubuntu ISO: `magnet:?xt=urn:btih:...` (search for current release)
- Blender Open Movies

## Security Considerations

- Torrent client credentials stored in config file (same as other credentials)
- HTTPS support for remote connections
- No auto-discovery to avoid network scanning concerns

## Future Enhancements (Not Planned)
- Deluge support
- Aria2 support (already has remote RPC)
- QNAP Download Station
- Torrent notifications via Telegram


================================================
FILE: docs/tui-file-browser.md
================================================
# TUI File Browser for Remote Paths

## Overview

When user runs `vget <remote>:/path/to/directory/`, instead of showing an error, vget displays an interactive TUI browser to navigate and select files for download.

## Behavior

```bash
vget pikpak:/电影/          # Directory → Opens TUI browser
vget pikpak:/电影/file.mkv  # File → Direct download
```

## Key Features

### Navigation
- `↑/↓` or `k/j`: Move cursor up/down
- `Enter`: Enter directory / Download file
- `b` or `Backspace` or `h`: Go up one directory
- `q` or `Esc`: Quit without downloading

### Display
- Current path shown as header
- Directories listed first (with 📁 icon), then files
- File sizes displayed
- Scrollable list with position indicator
- Highlighted cursor row

### Selection Behavior
- Enter on directory: navigate into it (stay in TUI)
- Enter on file: exit TUI and start download
- User can navigate through multiple directory levels before selecting a file

## Implementation

### Files
- `internal/cli/browse.go` - TUI browser component using Bubbletea
- `internal/cli/root.go` - Integration point in `runWebDAVDownload()`

### Flow
1. User runs `vget pikpak:/电影/`
2. vget detects it's a directory
3. TUI browser opens showing directory contents
4. User navigates and selects a file
5. TUI closes and download begins


================================================
FILE: docs/webdav-browsing.md
================================================
# WebDAV File Browsing (Web UI)

## Overview

Add file browsing capability to the vget Web UI for WebDAV remotes. Currently, browsing only works via CLI (`vget ls`). Users should be able to browse, navigate, and download files from WebDAV servers directly in the web interface.

## Current State

### What Works (CLI)
```bash
vget ls pikpak:/              # List root
vget ls pikpak:/Movies        # List subdirectory
vget pikpak:/Movies/film.mp4  # Download file
```

### What's Missing (Web UI)
- No way to browse WebDAV files in the browser
- Users must use CLI to discover file paths
- No visual navigation of remote directories
- No click-to-download functionality

## Motivation

The web UI is designed for convenience - users shouldn't need to switch to CLI just to browse files. A file browser makes WebDAV support actually usable:

1. **Discovery** - See what's available without memorizing paths
2. **Navigation** - Click through directories naturally
3. **Download** - Select files and download with one click
4. **Mobile-friendly** - Browse from phone/tablet (no CLI)

## Architecture

```
┌─────────────────────────────────────────────────────────────────┐
│  Web UI (Browser)                                               │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  File Browser Component                                  │   │
│  │  - Directory tree / breadcrumb navigation               │   │
│  │  - File list with size, type                            │   │
│  │  - Select & download actions                            │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              │ fetch()                          │
│                              ▼                                  │
└─────────────────────────────────────────────────────────────────┘
                               │
                               │ HTTP API
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│  vget Backend (Go)                                              │
│                                                                 │
│  GET /api/webdav/list?remote=pikpak&path=/Movies                │
│  POST /api/webdav/download                                      │
│                                                                 │
│  ┌─────────────────────────────────────────────────────────┐   │
│  │  internal/core/webdav/client.go                         │   │
│  │  - List() ✓ (already exists)                            │   │
│  │  - Stat() ✓ (already exists)                            │   │
│  │  - Open() ✓ (already exists)                            │   │
│  └─────────────────────────────────────────────────────────┘   │
│                              │                                  │
│                              │ WebDAV (PROPFIND/GET)           │
│                              ▼                                  │
└─────────────────────────────────────────────────────────────────┘
                               │
                               ▼
┌─────────────────────────────────────────────────────────────────┐
│  WebDAV Server (PikPak, Alist, Synology, etc.)                 │
└─────────────────────────────────────────────────────────────────┘
```

## UI Design

### WebDAV Page Layout

```
┌─────────────────────────────────────────────────────────────────┐
│  WebDAV                                               [Settings]│
├─────────────────────────────────────────────────────────────────┤
│                                                                 │
│  Remote: [pikpak          ▼]                                    │
│                                                                 │
│  ┌─── File Browser ──────────────────────────────────────────┐ │
│  │                                                           │ │
│  │  📍 pikpak: / Movies / Action /                           │ │
│  │  ─────────────────────────────────────────────────────── │ │
│  │  [ ] Name                          Size        Modified   │ │
│  │  ─────────────────────────────────────────────────────── │ │
│  │      📁 ..                          -                     │ │
│  │  [ ] 📁 Subtitles/                  -          2024-01-15│ │
│  │  [x] 📄 movie-1080p.mkv            4.7 GB     2024-01-13│ │
│  │  [ ] 📄 movie-720p.mkv             2.1 GB     2024-01-13│ │
│  │  [x] 📄 movie.srt                  45 KB      2024-01-13│ │
│  │  ─────────────────────────────────────────────────────── │ │
│  │                                                           │ │
│  │  Selected: 2 files (4.7 GB)                              │ │
│  │                                                           │ │
│  │  [Download Selected]  [Download All]                      │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
│  ┌─── Active Downloads ──────────────────────────────────────┐ │
│  │                                                           │ │
│  │  movie-1080p.mkv                                         │ │
│  │  ████████████████████░░░░░░░░░░░░  45%  23.5 MB/s        │ │
│  │                                                           │ │
│  │  movie.srt                                               │ │
│  │  ████████████████████████████████  100%  Complete        │ │
│  │                                                           │ │
│  └───────────────────────────────────────────────────────────┘ │
│                                                                 │
└─────────────────────────────────────────────────────────────────┘
```

### Navigation Features

1. **Breadcrumb** - Click any part to jump: `pikpak: / Movies / Action /`
2. **Parent directory** - `📁 ..` row to go up one level
3. **Click folder** - Navigate into subdirectory
4. **Click file** - Preview info or start download
5. **Checkbox select** - Multi-select for batch download

### Remote Selector

Dropdown shows all configured WebDAV servers from config:
```
[pikpak          ▼]
 ├─ pikpak
 ├─ alist
 └─ synology
```

## API Endpoints

### List Directory
```
GET /api/webdav/list?remote=pikpak&path=/Movies

Response:
{
  "remote": "pikpak",
  "path": "/Movies",
  "files": [
    {"name": "Action", "path": "/Movies/Action", "isDir": true, "size": 0, "modTime": "2024-01-15T10:30:00Z"},
    {"name": "movie.mkv", "path": "/Movies/movie.mkv", "isDir": false, "size": 5000000000, "modTime": "2024-01-13T08:00:00Z"}
  ]
}
```

### Get File Info
```
GET /api/webdav/info?remote=pikpak&path=/Movies/movie.mkv

Response:
{
  "name": "movie.mkv",
  "path": "/Movies/movie.mkv",
  "isDir": false,
  "size": 5000000000,
  "modTime": "2024-01-13T08:00:00Z",
  "downloadUrl": "pikpak:/Movies/movie.mkv"
}
```

### Download File(s)
```
POST /api/webdav/download
{
  "remote": "pikpak",
  "files": [
    "/Movies/movie.mkv",
    "/Movies/movie.srt"
  ],
  "outputDir": "/downloads"  // optional, uses default if not specified
}

Response:
{
  "taskId": "download-456",
  "status": "started",
  "files": [
    {"path": "/Movies/movie.mkv", "status": "queued"},
    {"path": "/Movies/movie.srt", "status": "queued"}
  ]
}
```

### Download Progress
```
GET /api/webdav/download/download-456

Response:
{
  "taskId": "download-456",
  "status": "in_progress",
  "files": [
    {"path": "/Movies/movie.mkv", "progress": 45, "speed": 24600000, "status": "downloading"},
    {"path": "/Movies/movie.srt", "progress": 100, "status": "completed"}
  ],
  "totalSize": 5000045000,
  "downloaded": 2250045000
}
```

### List Configured Remotes
```
GET /api/webdav/remotes

Response:
{
  "remotes": [
    {"name": "pikpak", "url": "https://dav.pikpak.com", "hasAuth": true},
    {"name": "alist", "url": "http://192.168.1.100:5244/dav", "hasAuth": true}
  ]
}
```

## Backend Implementation

### New Files
```
internal/server/webdav_browse.go    # API handlers for browsing
```

### Handler Code (webdav_browse.go)

```go
package server

import (
    "encoding/json"
    "net/http"

    "github.com/guiyumin/vget/internal/core/config"
    "github.com/guiyumin/vget/internal/core/webdav"
)

// GET /api/webdav/remotes
func (s *Server) handleWebDAVRemotes(w http.ResponseWriter, r *http.Request) {
    cfg := config.LoadOrDefault()

    remotes := make([]map[string]interface{}, 0)
    for name, server := range cfg.WebDAVServers {
        remotes = append(remotes, map[string]interface{}{
            "name":    name,
            "url":     server.URL,
            "hasAuth": server.Username != "",
        })
    }

    json.NewEncoder(w).Encode(map[string]interface{}{
        "remotes": remotes,
    })
}

// GET /api/webdav/list?remote=xxx&path=/xxx
func (s *Server) handleWebDAVList(w http.ResponseWriter, r *http.Request) {
    remoteName := r.URL.Query().Get("remote")
    path := r.URL.Query().Get("path")
    if path == "" {
        path = "/"
    }

    cfg := config.LoadOrDefault()
    server := cfg.GetWebDAVServer(remoteName)
    if server == nil {
        http.Error(w, "Remote not found", http.StatusNotFound)
        return
    }

    client, err := webdav.NewClientFromConfig(server)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    files, err := client.List(r.Context(), path)
    if err != nil {
        http.Error(w, err.Error(), http.StatusInternalServerError)
        return
    }

    // Convert to JSON response format
    // ... (format files array)

    json.NewEncoder(w).Encode(map[string]interface{}{
        "remote": remoteName,
        "path":   path,
        "files":  files,
    })
}

// POST /api/webdav/download
func (s *Server) handleWebDAVDownload(w http.ResponseWriter, r *http.Request) {
    // Parse request body
    // Queue download tasks
    // Return task ID for progress tracking
}
```

### Route Registration

```go
// internal/server/server.go

func (s *Server) setupRoutes() {
    // ... existing routes ...

    // WebDAV browsing
    s.router.HandleFunc("/api/webdav/remotes", s.handleWebDAVRemotes).Methods("GET")
    s.router.HandleFunc("/api/webdav/list", s.handleWebDAVList).Methods("GET")
    s.router.HandleFunc("/api/webdav/info", s.handleWebDAVInfo).Methods("GET")
    s.router.HandleFunc("/api/webdav/download", s.handleWebDAVDownload).Methods("POST")
    s.router.HandleFunc("/api/webdav/download/{taskId}", s.handleWebDAVDownloadProgress).Methods("GET")
}
```

## Frontend Implementation

### New Files
```
ui/src/pages/WebDAVPage.tsx           # Main page
ui/src/components/WebDAVBrowser.tsx   # File browser component
ui/src/components/WebDAVDownloads.tsx # Download progress component
ui/src/routes/webdav.tsx              # Route definition
```

### Component Structure

```tsx
// ui/src/pages/WebDAVPage.tsx

export function WebDAVPage() {
  const [selectedRemote, setSelectedRemote] = useState<string>("");
  const [currentPath, setCurrentPath] = useState<string>("/");
  const [files, setFiles] = useState<FileInfo[]>([]);
  const [selectedFiles, setSelectedFiles] = useState<Set<string>>(new Set());
  const [downloads, setDownloads] = useState<DownloadTask[]>([]);

  return (
    <div>
      <RemoteSelector
        value={selectedRemote}
        onChange={setSelectedRemote}
      />

      <WebDAVBrowser
        remote={selectedRemote}
        path={currentPath}
        files={files}
        selectedFiles={selectedFiles}
        onNavigate={setCurrentPath}
        onSelect={handleSelect}
        onDownload={handleDownload}
      />

      <WebDAVDownloads tasks={downloads} />
    </div>
  );
}
```

### File Browser Component

```tsx
// ui/src/components/WebDAVBrowser.tsx

interface WebDAVBrowserProps {
  remote: string;
  path: string;
  files: FileInfo[];
  selectedFiles: Set<string>;
  onNavigate: (path: string) => void;
  onSelect: (path: string, selected: boolean) => void;
  onDownload: (files: string[]) => void;
}

export function WebDAVBrowser(props: WebDAVBrowserProps) {
  // Breadcrumb navigation
  const pathParts = props.path.split('/').filter(Boolean);

  return (
    <div className="webdav-browser">
      {/* Breadcrumb */}
      <nav className="breadcrumb">
        <span onClick={() => props.onNavigate('/')}>{props.remote}:</span>
        <span> / </span>
        {pathParts.map((part, i) => (
          <span key={i}>
            <span onClick={() => props.onNavigate('/' + pathParts.slice(0, i + 1).join('/'))}>
              {part}
            </span>
            <span> / </span>
          </span>
        ))}
      </nav>

      {/* File list */}
      <table className="file-list">
        <thead>
          <tr>
            <th><input type="checkbox" /></th>
            <th>Name</th>
            <th>Size</th>
            <th>Modified</th>
          </tr>
        </thead>
        <tbody>
          {/* Parent directory */}
          {props.path !== '/' && (
            <tr onClick={() => props.onNavigate(getParentPath(props.path))}>
              <td></td>
              <td>📁 ..</td>
              <td>-</td>
              <td></td>
            </tr>
          )}

          {/* Files and folders */}
          {props.files.map(file => (
            <tr key={file.path}>
              <td>
                {!file.isDir && (
                  <input
                    type="checkbox"
                    checked={props.selectedFiles.has(file.path)}
                    onChange={(e) => props.onSelect(file.path, e.target.checked)}
                  />
                )}
              </td>
              <td onClick={() => file.isDir && props.onNavigate(file.path)}>
                {file.isDir ? '📁' : '📄'} {file.name}
              </td>
              <td>{file.isDir ? '-' : formatSize(file.size)}</td>
              <td>{formatDate(file.modTime)}</td>
            </tr>
          ))}
        </tbody>
      </table>

      {/* Actions */}
      <div className="actions">
        <span>Selected: {props.selectedFiles.size} files</span>
        <button onClick={() => props.onDownload(Array.from(props.selectedFiles))}>
          Download Selected
        </button>
      </div>
    </div>
  );
}
```

## Translations

Add to all locale files:

```yaml
# internal/core/i18n/locales/en.yml
webdav_browser:
  title: "File Browser"
  select_remote: "Select Remote"
  no_remotes: "No WebDAV servers configured"
  add_remote: "Add Server"
  empty_directory: "Empty directory"
  parent_directory: "Parent directory"
  download_selected: "Download Selected"
  download_all: "Download All"
  selected_count: "Selected: %d files (%s)"
  downloading: "Downloading"
  completed: "Completed"
  failed: "Failed"
```

## Implementation Plan

### Phase 1: Backend API
- [ ] `internal/server/webdav_browse.go` - API handlers
- [ ] Register routes in `internal/server/server.go`
- [ ] Download task queue with progress tracking

### Phase 2: Frontend Components
- [ ] `ui/src/pages/WebDAVPage.tsx` - Main page
- [ ] `ui/src/components/WebDAVBrowser.tsx` - File browser
- [ ] `ui/src/components/WebDAVDownloads.tsx` - Download progress
- [ ] Add to sidebar navigation
- [ ] Add routes

### Phase 3: Integration
- [ ] Connect browser to download API
- [ ] Progress polling / WebSocket for updates
- [ ] Error handling (auth failures, network errors)

### Phase 4: Polish
- [ ] i18n translations (all locales)
- [ ] Loading states and skeletons
- [ ] Empty states
- [ ] Mobile responsive design

## Relationship to Seedbox Feature

The WebDAV browser and Seedbox browser share similar UI patterns:

| Feature | WebDAV Browser | Seedbox Browser |
|---------|---------------|-----------------|
| Browse files | WebDAV PROPFIND | HTTP index / SFTP |
| Download | WebDAV GET | HTTP GET / SFTP |
| Backend | `webdav.Client` | `seedbox.HTTPFileServer` / `seedbox.SFTPFileServer` |
| UI Component | Can share `FileBrowser` base component |

Consider extracting a shared `FileBrowser` component that both features can use:

```tsx
// ui/src/components/FileBrowser.tsx (shared)
interface FileBrowserProps {
  files: FileInfo[];
  currentPath: string;
  onNavigate: (path: string) => void;
  onSelect: (paths: string[]) => void;
  onDownload: (paths: string[]) => void;
}
```

## Security Considerations

- WebDAV credentials already stored in config (same security model)
- API endpoints require same auth as other vget endpoints
- No cross-remote access (can only browse configured remotes)
- Path traversal protection (validate paths stay within remote)

## Future Enhancements (Not Planned)

- Upload files to WebDAV
- Rename/delete files
- Create directories
- Search within remote
- Favorites/bookmarks
- Recent files history


================================================
FILE: docs/webdav.md
================================================
# WebDAV Support

vget supports downloading files from WebDAV servers and browsing remote directories.

## Configuration

### Add a WebDAV Server

```bash
vget config webdav add <name>
```

Interactive prompts for:
- WebDAV URL (e.g., `https://dav.example.com`)
- Username (optional)
- Password (masked input)

### Manage Servers

```bash
vget config webdav list              # List all configured servers
vget config webdav show <name>       # Show server details
vget config webdav delete <name>     # Remove a server
```

## Commands

### Download Files

```bash
# Using configured remote
vget pikpak:/path/to/file.mp4

# Using full URL with credentials
vget webdav://user:pass@server.com/path/file.mp4

# With output filename
vget pikpak:/movies/video.mp4 -o my_video.mp4

# Show file metadata
vget --info pikpak:/movies/video.mp4
```

### List Directory

```bash
vget ls pikpak:/movies
vget ls pikpak:/                      # List root directory
vget ls pikpak:/movies --json         # JSON output for scripting
```

Output format:
```
pikpak:/movies
  📁 Action/
  📁 Comedy/
  📄 movie.mp4                    1.5 GB
  📄 readme.txt                   2.3 KB
```

JSON output (`--json`):
```json
[
  {"name": "Action", "path": "pikpak:/movies/Action", "is_dir": true, "size": 0},
  {"name": "movie.mp4", "path": "pikpak:/movies/movie.mp4", "is_dir": false, "size": 1610612736}
]
```

### Bulk Download with Pipe

Use `--json` with `jq` to download all files in a directory:

```bash
# Download all files (skip directories)
vget ls pikpak:/movies --json | jq -r '.[] | select(.is_dir == false) | .path' | xargs -n1 vget

# Download only files > 1GB
vget ls pikpak:/movies --json | jq -r '.[] | select(.is_dir == false and .size > 1073741824) | .path' | xargs -n1 vget
```

### Command Behavior

| Command | File | Directory |
|---------|------|-----------|
| `vget <url>` | Download | Error (use `vget ls`) |
| `vget ls <url>` | Error (not a directory) | List contents |
| `vget --info <url>` | Show metadata | Show metadata |

## URL Schemes

| Scheme | Protocol |
|--------|----------|
| `webdav://` | HTTPS (default) |
| `webdav+http://` | HTTP (insecure) |
| `https://` | HTTPS |

## Shell Completion

Enable tab completion for remote paths:

```bash
# Bash - add to ~/.bashrc
source <(vget completion bash)

# Zsh - add to ~/.zshrc
source <(vget completion zsh)

# Fish
vget completion fish > ~/.config/fish/completions/vget.fish

# PowerShell
vget completion powershell >> $PROFILE
```

After setup, tab completion works for remote paths:
```bash
vget pikpak:/Mo<TAB>           # Completes to pikpak:/Movies/
vget ls pikpak:/Movies/<TAB>   # Shows files in Movies/
```

## Examples

```bash
# Setup
vget config webdav add pikpak
# Enter URL: https://dav.pikpak.com
# Enter username: user@example.com
# Enter password: ****

# Browse
vget ls pikpak:/
vget ls pikpak:/Movies

# Download
vget pikpak:/Movies/film.mp4
vget pikpak:/Movies/film.mp4 -o ~/Downloads/film.mp4

# Info
vget --info pikpak:/Movies/film.mp4
```


================================================
FILE: docs/xhs-mcp-analysis.md
================================================
# Xiaohongshu MCP Analysis

Analysis of [xpzouying/xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp) for implementing a Xiaohongshu extractor in vget.

## Browser Automation Stack

The project uses **Rod** for browser automation via Chrome DevTools Protocol (CDP):

| Dependency | Purpose | Reputation |
|------------|---------|------------|
| `github.com/go-rod/rod` | Core browser automation library | ✅ 6k+ stars, actively maintained |
| `github.com/go-rod/stealth` | Anti-detection measures | ✅ Same maintainer as Rod |
| `github.com/xpzouying/headless_browser` | Thin wrapper (NOT recommended) | ⚠️ Personal lib, avoid |

### About `xpzouying/headless_browser`

This is a thin wrapper (~100 lines) that:
1. Wraps Rod with stealth mode enabled by default
2. Adds cookie loading from JSON
3. Provides simplified `NewPage()` API

**We should NOT use this library.** Instead, use Rod + stealth directly.

## How It Works

### 1. Browser Launch

Rod can launch Chrome in multiple ways:

```go
// Option 1: Auto-download Chromium
launcher.New().MustLaunch()

// Option 2: Use system Chrome (macOS)
launcher.New().
    Bin("/Applications/Google Chrome.app/Contents/MacOS/Google Chrome").
    Headless(false).
    MustLaunch()

// Option 3: Connect to existing Chrome with remote debugging
// First: chrome --remote-debugging-port=9222
rod.New().ControlURL("ws://127.0.0.1:9222").MustConnect()
```

### 2. Data Extraction Strategy

Xiaohongshu embeds post data in `window.__INITIAL_STATE__` (server-side rendered). The MCP extracts it via JavaScript evaluation:

**Source:** `xiaohongshu/feed_detail.go:38-46`

```go
result := page.MustEval(`() => {
    if (window.__INITIAL_STATE__ &&
        window.__INITIAL_STATE__.note &&
        window.__INITIAL_STATE__.note.noteDetailMap) {
        const noteDetailMap = window.__INITIAL_STATE__.note.noteDetailMap;
        return JSON.stringify(noteDetailMap);
    }
    return "";
}`).String()
```

### 3. URL Format

Post detail URL pattern:
```
https://www.xiaohongshu.com/explore/{feedID}?xsec_token={token}&xsec_source=pc_feed
```

**Source:** `xiaohongshu/feed_detail.go:72-74`

## Data Structures

### FeedDetail (Post Content)

**Source:** `xiaohongshu/types.go:94-106`

```go
type FeedDetail struct {
    NoteID       string            `json:"noteId"`
    XsecToken    string            `json:"xsecToken"`
    Title        string            `json:"title"`
    Desc         string            `json:"desc"`
    Type         string            `json:"type"`
Download .txt
gitextract_53wrt5os/

├── .dockerignore
├── .github/
│   └── workflows/
│       ├── ci.yml
│       └── release.yml
├── .gitignore
├── .goreleaser.yaml
├── .vscode/
│   └── settings.json
├── CLAUDE.md
├── LICENSE
├── Makefile
├── README.md
├── README_de.md
├── README_es.md
├── README_fr.md
├── README_jp.md
├── README_kr.md
├── README_zh.md
├── TODO.md
├── cmd/
│   ├── vget/
│   │   └── main.go
│   └── vget-server/
│       └── main.go
├── compose.yml
├── docker/
│   └── vget/
│       ├── Dockerfile
│       ├── Dockerfile.arm64
│       ├── entrypoint-arm64.sh
│       └── entrypoint.sh
├── docs/
│   ├── FAQs.md
│   ├── PRD.md
│   ├── YOUTUBE_NOTES.md
│   ├── bilibili-port-plan.md
│   ├── bugfix/
│   │   └── docker-browser-launch.md
│   ├── homebrew-distribution.md
│   ├── http-server-mode.md
│   ├── multi-binary-architecture.md
│   ├── seedbox.md
│   ├── tauri.md
│   ├── telegram.md
│   ├── torrent-dispatch.md
│   ├── tui-file-browser.md
│   ├── webdav-browsing.md
│   ├── webdav.md
│   ├── xhs-mcp-analysis.md
│   └── zsh-completion-limit.md
├── go.mod
├── go.sum
├── internal/
│   ├── cli/
│   │   ├── batch.go
│   │   ├── browse.go
│   │   ├── completion.go
│   │   ├── config.go
│   │   ├── extract.go
│   │   ├── init.go
│   │   ├── kuaidi100.go
│   │   ├── login/
│   │   │   ├── bilibili.go
│   │   │   └── qrwriter.go
│   │   ├── login.go
│   │   ├── ls.go
│   │   ├── root.go
│   │   ├── search.go
│   │   ├── search_tui.go
│   │   ├── telegram.go
│   │   ├── update.go
│   │   └── version.go
│   ├── core/
│   │   ├── config/
│   │   │   ├── config.go
│   │   │   ├── config_test.go
│   │   │   ├── sites.go
│   │   │   └── wizard.go
│   │   ├── downloader/
│   │   │   ├── downloader.go
│   │   │   ├── ffmpeg.go
│   │   │   ├── hls.go
│   │   │   ├── hls_parser.go
│   │   │   ├── magic.go
│   │   │   ├── multistream.go
│   │   │   └── progress.go
│   │   ├── extractor/
│   │   │   ├── bilibili.go
│   │   │   ├── browser.go
│   │   │   ├── direct.go
│   │   │   ├── instagram.go
│   │   │   ├── itunes.go
│   │   │   ├── m3u8.go
│   │   │   ├── registry.go
│   │   │   ├── telegram/
│   │   │   │   ├── constants.go
│   │   │   │   ├── download.go
│   │   │   │   ├── extractor.go
│   │   │   │   ├── media.go
│   │   │   │   ├── parser.go
│   │   │   │   ├── session.go
│   │   │   │   └── takeout.go
│   │   │   ├── telegram.go
│   │   │   ├── tiktok.go
│   │   │   ├── twitter.go
│   │   │   ├── types.go
│   │   │   ├── types_test.go
│   │   │   ├── xiaohongshu.go
│   │   │   ├── xiaoyuzhou.go
│   │   │   └── youtube.go
│   │   ├── i18n/
│   │   │   ├── i18n.go
│   │   │   └── locales/
│   │   │       ├── de.yml
│   │   │       ├── en.yml
│   │   │       ├── es.yml
│   │   │       ├── fr.yml
│   │   │       ├── jp.yml
│   │   │       ├── kr.yml
│   │   │       └── zh.yml
│   │   ├── site/
│   │   │   └── bilibili/
│   │   │       └── auth.go
│   │   ├── tracker/
│   │   │   └── kuaidi100.go
│   │   ├── version/
│   │   │   └── version.go
│   │   └── webdav/
│   │       └── client.go
│   ├── server/
│   │   ├── auth.go
│   │   ├── bilibili.go
│   │   ├── embed.go
│   │   ├── history.go
│   │   ├── job.go
│   │   ├── podcast.go
│   │   ├── server.go
│   │   └── webdav_browse.go
│   ├── torrent/
│   │   ├── client.go
│   │   ├── qbittorrent.go
│   │   ├── synology.go
│   │   └── transmission.go
│   └── updater/
│       └── updater.go
├── sites.md
├── tauri/
│   ├── .gitignore
│   ├── Makefile
│   ├── components.json
│   ├── index.html
│   ├── package.json
│   ├── src/
│   │   ├── components/
│   │   │   ├── AppSidebar.tsx
│   │   │   ├── home/
│   │   │   │   ├── DownloadItem.tsx
│   │   │   │   ├── HomePage.tsx
│   │   │   │   └── types.ts
│   │   │   ├── icons/
│   │   │   │   └── PdfIcon.tsx
│   │   │   ├── media-tools/
│   │   │   │   ├── MediaToolsPage.tsx
│   │   │   │   ├── panels/
│   │   │   │   │   ├── AudioConvertPanel.tsx
│   │   │   │   │   ├── CompressPanel.tsx
│   │   │   │   │   ├── ConvertPanel.tsx
│   │   │   │   │   ├── ExtractAudioPanel.tsx
│   │   │   │   │   ├── ExtractFramesPanel.tsx
│   │   │   │   │   ├── TrimPanel.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── pdf-tools/
│   │   │   │   ├── PDFToolsPage.tsx
│   │   │   │   ├── panels/
│   │   │   │   │   ├── DeletePagesPanel.tsx
│   │   │   │   │   ├── ImagesToPdfPanel.tsx
│   │   │   │   │   ├── Md2PdfPanel.tsx
│   │   │   │   │   ├── MergePdfPanel.tsx
│   │   │   │   │   ├── RemoveWatermarkPanel.tsx
│   │   │   │   │   └── index.ts
│   │   │   │   └── types.ts
│   │   │   ├── settings/
│   │   │   │   ├── AboutSettings.tsx
│   │   │   │   ├── GeneralSettings.tsx
│   │   │   │   ├── SettingsPage.tsx
│   │   │   │   ├── SiteSettings.tsx
│   │   │   │   ├── index.ts
│   │   │   │   └── types.ts
│   │   │   └── ui/
│   │   │       ├── accordion.tsx
│   │   │       ├── alert-dialog.tsx
│   │   │       ├── alert.tsx
│   │   │       ├── aspect-ratio.tsx
│   │   │       ├── avatar.tsx
│   │   │       ├── badge.tsx
│   │   │       ├── breadcrumb.tsx
│   │   │       ├── button-group.tsx
│   │   │       ├── button.tsx
│   │   │       ├── calendar.tsx
│   │   │       ├── card.tsx
│   │   │       ├── carousel.tsx
│   │   │       ├── chart.tsx
│   │   │       ├── checkbox.tsx
│   │   │       ├── collapsible.tsx
│   │   │       ├── command.tsx
│   │   │       ├── context-menu.tsx
│   │   │       ├── dialog.tsx
│   │   │       ├── drawer.tsx
│   │   │       ├── dropdown-menu.tsx
│   │   │       ├── empty.tsx
│   │   │       ├── field.tsx
│   │   │       ├── file-drop-input.tsx
│   │   │       ├── form.tsx
│   │   │       ├── hover-card.tsx
│   │   │       ├── input-group.tsx
│   │   │       ├── input-otp.tsx
│   │   │       ├── input.tsx
│   │   │       ├── item.tsx
│   │   │       ├── kbd.tsx
│   │   │       ├── label.tsx
│   │   │       ├── menubar.tsx
│   │   │       ├── navigation-menu.tsx
│   │   │       ├── pagination.tsx
│   │   │       ├── popover.tsx
│   │   │       ├── progress.tsx
│   │   │       ├── radio-group.tsx
│   │   │       ├── resizable.tsx
│   │   │       ├── scroll-area.tsx
│   │   │       ├── select.tsx
│   │   │       ├── separator.tsx
│   │   │       ├── sheet.tsx
│   │   │       ├── sidebar.tsx
│   │   │       ├── skeleton.tsx
│   │   │       ├── slider.tsx
│   │   │       ├── sonner.tsx
│   │   │       ├── spinner.tsx
│   │   │       ├── switch.tsx
│   │   │       ├── table.tsx
│   │   │       ├── tabs.tsx
│   │   │       ├── textarea.tsx
│   │   │       ├── toggle-group.tsx
│   │   │       ├── toggle.tsx
│   │   │       └── tooltip.tsx
│   │   ├── hooks/
│   │   │   ├── use-mobile.ts
│   │   │   └── useDropZone.ts
│   │   ├── i18n/
│   │   │   ├── index.ts
│   │   │   └── locales/
│   │   │       ├── de.yml
│   │   │       ├── en.yml
│   │   │       ├── es.yml
│   │   │       ├── fr.yml
│   │   │       ├── jp.yml
│   │   │       ├── kr.yml
│   │   │       └── zh.yml
│   │   ├── index.css
│   │   ├── lib/
│   │   │   └── utils.ts
│   │   ├── main.tsx
│   │   ├── routeTree.gen.ts
│   │   ├── routes/
│   │   │   ├── __root.tsx
│   │   │   ├── index.tsx
│   │   │   ├── media-tools.tsx
│   │   │   ├── pdf-tools.tsx
│   │   │   └── settings.tsx
│   │   ├── services/
│   │   │   └── dockerApi.ts
│   │   ├── stores/
│   │   │   ├── auth.ts
│   │   │   └── downloads.ts
│   │   └── vite-env.d.ts
│   ├── src-tauri/
│   │   ├── Cargo.toml
│   │   ├── binaries/
│   │   │   └── .gitkeep
│   │   ├── build.rs
│   │   ├── capabilities/
│   │   │   └── default.json
│   │   ├── gen/
│   │   │   └── schemas/
│   │   │       ├── acl-manifests.json
│   │   │       ├── capabilities.json
│   │   │       ├── desktop-schema.json
│   │   │       └── macOS-schema.json
│   │   ├── icons/
│   │   │   ├── android/
│   │   │   │   ├── mipmap-anydpi-v26/
│   │   │   │   │   └── ic_launcher.xml
│   │   │   │   └── values/
│   │   │   │       └── ic_launcher_background.xml
│   │   │   └── icon.icns
│   │   ├── rust-toolchain.toml
│   │   ├── src/
│   │   │   ├── auth.rs
│   │   │   ├── config.rs
│   │   │   ├── downloader/
│   │   │   │   ├── mod.rs
│   │   │   │   └── simple.rs
│   │   │   ├── extractor/
│   │   │   │   ├── bilibili.rs
│   │   │   │   ├── direct.rs
│   │   │   │   ├── mod.rs
│   │   │   │   ├── twitter.rs
│   │   │   │   └── types.rs
│   │   │   ├── ffmpeg.rs
│   │   │   ├── lib.rs
│   │   │   ├── main.rs
│   │   │   ├── md2pdf.rs
│   │   │   └── pdf.rs
│   │   └── tauri.conf.json
│   ├── tsconfig.json
│   ├── tsconfig.node.json
│   └── vite.config.ts
└── ui/
    ├── .gitignore
    ├── README.md
    ├── eslint.config.js
    ├── index.html
    ├── package.json
    ├── src/
    │   ├── components/
    │   │   ├── ConfigEditor.tsx
    │   │   ├── ConfigRow.tsx
    │   │   ├── DownloadJobCard.tsx
    │   │   ├── Kuaidi100.tsx
    │   │   ├── Layout.tsx
    │   │   ├── Sidebar.tsx
    │   │   ├── Toast.tsx
    │   │   ├── Torrent.tsx
    │   │   └── TorrentSettings.tsx
    │   ├── context/
    │   │   └── AppContext.tsx
    │   ├── index.css
    │   ├── main.tsx
    │   ├── pages/
    │   │   ├── BilibiliPage.tsx
    │   │   ├── BulkDownloadPage.tsx
    │   │   ├── ConfigPage.tsx
    │   │   ├── DownloadPage.tsx
    │   │   ├── HistoryPage.tsx
    │   │   ├── Kuaidi100Page.tsx
    │   │   ├── PodcastPage.tsx
    │   │   ├── TokenPage.tsx
    │   │   ├── TorrentPage.tsx
    │   │   └── WebDAVPage.tsx
    │   ├── routeTree.gen.ts
    │   ├── routes/
    │   │   ├── __root.tsx
    │   │   ├── bilibili.tsx
    │   │   ├── bulk.tsx
    │   │   ├── config.tsx
    │   │   ├── history.tsx
    │   │   ├── index.tsx
    │   │   ├── kuaidi100.tsx
    │   │   ├── podcast.tsx
    │   │   ├── token.tsx
    │   │   ├── torrent.tsx
    │   │   └── webdav.tsx
    │   └── utils/
    │       ├── apis.ts
    │       └── translations.ts
    ├── tsconfig.app.json
    ├── tsconfig.json
    ├── tsconfig.node.json
    └── vite.config.ts
Download .txt
SYMBOL INDEX (1622 symbols across 196 files)

FILE: cmd/vget-server/main.go
  function main (line 19) | func main() {

FILE: cmd/vget/main.go
  function main (line 9) | func main() {

FILE: internal/cli/batch.go
  function runBatch (line 13) | func runBatch(filename string) error {
  function truncateURL (line 131) | func truncateURL(url string, maxLen int) string {

FILE: internal/cli/browse.go
  type browseModel (line 27) | type browseModel struct
    method Init (line 92) | func (m browseModel) Init() tea.Cmd {
    method loadDirectory (line 96) | func (m browseModel) loadDirectory() tea.Cmd {
    method visibleLines (line 118) | func (m browseModel) visibleLines() int {
    method adjustScroll (line 133) | func (m *browseModel) adjustScroll() {
    method Update (line 142) | func (m browseModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method goUp (line 223) | func (m browseModel) goUp() (tea.Model, tea.Cmd) {
    method View (line 237) | func (m browseModel) View() string {
  type browseKeyMap (line 43) | type browseKeyMap struct
  function defaultBrowseKeyMap (line 51) | func defaultBrowseKeyMap() browseKeyMap {
  type loadedMsg (line 77) | type loadedMsg struct
  function newBrowseModel (line 82) | func newBrowseModel(client *webdav.Client, serverName, initialPath strin...
  constant browseMaxVisibleLines (line 116) | browseMaxVisibleLines = 20
  type BrowseResult (line 314) | type BrowseResult struct
  function RunBrowseTUI (line 320) | func RunBrowseTUI(client *webdav.Client, serverName, initialPath string)...

FILE: internal/cli/completion.go
  function init (line 57) | func init() {
  function unescapeShellPath (line 65) | func unescapeShellPath(s string) string {
  function completeRemotePath (line 80) | func completeRemotePath(cmd *cobra.Command, args []string, toComplete st...
  function completeRemotes (line 96) | func completeRemotes(prefix string) ([]string, cobra.ShellCompDirective) {
  function completeRemoteFiles (line 116) | func completeRemoteFiles(toComplete string) ([]string, cobra.ShellCompDi...

FILE: internal/cli/config.go
  function setConfigValue (line 195) | func setConfigValue(cfg *config.Config, key, value string) error {
  function getConfigValue (line 242) | func getConfigValue(cfg *config.Config, key string) (string, error) {
  function unsetConfigValue (line 283) | func unsetConfigValue(cfg *config.Config, key string) error {
  function init (line 516) | func init() {

FILE: internal/cli/extract.go
  type extractState (line 25) | type extractState struct
    method setDone (line 32) | func (s *extractState) setDone(result extractor.Media) {
    method setError (line 39) | func (s *extractState) setError(err error) {
    method get (line 46) | func (s *extractState) get() (bool, extractor.Media, error) {
  type extractTickMsg (line 52) | type extractTickMsg
  type extractModel (line 54) | type extractModel struct
    method Init (line 80) | func (m extractModel) Init() tea.Cmd {
    method Update (line 84) | func (m extractModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 108) | func (m extractModel) View() string {
  function newExtractModel (line 61) | func newExtractModel(url, lang string, state *extractState) extractModel {
  function extractTickCmd (line 74) | func extractTickCmd() tea.Cmd {
  function runExtractWithSpinner (line 171) | func runExtractWithSpinner(ext extractor.Extractor, url, lang string) (e...

FILE: internal/cli/init.go
  function init (line 30) | func init() {

FILE: internal/cli/kuaidi100.go
  function init (line 53) | func init() {
  function runKuaidi100 (line 58) | func runKuaidi100(cmd *cobra.Command, args []string) error {
  function printKuaidi100Result (line 104) | func printKuaidi100Result(result *tracker.TrackingResponse) {

FILE: internal/cli/login.go
  function init (line 20) | func init() {

FILE: internal/cli/login/bilibili.go
  function BilibiliCmd (line 42) | func BilibiliCmd() *cobra.Command {
  function BilibiliLogoutCmd (line 60) | func BilibiliLogoutCmd() *cobra.Command {
  function bilibiliQRCmd (line 76) | func bilibiliQRCmd() *cobra.Command {
  function bilibiliCookieCmd (line 87) | func bilibiliCookieCmd() *cobra.Command {
  function bilibiliStatusCmd (line 105) | func bilibiliStatusCmd() *cobra.Command {
  type loginMethod (line 122) | type loginMethod
  constant methodQR (line 125) | methodQR loginMethod = iota
  constant methodCookie (line 126) | methodCookie
  type selectorModel (line 129) | type selectorModel struct
    method Init (line 146) | func (m selectorModel) Init() tea.Cmd {
    method Update (line 150) | func (m selectorModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 173) | func (m selectorModel) View() string {
  function newSelectorModel (line 136) | func newSelectorModel() selectorModel {
  function runLoginSelector (line 202) | func runLoginSelector() error {
  type cookieLoginModel (line 229) | type cookieLoginModel struct
    method Init (line 286) | func (m cookieLoginModel) Init() tea.Cmd {
    method Update (line 290) | func (m cookieLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 356) | func (m cookieLoginModel) View() string {
  function newCookieLoginModel (line 237) | func newCookieLoginModel() cookieLoginModel {
  function runCookieLogin (line 411) | func runCookieLogin() error {
  type qrLoginState (line 435) | type qrLoginState
  constant qrStateGenerating (line 438) | qrStateGenerating qrLoginState = iota
  constant qrStateWaiting (line 439) | qrStateWaiting
  constant qrStateScanned (line 440) | qrStateScanned
  constant qrStateSuccess (line 441) | qrStateSuccess
  constant qrStateExpired (line 442) | qrStateExpired
  constant qrStateError (line 443) | qrStateError
  type qrLoginModel (line 446) | type qrLoginModel struct
    method Init (line 479) | func (m qrLoginModel) Init() tea.Cmd {
    method generateQR (line 486) | func (m qrLoginModel) generateQR() tea.Msg {
    method pollStatus (line 491) | func (m qrLoginModel) pollStatus() tea.Cmd {
    method Update (line 499) | func (m qrLoginModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 570) | func (m qrLoginModel) View() string {
  type qrPollMsg (line 456) | type qrPollMsg struct
  type qrGeneratedMsg (line 462) | type qrGeneratedMsg struct
  function newQRLoginModel (line 467) | func newQRLoginModel() qrLoginModel {
  function printQRCode (line 627) | func printQRCode(url string) {
  function runQRLogin (line 645) | func runQRLogin() error {

FILE: internal/cli/login/qrwriter.go
  type compactQRWriter (line 11) | type compactQRWriter struct
    method Write (line 21) | func (w *compactQRWriter) Write(mat qrcode.Matrix) error {
    method Close (line 36) | func (w *compactQRWriter) Close() error {
    method getPixel (line 41) | func (w *compactQRWriter) getPixel(x, y int) bool {
    method render (line 48) | func (w *compactQRWriter) render() error {
  function vGetCompactQRWriter (line 17) | func vGetCompactQRWriter() *compactQRWriter {

FILE: internal/cli/ls.go
  function init (line 28) | func init() {
  type FileEntry (line 34) | type FileEntry struct
  function runLs (line 41) | func runLs(cmd *cobra.Command, args []string) error {

FILE: internal/cli/root.go
  function init (line 54) | func init() {
  function Execute (line 62) | func Execute() error {
  function runDownload (line 66) | func runDownload(url string) error {
  function runWebDAVDownload (line 175) | func runWebDAVDownload(rawURL, lang string) error {
  function formatSize (line 267) | func formatSize(b int64) string {
  function downloadMultiVideo (line 280) | func downloadMultiVideo(m *extractor.MultiVideoMedia, dl *downloader.Dow...
  function downloadVideo (line 309) | func downloadVideo(m *extractor.VideoMedia, dl *downloader.Downloader, t...
  function downloadVideoWithIndex (line 384) | func downloadVideoWithIndex(m *extractor.VideoMedia, dl *downloader.Down...
  function downloadVideoAndAudio (line 460) | func downloadVideoAndAudio(format *extractor.VideoFormat, outputFile, vi...
  function downloadAudio (line 523) | func downloadAudio(m *extractor.AudioMedia, dl *downloader.Downloader, o...
  function downloadImages (line 548) | func downloadImages(m *extractor.ImageMedia, dl *downloader.Downloader, ...
  function selectVideoFormat (line 593) | func selectVideoFormat(formats []extractor.VideoFormat, preferred string...
  function isTelegramURL (line 652) | func isTelegramURL(urlStr string) bool {
  function confirmBilibiliNoLogin (line 657) | func confirmBilibiliNoLogin() bool {
  function runTelegramDownload (line 673) | func runTelegramDownload(urlStr, outputPath string) error {
  function runTelegramBatchDownload (line 695) | func runTelegramBatchDownload(urls []string) (succeeded, failed int, fai...

FILE: internal/cli/search.go
  function init (line 57) | func init() {
  function containsChinese (line 63) | func containsChinese(s string) bool {
  type XiaoyuzhouSearchResponse (line 73) | type XiaoyuzhouSearchResponse struct
  type XiaoyuzhouPodcast (line 80) | type XiaoyuzhouPodcast struct
  type XiaoyuzhouEpisode (line 90) | type XiaoyuzhouEpisode struct
  function searchXiaoyuzhou (line 106) | func searchXiaoyuzhou(query string) error {
  function formatEpisodeDuration (line 223) | func formatEpisodeDuration(seconds int) string {
  type iTunesSearchResponse (line 237) | type iTunesSearchResponse struct
  type iTunesResult (line 242) | type iTunesResult struct
  function searchITunes (line 260) | func searchITunes(query string) error {
  function handleSelectedItems (line 395) | func handleSelectedItems(items []SearchItem, source, lang string) error {
  function fetchAndShowEpisodes (line 419) | func fetchAndShowEpisodes(podcast SearchItem, source, lang string) error {
  function fetchITunesEpisodes (line 479) | func fetchITunesEpisodes(podcastID string) ([]SearchItem, error) {
  function fetchXiaoyuzhouEpisodes (line 530) | func fetchXiaoyuzhouEpisodes(podcastID string) ([]SearchItem, error) {
  function downloadSelectedEpisodes (line 611) | func downloadSelectedEpisodes(items []SearchItem) error {
  function runDirectDownload (line 639) | func runDirectDownload(downloadURL, title string) error {
  function sanitizeFilenameForDownload (line 663) | func sanitizeFilenameForDownload(name string) string {
  type searchSpinnerModel (line 686) | type searchSpinnerModel struct
    method Init (line 710) | func (m searchSpinnerModel) Init() tea.Cmd {
    method checkDone (line 714) | func (m searchSpinnerModel) checkDone() tea.Cmd {
    method Update (line 720) | func (m searchSpinnerModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 745) | func (m searchSpinnerModel) View() string {
  type searchTickMsg (line 695) | type searchTickMsg
  function newSearchSpinnerModel (line 697) | func newSearchSpinnerModel(message, query, lang string, done chan bool) ...
  function runSearchSpinner (line 749) | func runSearchSpinner(query, lang string, done chan bool) error {
  function runFetchEpisodesSpinner (line 763) | func runFetchEpisodesSpinner(podcastTitle, lang string, done chan bool) ...

FILE: internal/cli/search_tui.go
  type ItemType (line 29) | type ItemType
  constant ItemTypePodcast (line 32) | ItemTypePodcast ItemType = iota
  constant ItemTypeEpisode (line 33) | ItemTypeEpisode
  type SearchItem (line 37) | type SearchItem struct
  type SearchSection (line 50) | type SearchSection struct
  constant maxSelections (line 55) | maxSelections = 5
  type searchModel (line 57) | type searchModel struct
    method Init (line 149) | func (m searchModel) Init() tea.Cmd {
    method visibleLines (line 156) | func (m searchModel) visibleLines() int {
    method currentSection (line 172) | func (m searchModel) currentSection() *SearchSection {
    method currentItemType (line 180) | func (m searchModel) currentItemType() ItemType {
    method clearSelections (line 189) | func (m *searchModel) clearSelections() {
    method adjustScroll (line 195) | func (m *searchModel) adjustScroll() {
    method Update (line 207) | func (m searchModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 287) | func (m searchModel) View() string {
    method GetSelectedItems (line 432) | func (m searchModel) GetSelectedItems() []SearchItem {
  type searchKeyMap (line 74) | type searchKeyMap struct
  function defaultSearchKeyMap (line 87) | func defaultSearchKeyMap() searchKeyMap {
  function newSearchModel (line 132) | func newSearchModel(sections []SearchSection, query, lang string) search...
  constant maxVisibleLines (line 153) | maxVisibleLines = 15
  type SearchTUIResult (line 448) | type SearchTUIResult struct
  function RunSearchTUI (line 454) | func RunSearchTUI(sections []SearchSection, query, lang string) ([]Searc...
  function RunSearchTUIWithBack (line 459) | func RunSearchTUIWithBack(sections []SearchSection, query, lang string, ...

FILE: internal/cli/telegram.go
  function runTelegramLogin (line 53) | func runTelegramLogin(cmd *cobra.Command, args []string) error {
  function runTelegramLogout (line 210) | func runTelegramLogout(cmd *cobra.Command, args []string) error {
  function runTelegramStatus (line 226) | func runTelegramStatus(cmd *cobra.Command, args []string) error {
  function formatUserInfo (line 277) | func formatUserInfo(self *tg.User) string {
  function getTelegramDesktopPath (line 292) | func getTelegramDesktopPath() string {
  function getAccountInfo (line 327) | func getAccountInfo(acc tdesktop.Account) (name, username string) {
  function init (line 366) | func init() {

FILE: internal/cli/update.go
  function init (line 16) | func init() {

FILE: internal/cli/version.go
  function init (line 19) | func init() {

FILE: internal/core/config/config.go
  constant ConfigFileName (line 14) | ConfigFileName = "config.yml"
  constant AppDirName (line 15) | AppDirName     = "vget"
  function ConfigDir (line 21) | func ConfigDir() (string, error) {
  function ConfigPath (line 38) | func ConfigPath() (string, error) {
  type Config (line 46) | type Config struct
    method GetExpressConfig (line 126) | func (c *Config) GetExpressConfig(provider string) map[string]string {
    method SetExpressConfig (line 134) | func (c *Config) SetExpressConfig(provider, key, value string) {
    method DeleteExpressConfig (line 145) | func (c *Config) DeleteExpressConfig(provider, key string) {
    method GetWebDAVServer (line 187) | func (c *Config) GetWebDAVServer(name string) *WebDAVServer {
    method SetWebDAVServer (line 198) | func (c *Config) SetWebDAVServer(name string, server WebDAVServer) {
    method DeleteWebDAVServer (line 206) | func (c *Config) DeleteWebDAVServer(name string) {
  type BilibiliConfig (line 90) | type BilibiliConfig struct
  type TelegramConfig (line 96) | type TelegramConfig struct
  type TorrentConfig (line 102) | type TorrentConfig struct
  type TwitterConfig (line 157) | type TwitterConfig struct
  type ServerConfig (line 163) | type ServerConfig struct
  type WebDAVServer (line 175) | type WebDAVServer struct
  function DefaultDownloadDir (line 216) | func DefaultDownloadDir() string {
  function IsRunningInDocker (line 237) | func IsRunningInDocker() bool {
  function DefaultConfig (line 257) | func DefaultConfig() *Config {
  function Exists (line 267) | func Exists() bool {
  function Load (line 277) | func Load() (*Config, error) {
  function expandPath (line 302) | func expandPath(path string) string {
  function Save (line 327) | func Save(cfg *Config) error {
  function SavePath (line 352) | func SavePath() string {
  function Init (line 360) | func Init() error {
  function LoadOrDefault (line 370) | func LoadOrDefault() *Config {

FILE: internal/core/config/config_test.go
  function TestExpandPath (line 9) | func TestExpandPath(t *testing.T) {

FILE: internal/core/config/sites.go
  constant SitesFileName (line 11) | SitesFileName = "sites.yml"
  type Site (line 14) | type Site struct
  type SitesConfig (line 23) | type SitesConfig struct
    method MatchSite (line 59) | func (c *SitesConfig) MatchSite(url string) *Site {
    method AddSite (line 72) | func (c *SitesConfig) AddSite(match, mediaType string) {
    method RemoveSite (line 80) | func (c *SitesConfig) RemoveSite(match string) bool {
  function LoadSites (line 28) | func LoadSites() (*SitesConfig, error) {
  function SaveSites (line 46) | func SaveSites(cfg *SitesConfig) error {
  function SitesExist (line 91) | func SitesExist() bool {

FILE: internal/core/config/wizard.go
  constant asciiArt (line 12) | asciiArt = `
  type model (line 36) | type model struct
    method t (line 61) | func (m *model) t() *i18n.Translations {
    method getStepTitle (line 65) | func (m *model) getStepTitle() string {
    method getStepDescription (line 82) | func (m *model) getStepDescription() string {
    method getOptions (line 99) | func (m *model) getOptions() []struct{ label, value string } {
    method isInputStep (line 132) | func (m *model) isInputStep() bool {
    method setCursorFromConfig (line 136) | func (m *model) setCursorFromConfig() {
    method Init (line 169) | func (m model) Init() tea.Cmd {
    method Update (line 173) | func (m model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method saveCurrentValue (line 252) | func (m *model) saveCurrentValue() {
    method View (line 275) | func (m model) View() string {
    method renderReview (line 340) | func (m model) renderReview() string {
  function initialModel (line 48) | func initialModel(cfg *Config) model {
  function RunInitWizard (line 369) | func RunInitWizard() (*Config, error) {
  function getLanguageName (line 394) | func getLanguageName(code string) string {

FILE: internal/core/downloader/downloader.go
  constant DefaultUserAgent (line 10) | DefaultUserAgent = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) Appl...
  type Downloader (line 13) | type Downloader struct
    method Download (line 25) | func (d *Downloader) Download(url, output, videoID string) error {
    method DownloadWithHeaders (line 30) | func (d *Downloader) DownloadWithHeaders(url, output, videoID string, ...
    method DownloadFromReader (line 36) | func (d *Downloader) DownloadFromReader(reader io.ReadCloser, size int...
  function New (line 18) | func New(lang string) *Downloader {
  function formatBytes (line 40) | func formatBytes(b int64) string {
  function formatDuration (line 53) | func formatDuration(d time.Duration) string {

FILE: internal/core/downloader/ffmpeg.go
  function FFmpegAvailable (line 12) | func FFmpegAvailable() bool {
  function MergeVideoAudio (line 21) | func MergeVideoAudio(videoPath, audioPath, outputPath string, deleteOrig...

FILE: internal/core/downloader/hls.go
  type HLSConfig (line 26) | type HLSConfig struct
  function DefaultHLSConfig (line 32) | func DefaultHLSConfig() HLSConfig {
  type hlsState (line 40) | type hlsState struct
    method getProgress (line 46) | func (s *hlsState) getProgress() (downloaded, total int64) {
    method getBytes (line 50) | func (s *hlsState) getBytes() int64 {
    method addBytes (line 54) | func (s *hlsState) addBytes(n int64) {
    method incDownloaded (line 58) | func (s *hlsState) incDownloaded() {
  function RunHLSDownloadTUI (line 63) | func RunHLSDownloadTUI(m3u8URL, output, displayID, lang string) error {
  function RunHLSDownloadWithHeadersTUI (line 68) | func RunHLSDownloadWithHeadersTUI(m3u8URL, output, displayID, lang strin...
  function downloadHLSWithHeaders (line 112) | func downloadHLSWithHeaders(ctx context.Context, m3u8URL, output string,...
  function downloadSegmentsOrdered (line 194) | func downloadSegmentsOrdered(ctx context.Context, segments []Segment, fi...
  function downloadSegment (line 295) | func downloadSegment(client *http.Client, url string, decryptKey, decryp...
  function fetchKeyWithHeaders (line 334) | func fetchKeyWithHeaders(url string, headers map[string]string) ([]byte,...
  function decryptAES128 (line 367) | func decryptAES128(data, key, iv []byte, segmentIndex int) ([]byte, erro...
  function DownloadHLSWithProgress (line 403) | func DownloadHLSWithProgress(ctx context.Context, m3u8URL, output string...
  function convertTsToMp4 (line 509) | func convertTsToMp4(tsPath string) (string, error) {

FILE: internal/core/downloader/hls_parser.go
  type M3U8Playlist (line 16) | type M3U8Playlist struct
    method SelectBestVariant (line 226) | func (p *M3U8Playlist) SelectBestVariant() *Variant {
    method SelectVariantByResolution (line 241) | func (p *M3U8Playlist) SelectVariantByResolution(resolution string) *V...
  type Variant (line 27) | type Variant struct
  type Segment (line 36) | type Segment struct
  function ParseM3U8 (line 55) | func ParseM3U8(m3u8URL string) (*M3U8Playlist, error) {
  function ParseM3U8WithHeaders (line 60) | func ParseM3U8WithHeaders(m3u8URL string, headers map[string]string) (*M...
  function parseM3U8Content (line 95) | func parseM3U8Content(reader io.Reader, baseURL string) (*M3U8Playlist, ...
  function parseVariant (line 188) | func parseVariant(line string) Variant {
  function resolveURL (line 198) | func resolveURL(base *url.URL, ref string) string {
  function extractRegex (line 207) | func extractRegex(re *regexp.Regexp, s string) string {
  function extractInt (line 216) | func extractInt(re *regexp.Regexp, s string) int {

FILE: internal/core/downloader/magic.go
  function DetectFileType (line 13) | func DetectFileType(path string) (string, error) {
  function RenameByMagicBytes (line 58) | func RenameByMagicBytes(path string) string {

FILE: internal/core/downloader/multistream.go
  type MultiStreamConfig (line 17) | type MultiStreamConfig struct
  function DefaultMultiStreamConfig (line 25) | func DefaultMultiStreamConfig() MultiStreamConfig {
  type multiStreamState (line 35) | type multiStreamState struct
    method addBytes (line 43) | func (s *multiStreamState) addBytes(n int64) {
    method getDownloaded (line 47) | func (s *multiStreamState) getDownloaded() int64 {
    method addError (line 51) | func (s *multiStreamState) addError(err error) {
    method getErrors (line 57) | func (s *multiStreamState) getErrors() []error {
  type chunk (line 64) | type chunk struct
  function probeRangeSupport (line 73) | func probeRangeSupport(ctx context.Context, client *http.Client, url, au...
  function probeWithHEAD (line 122) | func probeWithHEAD(ctx context.Context, client *http.Client, url, authHe...
  function MultiStreamDownload (line 143) | func MultiStreamDownload(ctx context.Context, url, output string, config...
  function calculateChunks (line 259) | func calculateChunks(totalSize int64, chunkSize int64) []chunk {
  function downloadChunk (line 291) | func downloadChunk(ctx context.Context, client *http.Client, url string,...
  function downloadChunkOnce (line 346) | func downloadChunkOnce(ctx context.Context, client *http.Client, url str...
  function RunMultiStreamDownloadTUI (line 398) | func RunMultiStreamDownloadTUI(url, output, displayID, lang string, conf...
  function MultiStreamDownloadWithAuth (line 435) | func MultiStreamDownloadWithAuth(ctx context.Context, url, authHeader, o...
  function downloadChunkWithAuth (line 546) | func downloadChunkWithAuth(ctx context.Context, client *http.Client, url...
  function downloadChunkWithAuthOnce (line 599) | func downloadChunkWithAuthOnce(ctx context.Context, client *http.Client,...
  function downloadWithAuthSingleStream (line 655) | func downloadWithAuthSingleStream(ctx context.Context, client *http.Clie...
  function RunMultiStreamDownloadWithAuthTUI (line 713) | func RunMultiStreamDownloadWithAuthTUI(url, authHeader, output, displayI...

FILE: internal/core/downloader/progress.go
  type downloadState (line 28) | type downloadState struct
    method update (line 41) | func (s *downloadState) update(current, total int64) {
    method setDone (line 52) | func (s *downloadState) setDone() {
    method setError (line 63) | func (s *downloadState) setError(err error) {
    method setFinalPath (line 70) | func (s *downloadState) setFinalPath(path string) {
    method getFinalPath (line 76) | func (s *downloadState) getFinalPath() string {
    method get (line 82) | func (s *downloadState) get() (int64, int64, float64, bool, error) {
    method getFinal (line 88) | func (s *downloadState) getFinal() (elapsed time.Duration, avgSpeed fl...
  type tickMsg (line 98) | type tickMsg
  type downloadDoneMsg (line 101) | type downloadDoneMsg struct
  type downloadModel (line 104) | type downloadModel struct
    method Init (line 143) | func (m downloadModel) Init() tea.Cmd {
    method Update (line 150) | func (m downloadModel) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
    method View (line 191) | func (m downloadModel) View() string {
  function newDownloadModel (line 115) | func newDownloadModel(output, videoID, lang string, state *downloadState...
  function tickCmd (line 137) | func tickCmd() tea.Cmd {
  function calculateETA (line 267) | func calculateETA(remaining int64, speed float64) string {
  function RunDownloadTUI (line 276) | func RunDownloadTUI(url, output, videoID, lang string, headers map[strin...
  function downloadWithProgress (line 315) | func downloadWithProgress(client *http.Client, url, output string, state...
  function RunDownloadFromReaderTUI (line 383) | func RunDownloadFromReaderTUI(reader io.ReadCloser, size int64, output, ...
  function downloadFromReaderWithProgress (line 415) | func downloadFromReaderWithProgress(reader io.ReadCloser, total int64, o...
  type TelegramDownloadResult (line 457) | type TelegramDownloadResult struct
  type TelegramDownloadFunc (line 464) | type TelegramDownloadFunc
  function RunTelegramDownloadTUI (line 467) | func RunTelegramDownloadTUI(urlStr, outputPath, lang string, downloadFn ...
  function RunMultiStreamDownloadWithAuthCallback (line 515) | func RunMultiStreamDownloadWithAuthCallback(ctx context.Context, url, au...

FILE: internal/core/extractor/bilibili.go
  constant xorCode (line 22) | xorCode  int64 = 23442827791579
  constant maskCode (line 23) | maskCode int64 = (1 << 51) - 1
  constant maxAID (line 24) | maxAID   int64 = maskCode + 1
  constant minAID (line 25) | minAID   int64 = 1
  constant base (line 26) | base     int64 = 58
  constant bvLen (line 27) | bvLen    int   = 9
  function init (line 35) | func init() {
  function BVToAV (line 42) | func BVToAV(bvid string) (int64, error) {
  function AVToBV (line 72) | func AVToBV(avid int64) (string, error) {
  type BilibiliExtractor (line 120) | type BilibiliExtractor struct
    method Name (line 127) | func (b *BilibiliExtractor) Name() string {
    method Match (line 132) | func (b *BilibiliExtractor) Match(u *url.URL) bool {
    method Extract (line 140) | func (b *BilibiliExtractor) Extract(urlStr string) (Media, error) {
    method resolveVideoID (line 204) | func (b *BilibiliExtractor) resolveVideoID(urlStr string) (aid int64, ...
    method resolveShortURL (line 240) | func (b *BilibiliExtractor) resolveShortURL(shortURL string) (string, ...
    method fetchWBIKeys (line 264) | func (b *BilibiliExtractor) fetchWBIKeys() error {
    method wbiSign (line 339) | func (b *BilibiliExtractor) wbiSign(params url.Values) string {
    method fetchVideoInfo (line 406) | func (b *BilibiliExtractor) fetchVideoInfo(aid int64) (*BilibiliVideoI...
    method fetchPlayURL (line 465) | func (b *BilibiliExtractor) fetchPlayURL(aid, cid int64) (*BilibiliStr...
    method buildFormats (line 520) | func (b *BilibiliExtractor) buildFormats(streams *BilibiliStreamInfo) ...
    method setHeaders (line 585) | func (b *BilibiliExtractor) setHeaders(req *http.Request) {
    method userAgent (line 596) | func (b *BilibiliExtractor) userAgent() string {
  function extractKeyFromURL (line 308) | func extractKeyFromURL(urlStr string) string {
  function getMixinKey (line 323) | func getMixinKey(orig string) string {
  function filterWBIValue (line 376) | func filterWBIValue(s string) string {
  type BilibiliVideoInfo (line 388) | type BilibiliVideoInfo struct
  type BilibiliStreamInfo (line 444) | type BilibiliStreamInfo struct
  function getCodecName (line 571) | func getCodecName(codecID int) string {
  function init (line 600) | func init() {

FILE: internal/core/extractor/browser.go
  type BrowserExtractor (line 21) | type BrowserExtractor struct
    method Name (line 39) | func (e *BrowserExtractor) Name() string {
    method Match (line 43) | func (e *BrowserExtractor) Match(u *url.URL) bool {
    method Extract (line 50) | func (e *BrowserExtractor) Extract(rawURL string) (Media, error) {
    method captureFromNetwork (line 143) | func (e *BrowserExtractor) captureFromNetwork(page *rod.Page, rawURL, ...
    method findInPerformanceAPI (line 221) | func (e *BrowserExtractor) findInPerformanceAPI(page *rod.Page, target...
    method findInVideoPlayer (line 244) | func (e *BrowserExtractor) findInVideoPlayer(page *rod.Page, targetExt...
    method findInPageSource (line 278) | func (e *BrowserExtractor) findInPageSource(page *rod.Page, targetExt ...
    method createLauncher (line 329) | func (e *BrowserExtractor) createLauncher(headless bool) *launcher.Lau...
    method getUserDataDir (line 359) | func (e *BrowserExtractor) getUserDataDir() string {
  function NewBrowserExtractor (line 27) | func NewBrowserExtractor(site *config.Site, visible bool) *BrowserExtrac...
  function NewGenericBrowserExtractor (line 32) | func NewGenericBrowserExtractor(visible bool) *BrowserExtractor {
  type extractionStrategy (line 48) | type extractionStrategy

FILE: internal/core/extractor/direct.go
  type DirectExtractor (line 14) | type DirectExtractor struct
    method Name (line 19) | func (d *DirectExtractor) Name() string {
    method Match (line 24) | func (d *DirectExtractor) Match(u *url.URL) bool {
    method Extract (line 30) | func (d *DirectExtractor) Extract(urlStr string) (Media, error) {
  function detectMediaType (line 135) | func detectMediaType(contentType, urlStr string) (MediaType, string) {
  function generateID (line 202) | func generateID(urlStr string) string {
  function init (line 227) | func init() {

FILE: internal/core/extractor/instagram.go
  type InstagramExtractor (line 9) | type InstagramExtractor struct
    method Name (line 11) | func (e *InstagramExtractor) Name() string {
    method Match (line 15) | func (e *InstagramExtractor) Match(u *url.URL) bool {
    method Extract (line 20) | func (e *InstagramExtractor) Extract(url string) (Media, error) {
  function init (line 24) | func init() {

FILE: internal/core/extractor/itunes.go
  type iTunesExtractor (line 12) | type iTunesExtractor struct
    method Name (line 14) | func (e *iTunesExtractor) Name() string {
    method Match (line 24) | func (e *iTunesExtractor) Match(u *url.URL) bool {
    method Extract (line 29) | func (e *iTunesExtractor) Extract(rawURL string) (Media, error) {
    method extractEpisode (line 52) | func (e *iTunesExtractor) extractEpisode(podcastID, episodeID string) ...
    method listEpisodes (line 92) | func (e *iTunesExtractor) listEpisodes() (*AudioMedia, error) {
  type iTunesLookupResponse (line 99) | type iTunesLookupResponse struct
  type iTunesLookupResult (line 104) | type iTunesLookupResult struct
  function init (line 117) | func init() {

FILE: internal/core/extractor/m3u8.go
  type M3U8Extractor (line 12) | type M3U8Extractor struct
    method Name (line 17) | func (m *M3U8Extractor) Name() string {
    method Match (line 22) | func (m *M3U8Extractor) Match(u *url.URL) bool {
    method Extract (line 34) | func (m *M3U8Extractor) Extract(urlStr string) (Media, error) {
  function generateM3U8ID (line 68) | func generateM3U8ID(urlStr string) string {

FILE: internal/core/extractor/registry.go
  function Register (line 41) | func Register(e Extractor, hosts ...string) {
  function NormalizeURL (line 48) | func NormalizeURL(rawURL string) (string, error) {
  function RegisterFallback (line 79) | func RegisterFallback(e Extractor) {
  function Match (line 85) | func Match(rawURL string) Extractor {
  function List (line 132) | func List() []Extractor {

FILE: internal/core/extractor/telegram.go
  constant TelegramDesktopAppID (line 19) | TelegramDesktopAppID   = telegram.DesktopAppID
  constant TelegramDesktopAppHash (line 20) | TelegramDesktopAppHash = telegram.DesktopAppHash
  type TelegramExtractor (line 28) | type TelegramExtractor struct
    method Name (line 32) | func (t *TelegramExtractor) Name() string {
    method Match (line 36) | func (t *TelegramExtractor) Match(u *url.URL) bool {
    method Extract (line 40) | func (t *TelegramExtractor) Extract(urlStr string) (Media, error) {
  function init (line 89) | func init() {

FILE: internal/core/extractor/telegram/constants.go
  constant DesktopAppID (line 7) | DesktopAppID   = 2040
  constant DesktopAppHash (line 8) | DesktopAppHash = "b18441a1ff607e10a989891a5462e627"

FILE: internal/core/extractor/telegram/download.go
  type DownloadResult (line 19) | type DownloadResult struct
  type DownloadOptions (line 26) | type DownloadOptions struct
  function Download (line 36) | func Download(urlStr string, outputPath string, progressFn func(download...
  function DownloadWithOptions (line 45) | func DownloadWithOptions(opts DownloadOptions) (*DownloadResult, error) {
  function resolveChannel (line 143) | func resolveChannel(ctx context.Context, api *tg.Client, msg *Message) (...
  type ChannelInfo (line 175) | type ChannelInfo struct
  function resolvePrivateChannel (line 182) | func resolvePrivateChannel(ctx context.Context, api *tg.Client, channelI...
  function getAllChannels (line 198) | func getAllChannels(ctx context.Context, api *tg.Client) ([]ChannelInfo,...
  function extractMessage (line 265) | func extractMessage(result tg.MessagesMessagesClass) (*tg.Message, error) {
  function downloadDocument (line 287) | func downloadDocument(
  function downloadPhoto (line 343) | func downloadPhoto(
  type progressWriter (line 397) | type progressWriter struct
    method Write (line 404) | func (pw *progressWriter) Write(p []byte) (int, error) {
  function sanitizeFilename (line 413) | func sanitizeFilename(name string) string {

FILE: internal/core/extractor/telegram/extractor.go
  type Extractor (line 16) | type Extractor struct
    method Name (line 19) | func (e *Extractor) Name() string {
    method Match (line 24) | func (e *Extractor) Match(u *url.URL) bool {
    method Extract (line 48) | func (e *Extractor) Extract(urlStr string) (*MediaInfo, error) {
    method extractMedia (line 64) | func (e *Extractor) extractMedia(ctx context.Context, msg *Message) (*...
    method extractMediaInfo (line 117) | func (e *Extractor) extractMediaInfo(msg *tg.Message) (*MediaInfo, err...
  type MediaInfo (line 33) | type MediaInfo struct

FILE: internal/core/extractor/telegram/media.go
  type ExtractedMedia (line 11) | type ExtractedMedia struct
  function ExtractDocumentInfo (line 24) | func ExtractDocumentInfo(doc *tg.Document, messageText string, msgID int...
  function FindLargestPhotoSize (line 67) | func FindLargestPhotoSize(sizes []tg.PhotoSizeClass) *tg.PhotoSize {
  function ExtFromMime (line 85) | func ExtFromMime(mime string) string {
  function truncateText (line 112) | func truncateText(s string, maxLen int) string {

FILE: internal/core/extractor/telegram/parser.go
  type Message (line 17) | type Message struct
  function ParseURL (line 25) | func ParseURL(urlStr string) (*Message, error) {
  function MatchURL (line 60) | func MatchURL(urlStr string) bool {

FILE: internal/core/extractor/telegram/session.go
  function SessionPath (line 10) | func SessionPath() string {
  function SessionFile (line 19) | func SessionFile() string {
  function SessionExists (line 24) | func SessionExists() bool {

FILE: internal/core/extractor/telegram/takeout.go
  type TakeoutSession (line 14) | type TakeoutSession struct
    method Start (line 26) | func (t *TakeoutSession) Start(ctx context.Context) error {
    method Finish (line 43) | func (t *TakeoutSession) Finish(ctx context.Context) error {
    method ID (line 61) | func (t *TakeoutSession) ID() int64 {
    method Active (line 66) | func (t *TakeoutSession) Active() bool {
    method Middleware (line 95) | func (t *TakeoutSession) Middleware() telegram.Middleware {
  function NewTakeoutSession (line 20) | func NewTakeoutSession(api *tg.Client) *TakeoutSession {
  type takeoutMiddleware (line 71) | type takeoutMiddleware struct
    method Handle (line 85) | func (t takeoutMiddleware) Handle(next tg.Invoker) telegram.InvokeFunc {
  type nopDecoder (line 76) | type nopDecoder struct
    method Decode (line 80) | func (n nopDecoder) Decode(_ *bin.Buffer) error {

FILE: internal/core/extractor/tiktok.go
  type TikTokExtractor (line 9) | type TikTokExtractor struct
    method Name (line 11) | func (e *TikTokExtractor) Name() string {
    method Match (line 15) | func (e *TikTokExtractor) Match(u *url.URL) bool {
    method Extract (line 20) | func (e *TikTokExtractor) Extract(url string) (Media, error) {
  function init (line 24) | func init() {

FILE: internal/core/extractor/twitter.go
  constant twitterBearerToken (line 18) | twitterBearerToken = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOuH5E6I8...
  constant twitterGuestTokenURL (line 20) | twitterGuestTokenURL  = "https://api.x.com/1.1/guest/activate.json"
  constant twitterGraphQLURL (line 21) | twitterGraphQLURL     = "https://x.com/i/api/graphql/2ICDjqPd81tulZcYrtp...
  constant twitterSyndicationURL (line 22) | twitterSyndicationURL = "https://cdn.syndication.twimg.com/tweet-result"
  type TwitterError (line 31) | type TwitterError struct
    method Error (line 36) | func (e *TwitterError) Error() string {
  constant TwitterErrorNSFW (line 42) | TwitterErrorNSFW        = "nsfw"
  constant TwitterErrorProtected (line 43) | TwitterErrorProtected   = "protected"
  constant TwitterErrorUnavailable (line 44) | TwitterErrorUnavailable = "unavailable"
  type TwitterExtractor (line 48) | type TwitterExtractor struct
    method Name (line 56) | func (t *TwitterExtractor) Name() string {
    method Match (line 61) | func (t *TwitterExtractor) Match(u *url.URL) bool {
    method SetAuth (line 67) | func (t *TwitterExtractor) SetAuth(authToken string) {
    method IsAuthenticated (line 72) | func (t *TwitterExtractor) IsAuthenticated() bool {
    method Extract (line 77) | func (t *TwitterExtractor) Extract(urlStr string) (Media, error) {
    method fetchFromSyndication (line 124) | func (t *TwitterExtractor) fetchFromSyndication(tweetID string) (Media...
    method fetchGuestToken (line 159) | func (t *TwitterExtractor) fetchGuestToken() error {
    method fetchFromGraphQL (line 190) | func (t *TwitterExtractor) fetchFromGraphQL(tweetID string) (Media, er...
    method fetchCsrfToken (line 260) | func (t *TwitterExtractor) fetchCsrfToken() error {
    method fetchFromGraphQLAuth (line 286) | func (t *TwitterExtractor) fetchFromGraphQLAuth(tweetID string) (Media...
    method parseSyndicationResponse (line 371) | func (t *TwitterExtractor) parseSyndicationResponse(data *syndicationR...
    method parseGraphQLResponse (line 508) | func (t *TwitterExtractor) parseGraphQLResponse(body []byte, tweetID s...
  type syndicationResponse (line 659) | type syndicationResponse struct
  type graphQLResponse (line 687) | type graphQLResponse struct
  type graphQLTweetResult (line 695) | type graphQLTweetResult struct
  type graphQLCore (line 708) | type graphQLCore struct
  type graphQLLegacy (line 718) | type graphQLLegacy struct
  function truncateText (line 742) | func truncateText(s string, maxLen int) string {
  function extractResolutionFromURL (line 753) | func extractResolutionFromURL(url string) (width, height int) {
  function estimateQualityFromBitrate (line 763) | func estimateQualityFromBitrate(bitrate int) string {
  function getHighQualityImageURL (line 777) | func getHighQualityImageURL(imageURL string) string {
  function getImageExtension (line 791) | func getImageExtension(imageURL string) string {
  function init (line 803) | func init() {

FILE: internal/core/extractor/types.go
  type MediaType (line 12) | type MediaType
  constant MediaTypeVideo (line 15) | MediaTypeVideo MediaType = "video"
  constant MediaTypeAudio (line 16) | MediaTypeAudio MediaType = "audio"
  constant MediaTypeImage (line 17) | MediaTypeImage MediaType = "image"
  type Media (line 21) | type Media interface
  type Extractor (line 29) | type Extractor interface
  type VideoMedia (line 42) | type VideoMedia struct
    method GetID (line 51) | func (v *VideoMedia) GetID() string       { return v.ID }
    method GetTitle (line 52) | func (v *VideoMedia) GetTitle() string    { return v.Title }
    method GetUploader (line 53) | func (v *VideoMedia) GetUploader() string { return v.Uploader }
    method Type (line 54) | func (v *VideoMedia) Type() MediaType     { return MediaTypeVideo }
  type VideoFormat (line 57) | type VideoFormat struct
    method QualityLabel (line 69) | func (f *VideoFormat) QualityLabel() string {
  type AudioMedia (line 80) | type AudioMedia struct
    method GetID (line 89) | func (a *AudioMedia) GetID() string       { return a.ID }
    method GetTitle (line 90) | func (a *AudioMedia) GetTitle() string    { return a.Title }
    method GetUploader (line 91) | func (a *AudioMedia) GetUploader() string { return a.Uploader }
    method Type (line 92) | func (a *AudioMedia) Type() MediaType     { return MediaTypeAudio }
  type ImageMedia (line 95) | type ImageMedia struct
    method GetID (line 102) | func (i *ImageMedia) GetID() string       { return i.ID }
    method GetTitle (line 103) | func (i *ImageMedia) GetTitle() string    { return i.Title }
    method GetUploader (line 104) | func (i *ImageMedia) GetUploader() string { return i.Uploader }
    method Type (line 105) | func (i *ImageMedia) Type() MediaType     { return MediaTypeImage }
  type MultiVideoMedia (line 108) | type MultiVideoMedia struct
    method GetID (line 115) | func (m *MultiVideoMedia) GetID() string       { return m.ID }
    method GetTitle (line 116) | func (m *MultiVideoMedia) GetTitle() string    { return m.Title }
    method GetUploader (line 117) | func (m *MultiVideoMedia) GetUploader() string { return m.Uploader }
    method Type (line 118) | func (m *MultiVideoMedia) Type() MediaType     { return MediaTypeVideo }
  type Image (line 121) | type Image struct
  function SanitizeFilename (line 129) | func SanitizeFilename(name string) string {

FILE: internal/core/extractor/types_test.go
  function TestSanitizeFilename (line 5) | func TestSanitizeFilename(t *testing.T) {

FILE: internal/core/extractor/xiaohongshu.go
  type XiaohongshuExtractor (line 21) | type XiaohongshuExtractor struct
    method SetVisible (line 26) | func (e *XiaohongshuExtractor) SetVisible(visible bool) {
    method Name (line 30) | func (e *XiaohongshuExtractor) Name() string {
    method Match (line 34) | func (e *XiaohongshuExtractor) Match(u *url.URL) bool {
    method Extract (line 66) | func (e *XiaohongshuExtractor) Extract(rawURL string) (Media, error) {
    method extractNoteID (line 87) | func (e *XiaohongshuExtractor) extractNoteID(rawURL string) string {
    method resolveShortURL (line 104) | func (e *XiaohongshuExtractor) resolveShortURL(shortURL string) (strin...
    method extractWithBrowser (line 137) | func (e *XiaohongshuExtractor) extractWithBrowser(targetURL, noteID st...
    method extractVideo (line 280) | func (e *XiaohongshuExtractor) extractVideo(id, title, uploader string...
    method extractImages (line 317) | func (e *XiaohongshuExtractor) extractImages(id, title, uploader strin...
    method createLauncher (line 352) | func (e *XiaohongshuExtractor) createLauncher(headless bool) *launcher...
    method getUserDataDir (line 387) | func (e *XiaohongshuExtractor) getUserDataDir() string {
    method loadCookies (line 396) | func (e *XiaohongshuExtractor) loadCookies(browser *rod.Browser) {
    method saveCookies (line 418) | func (e *XiaohongshuExtractor) saveCookies(browser *rod.Browser) {
  type xhsNoteDetail (line 39) | type xhsNoteDetail struct
  function init (line 460) | func init() {

FILE: internal/core/extractor/xiaoyuzhou.go
  type XiaoyuzhouExtractor (line 14) | type XiaoyuzhouExtractor struct
    method Name (line 16) | func (e *XiaoyuzhouExtractor) Name() string {
    method Match (line 20) | func (e *XiaoyuzhouExtractor) Match(u *url.URL) bool {
    method Extract (line 25) | func (e *XiaoyuzhouExtractor) Extract(url string) (Media, error) {
    method extractEpisode (line 36) | func (e *XiaoyuzhouExtractor) extractEpisode(url string) (*AudioMedia,...
    method extractPodcast (line 122) | func (e *XiaoyuzhouExtractor) extractPodcast(_ string) (*AudioMedia, e...
  function init (line 129) | func init() {

FILE: internal/core/extractor/youtube.go
  type YouTubeDockerRequiredError (line 18) | type YouTubeDockerRequiredError struct
    method Error (line 22) | func (e *YouTubeDockerRequiredError) Error() string {
  type YouTubeDirectDownload (line 27) | type YouTubeDirectDownload struct
    method GetID (line 33) | func (y *YouTubeDirectDownload) GetID() string       { return y.URL }
    method GetTitle (line 34) | func (y *YouTubeDirectDownload) GetTitle() string    { return "YouTube...
    method GetUploader (line 35) | func (y *YouTubeDirectDownload) GetUploader() string { return "" }
    method Type (line 36) | func (y *YouTubeDirectDownload) Type() MediaType     { return MediaTyp...
  type ytdlpExtractor (line 39) | type ytdlpExtractor struct
    method Name (line 41) | func (e *ytdlpExtractor) Name() string {
    method Match (line 45) | func (e *ytdlpExtractor) Match(u *url.URL) bool {
    method Extract (line 54) | func (e *ytdlpExtractor) Extract(urlStr string) (Media, error) {
  function DownloadWithYtdlp (line 68) | func DownloadWithYtdlp(url, outputDir string) error {
  function DownloadWithYtdlpProgress (line 73) | func DownloadWithYtdlpProgress(ctx context.Context, url, outputDir strin...
  function downloadWithYoutubeDL (line 148) | func downloadWithYoutubeDL(ctx context.Context, url, outputDir string) e...
  function init (line 162) | func init() {

FILE: internal/core/i18n/i18n.go
  type Translations (line 15) | type Translations struct
  type ConfigTranslations (line 29) | type ConfigTranslations struct
  type ConfigReviewTranslations (line 47) | type ConfigReviewTranslations struct
  type HelpTranslations (line 54) | type HelpTranslations struct
  type DownloadTranslations (line 62) | type DownloadTranslations struct
  type ErrorTranslations (line 80) | type ErrorTranslations struct
  type SearchTranslations (line 89) | type SearchTranslations struct
  type TwitterTranslations (line 102) | type TwitterTranslations struct
  type SitesTranslations (line 118) | type SitesTranslations struct
  type UITranslations (line 132) | type UITranslations struct
  type ServerTranslations (line 238) | type ServerTranslations struct
  type YouTubeTranslations (line 244) | type YouTubeTranslations struct
  function GetTranslations (line 271) | func GetTranslations(lang string) *Translations {
  function loadTranslations (line 297) | func loadTranslations(lang string) (*Translations, error) {
  function T (line 313) | func T(lang string) *Translations {

FILE: internal/core/site/bilibili/auth.go
  type Auth (line 16) | type Auth struct
    method GenerateQRCode (line 53) | func (a *Auth) GenerateQRCode() (*QRSession, error) {
    method PollQRStatus (line 98) | func (a *Auth) PollQRStatus(qrcodeKey string) (QRStatus, *Credentials,...
    method parseCredentialsFromURL (line 150) | func (a *Auth) parseCredentialsFromURL(urlStr string) (*Credentials, e...
    method SaveCredentials (line 171) | func (a *Auth) SaveCredentials(creds *Credentials) error {
    method LoadCredentials (line 184) | func (a *Auth) LoadCredentials() *Credentials {
    method ValidateCredentials (line 212) | func (a *Auth) ValidateCredentials(creds *Credentials) (string, error) {
    method setHeaders (line 275) | func (a *Auth) setHeaders(req *http.Request) {
  type QRSession (line 21) | type QRSession struct
  type QRStatus (line 27) | type QRStatus
    method String (line 259) | func (s QRStatus) String() string {
  constant QRWaiting (line 30) | QRWaiting   QRStatus = 86101
  constant QRScanned (line 31) | QRScanned   QRStatus = 86090
  constant QRExpired (line 32) | QRExpired   QRStatus = 86038
  constant QRConfirmed (line 33) | QRConfirmed QRStatus = 0
  type Credentials (line 37) | type Credentials struct
    method ToCookieString (line 178) | func (c *Credentials) ToCookieString() string {
  function NewAuth (line 44) | func NewAuth() *Auth {
  function ParseCookieString (line 194) | func ParseCookieString(cookie string) *Credentials {

FILE: internal/core/tracker/kuaidi100.go
  constant Kuaidi100APIURL (line 16) | Kuaidi100APIURL         = "https://poll.kuaidi100.com/poll/query.do"
  constant Kuaidi100AutoNumberURL (line 17) | Kuaidi100AutoNumberURL  = "http://www.kuaidi100.com/autonumber/auto"
  constant Kuaidi100DeliveryTimeURL (line 18) | Kuaidi100DeliveryTimeURL = "https://api.kuaidi100.com/label/order?method...
  type Kuaidi100Config (line 22) | type Kuaidi100Config struct
  type Kuaidi100Tracker (line 29) | type Kuaidi100Tracker struct
    method SetSecret (line 62) | func (t *Kuaidi100Tracker) SetSecret(secret string) {
    method Track (line 131) | func (t *Kuaidi100Tracker) Track(courierCode, trackingNumber string) (...
    method TrackWithPhone (line 136) | func (t *Kuaidi100Tracker) TrackWithPhone(courierCode, trackingNumber,...
    method AutoNumber (line 199) | func (t *Kuaidi100Tracker) AutoNumber(trackingNumber string) ([]AutoNu...
    method EstimateDeliveryTime (line 253) | func (t *Kuaidi100Tracker) EstimateDeliveryTime(param DeliveryTimePara...
  function NewKuaidi100Tracker (line 35) | func NewKuaidi100Tracker(key, customer string) *Kuaidi100Tracker {
  function NewKuaidi100TrackerWithSecret (line 48) | func NewKuaidi100TrackerWithSecret(key, customer, secret string) *Kuaidi...
  type QueryParam (line 67) | type QueryParam struct
  type TrackingResponse (line 79) | type TrackingResponse struct
    method StateDescription (line 103) | func (r *TrackingResponse) StateDescription() string {
    method IsDelivered (line 126) | func (r *TrackingResponse) IsDelivered() bool {
  type TrackingData (line 91) | type TrackingData struct
  type AutoNumberResponse (line 190) | type AutoNumberResponse struct
  type DeliveryTimeParam (line 224) | type DeliveryTimeParam struct
  type DeliveryTimeResponse (line 233) | type DeliveryTimeResponse struct
  type DeliveryTimeData (line 241) | type DeliveryTimeData struct
  type CourierInfo (line 307) | type CourierInfo struct
  function GetCourierCode (line 392) | func GetCourierCode(alias string) string {
  function GetCourierInfo (line 402) | func GetCourierInfo(alias string) *CourierInfo {
  function ListCouriers (line 411) | func ListCouriers() []CourierInfo {

FILE: internal/core/webdav/client.go
  type Client (line 18) | type Client struct
    method Stat (line 89) | func (c *Client) Stat(ctx context.Context, filePath string) (*FileInfo...
    method List (line 104) | func (c *Client) List(ctx context.Context, dirPath string) ([]FileInfo...
    method Open (line 141) | func (c *Client) Open(ctx context.Context, filePath string) (io.ReadCl...
    method GetFileURL (line 233) | func (c *Client) GetFileURL(filePath string) string {
    method GetAuthHeader (line 242) | func (c *Client) GetAuthHeader() string {
    method SupportsRangeRequests (line 251) | func (c *Client) SupportsRangeRequests(ctx context.Context, filePath s...
  type FileInfo (line 26) | type FileInfo struct
  function NewClient (line 35) | func NewClient(rawURL string) (*Client, error) {
  function ParseURL (line 80) | func ParseURL(rawURL string) (string, error) {
  function IsWebDAVURL (line 161) | func IsWebDAVURL(rawURL string) bool {
  function IsRemotePath (line 168) | func IsRemotePath(rawURL string) bool {
  function ParseRemotePath (line 191) | func ParseRemotePath(remotePath string) (remoteName, filePath string, er...
  function NewClientFromConfig (line 208) | func NewClientFromConfig(server *config.WebDAVServer) (*Client, error) {
  function ExtractFilename (line 228) | func ExtractFilename(filePath string) string {

FILE: internal/server/auth.go
  constant SessionCookieName (line 14) | SessionCookieName = "vget_session"
  constant SessionDuration (line 16) | SessionDuration = 24 * time.Hour
  constant APITokenDuration (line 18) | APITokenDuration = 365 * 24 * time.Hour
  type JWTClaims (line 22) | type JWTClaims struct
  type GenerateTokenRequest (line 29) | type GenerateTokenRequest struct
  method generateJWT (line 34) | func (s *Server) generateJWT(tokenType string, duration time.Duration, c...
  method validateJWT (line 52) | func (s *Server) validateJWT(tokenString string) (*JWTClaims, error) {
  method jwtAuthMiddleware (line 69) | func (s *Server) jwtAuthMiddleware() gin.HandlerFunc {
  method setSessionCookie (line 125) | func (s *Server) setSessionCookie(c *gin.Context) {
  method handleAuthStatus (line 157) | func (s *Server) handleAuthStatus(c *gin.Context) {
  method handleGenerateToken (line 169) | func (s *Server) handleGenerateToken(c *gin.Context) {

FILE: internal/server/bilibili.go
  method handleBilibiliQRGenerate (line 13) | func (s *Server) handleBilibiliQRGenerate(c *gin.Context) {
  method handleBilibiliQRPoll (line 37) | func (s *Server) handleBilibiliQRPoll(c *gin.Context) {
  method handleBilibiliStatus (line 103) | func (s *Server) handleBilibiliStatus(c *gin.Context) {

FILE: internal/server/embed.go
  function GetDistFS (line 13) | func GetDistFS() fs.FS {

FILE: internal/server/history.go
  constant historyDBFile (line 14) | historyDBFile = "history.db"
  type HistoryRecord (line 17) | type HistoryRecord struct
  type HistoryDB (line 30) | type HistoryDB struct
    method Close (line 81) | func (h *HistoryDB) Close() error {
    method RecordJob (line 89) | func (h *HistoryDB) RecordJob(job *Job) error {
    method GetHistory (line 115) | func (h *HistoryDB) GetHistory(limit, offset int) ([]HistoryRecord, in...
    method GetStats (line 171) | func (h *HistoryDB) GetStats() (completed int, failed int, totalBytes ...
    method DeleteRecord (line 187) | func (h *HistoryDB) DeleteRecord(id string) error {
    method ClearHistory (line 205) | func (h *HistoryDB) ClearHistory() (int64, error) {
  function NewHistoryDB (line 36) | func NewHistoryDB() (*HistoryDB, error) {

FILE: internal/server/job.go
  type JobStatus (line 16) | type JobStatus
  constant JobStatusQueued (line 19) | JobStatusQueued      JobStatus = "queued"
  constant JobStatusDownloading (line 20) | JobStatusDownloading JobStatus = "downloading"
  constant JobStatusCompleted (line 21) | JobStatusCompleted   JobStatus = "completed"
  constant JobStatusFailed (line 22) | JobStatusFailed      JobStatus = "failed"
  constant JobStatusCancelled (line 23) | JobStatusCancelled   JobStatus = "cancelled"
  type Job (line 27) | type Job struct
  type JobQueue (line 45) | type JobQueue struct
    method SetHistoryDB (line 82) | func (jq *JobQueue) SetHistoryDB(db *HistoryDB) {
    method Start (line 87) | func (jq *JobQueue) Start() {
    method Stop (line 100) | func (jq *JobQueue) Stop() {
    method worker (line 109) | func (jq *JobQueue) worker() {
    method processJob (line 117) | func (jq *JobQueue) processJob(job *Job) {
    method recordJobToHistory (line 143) | func (jq *JobQueue) recordJobToHistory(id string) {
    method cleanupLoop (line 167) | func (jq *JobQueue) cleanupLoop() {
    method cleanupOldJobs (line 178) | func (jq *JobQueue) cleanupOldJobs() {
    method ClearHistory (line 193) | func (jq *JobQueue) ClearHistory() int {
    method RemoveJob (line 208) | func (jq *JobQueue) RemoveJob(id string) bool {
    method AddFailedJob (line 227) | func (jq *JobQueue) AddFailedJob(rawURL, errorMsg string) *Job {
    method AddJob (line 248) | func (jq *JobQueue) AddJob(rawURL, filename string) (*Job, error) {
    method GetJob (line 293) | func (jq *JobQueue) GetJob(id string) *Job {
    method GetAllJobs (line 306) | func (jq *JobQueue) GetAllJobs() []*Job {
    method CancelJob (line 319) | func (jq *JobQueue) CancelJob(id string) bool {
    method updateJobStatus (line 339) | func (jq *JobQueue) updateJobStatus(id string, status JobStatus, progr...
    method updateJobProgressBytes (line 355) | func (jq *JobQueue) updateJobProgressBytes(id string, downloaded, tota...
  type DownloadFunc (line 60) | type DownloadFunc
  function NewJobQueue (line 63) | func NewJobQueue(maxConcurrent int, outputDir string, downloadFn Downloa...
  function generateJobID (line 369) | func generateJobID() (string, error) {

FILE: internal/server/podcast.go
  type PodcastSearchRequest (line 18) | type PodcastSearchRequest struct
  type PodcastSearchResult (line 23) | type PodcastSearchResult struct
  type PodcastChannel (line 29) | type PodcastChannel struct
  type PodcastEpisode (line 39) | type PodcastEpisode struct
  type PodcastEpisodesRequest (line 49) | type PodcastEpisodesRequest struct
  function containsChinese (line 55) | func containsChinese(s string) bool {
  method handlePodcastSearch (line 65) | func (s *Server) handlePodcastSearch(c *gin.Context) {
  method handlePodcastEpisodes (line 169) | func (s *Server) handlePodcastEpisodes(c *gin.Context) {
  type xiaoyuzhouSearchResponse (line 219) | type xiaoyuzhouSearchResponse struct
  function searchXiaoyuzhouAPI (line 246) | func searchXiaoyuzhouAPI(query string) (*PodcastSearchResult, error) {
  function fetchXiaoyuzhouEpisodesAPI (line 300) | func fetchXiaoyuzhouEpisodesAPI(podcastID string) ([]PodcastEpisode, str...
  type iTunesSearchResponse (line 375) | type iTunesSearchResponse struct
  function searchITunesAPI (line 396) | func searchITunesAPI(query string) (*PodcastSearchResult, error) {
  function fetchITunesEpisodesAPI (line 474) | func fetchITunesEpisodesAPI(podcastID string) ([]PodcastEpisode, string,...

FILE: internal/server/server.go
  type Response (line 28) | type Response struct
  type DownloadRequest (line 35) | type DownloadRequest struct
  type BulkDownloadRequest (line 42) | type BulkDownloadRequest struct
  type Server (line 47) | type Server struct
    method Start (line 85) | func (s *Server) Start() error {
    method Stop (line 193) | func (s *Server) Stop(ctx context.Context) error {
    method loggingMiddleware (line 203) | func (s *Server) loggingMiddleware() gin.HandlerFunc {
    method setupStaticFiles (line 212) | func (s *Server) setupStaticFiles(distFS fs.FS) {
    method handleHealth (line 251) | func (s *Server) handleHealth(c *gin.Context) {
    method handleFileDownload (line 263) | func (s *Server) handleFileDownload(c *gin.Context) {
    method handleDownload (line 311) | func (s *Server) handleDownload(c *gin.Context) {
    method handleBulkDownload (line 349) | func (s *Server) handleBulkDownload(c *gin.Context) {
    method handleStatus (line 412) | func (s *Server) handleStatus(c *gin.Context) {
    method handleGetJobs (line 438) | func (s *Server) handleGetJobs(c *gin.Context) {
    method handleClearJobs (line 464) | func (s *Server) handleClearJobs(c *gin.Context) {
    method handleDeleteJob (line 481) | func (s *Server) handleDeleteJob(c *gin.Context) {
    method handleGetConfig (line 517) | func (s *Server) handleGetConfig(c *gin.Context) {
    method handleSetConfig (line 551) | func (s *Server) handleSetConfig(c *gin.Context) {
    method handleUpdateConfig (line 609) | func (s *Server) handleUpdateConfig(c *gin.Context) {
    method handleI18n (line 649) | func (s *Server) handleI18n(c *gin.Context) {
    method handleGetWebDAV (line 679) | func (s *Server) handleGetWebDAV(c *gin.Context) {
    method handleAddWebDAV (line 698) | func (s *Server) handleAddWebDAV(c *gin.Context) {
    method handleDeleteWebDAV (line 733) | func (s *Server) handleDeleteWebDAV(c *gin.Context) {
    method handleKuaidi100 (line 773) | func (s *Server) handleKuaidi100(c *gin.Context) {
    method handleGetTorrentConfig (line 852) | func (s *Server) handleGetTorrentConfig(c *gin.Context) {
    method handleSetTorrentConfig (line 870) | func (s *Server) handleSetTorrentConfig(c *gin.Context) {
    method handleTestTorrentConnection (line 933) | func (s *Server) handleTestTorrentConnection(c *gin.Context) {
    method handleAddTorrent (line 972) | func (s *Server) handleAddTorrent(c *gin.Context) {
    method handleListTorrents (line 1053) | func (s *Server) handleListTorrents(c *gin.Context) {
    method createTorrentClient (line 1126) | func (s *Server) createTorrentClient(cfg *config.TorrentConfig) (torre...
    method setConfigValue (line 1140) | func (s *Server) setConfigValue(cfg *config.Config, key, value string)...
    method downloadWebDAV (line 1183) | func (s *Server) downloadWebDAV(ctx context.Context, rawURL, filename ...
    method downloadWithExtractor (line 1275) | func (s *Server) downloadWithExtractor(ctx context.Context, url, filen...
    method updateJobFilename (line 1430) | func (s *Server) updateJobFilename(url, filename string) {
    method downloadVideoWithAudio (line 1445) | func (s *Server) downloadVideoWithAudio(ctx context.Context, format *e...
    method downloadAndStream (line 1531) | func (s *Server) downloadAndStream(c *gin.Context, url, filename strin...
    method handleGetHistory (line 1794) | func (s *Server) handleGetHistory(c *gin.Context) {
    method handleClearHistory (line 1850) | func (s *Server) handleClearHistory(c *gin.Context) {
    method handleDeleteHistory (line 1879) | func (s *Server) handleDeleteHistory(c *gin.Context) {
  function NewServer (line 59) | func NewServer(port int, outputDir, apiKey string, maxConcurrent int) *S...
  type ConfigSetRequest (line 507) | type ConfigSetRequest struct
  type ConfigRequest (line 513) | type ConfigRequest struct
  type WebDAVConfigRequest (line 672) | type WebDAVConfigRequest struct
  type TrackRequest (line 768) | type TrackRequest struct
  type TorrentConfigRequest (line 835) | type TorrentConfigRequest struct
  type TorrentAddRequest (line 846) | type TorrentAddRequest struct
  function downloadWebDAVMultiStream (line 1269) | func downloadWebDAVMultiStream(ctx context.Context, url, authHeader, out...
  function selectBestFormat (line 1649) | func selectBestFormat(formats []extractor.VideoFormat) *extractor.VideoF...
  function downloadFile (line 1676) | func downloadFile(ctx context.Context, url, outputPath string, headers m...
  function streamFile (line 1747) | func streamFile(w http.ResponseWriter, url, filename string, headers map...

FILE: internal/server/webdav_browse.go
  type WebDAVRemoteInfo (line 13) | type WebDAVRemoteInfo struct
  type WebDAVFileInfo (line 20) | type WebDAVFileInfo struct
  type WebDAVDownloadRequest (line 28) | type WebDAVDownloadRequest struct
  method handleWebDAVRemotes (line 34) | func (s *Server) handleWebDAVRemotes(c *gin.Context) {
  method handleWebDAVList (line 59) | func (s *Server) handleWebDAVList(c *gin.Context) {
  method handleWebDAVDownload (line 137) | func (s *Server) handleWebDAVDownload(c *gin.Context) {

FILE: internal/torrent/client.go
  type TorrentState (line 12) | type TorrentState
    method String (line 25) | func (s TorrentState) String() string {
  constant StateStopped (line 15) | StateStopped TorrentState = iota
  constant StateQueued (line 16) | StateQueued
  constant StateDownloading (line 17) | StateDownloading
  constant StateSeeding (line 18) | StateSeeding
  constant StatePaused (line 19) | StatePaused
  constant StateChecking (line 20) | StateChecking
  constant StateError (line 21) | StateError
  constant StateUnknown (line 22) | StateUnknown
  type TorrentInfo (line 47) | type TorrentInfo struct
  type AddOptions (line 65) | type AddOptions struct
  type AddResult (line 86) | type AddResult struct
  type Client (line 94) | type Client interface
  type ClientType (line 132) | type ClientType
  constant ClientTransmission (line 135) | ClientTransmission ClientType = "transmission"
  constant ClientQBittorrent (line 136) | ClientQBittorrent  ClientType = "qbittorrent"
  constant ClientSynology (line 137) | ClientSynology     ClientType = "synology"
  type Config (line 141) | type Config struct
  function NewClient (line 161) | func NewClient(cfg *Config) (Client, error) {
  function IsMagnetLink (line 175) | func IsMagnetLink(url string) bool {
  function IsTorrentURL (line 180) | func IsTorrentURL(url string) bool {

FILE: internal/torrent/qbittorrent.go
  type QBittorrentClient (line 20) | type QBittorrentClient struct
    method Name (line 70) | func (c *QBittorrentClient) Name() string {
    method Connect (line 75) | func (c *QBittorrentClient) Connect() error {
    method getAPIVersion (line 110) | func (c *QBittorrentClient) getAPIVersion() {
    method Close (line 122) | func (c *QBittorrentClient) Close() error {
    method AddMagnet (line 132) | func (c *QBittorrentClient) AddMagnet(magnetURL string, opts *AddOptio...
    method AddTorrentURL (line 141) | func (c *QBittorrentClient) AddTorrentURL(torrentURL string, opts *Add...
    method AddTorrentFile (line 146) | func (c *QBittorrentClient) AddTorrentFile(path string, opts *AddOptio...
    method addTorrent (line 163) | func (c *QBittorrentClient) addTorrent(urls string, file *torrentFile,...
    method GetTorrent (line 272) | func (c *QBittorrentClient) GetTorrent(id string) (*TorrentInfo, error) {
    method ListTorrents (line 286) | func (c *QBittorrentClient) ListTorrents() ([]TorrentInfo, error) {
    method getTorrents (line 290) | func (c *QBittorrentClient) getTorrents(hash string) ([]TorrentInfo, e...
    method convertTorrent (line 342) | func (c *QBittorrentClient) convertTorrent(t *qbTorrent) TorrentInfo {
    method convertState (line 360) | func (c *QBittorrentClient) convertState(state string) TorrentState {
    method RemoveTorrent (line 382) | func (c *QBittorrentClient) RemoveTorrent(id string, deleteData bool) ...
    method PauseTorrent (line 401) | func (c *QBittorrentClient) PauseTorrent(id string) error {
    method ResumeTorrent (line 419) | func (c *QBittorrentClient) ResumeTorrent(id string) error {
  constant qbStateError (line 29) | qbStateError              = "error"
  constant qbStateMissingFiles (line 30) | qbStateMissingFiles       = "missingFiles"
  constant qbStateUploading (line 31) | qbStateUploading          = "uploading"
  constant qbStatePausedUP (line 32) | qbStatePausedUP           = "pausedUP"
  constant qbStateQueuedUP (line 33) | qbStateQueuedUP           = "queuedUP"
  constant qbStateStalledUP (line 34) | qbStateStalledUP          = "stalledUP"
  constant qbStateCheckingUP (line 35) | qbStateCheckingUP         = "checkingUP"
  constant qbStateForcedUP (line 36) | qbStateForcedUP           = "forcedUP"
  constant qbStateAllocating (line 37) | qbStateAllocating         = "allocating"
  constant qbStateDownloading (line 38) | qbStateDownloading        = "downloading"
  constant qbStateMetaDL (line 39) | qbStateMetaDL             = "metaDL"
  constant qbStatePausedDL (line 40) | qbStatePausedDL           = "pausedDL"
  constant qbStateQueuedDL (line 41) | qbStateQueuedDL           = "queuedDL"
  constant qbStateStalledDL (line 42) | qbStateStalledDL          = "stalledDL"
  constant qbStateCheckingDL (line 43) | qbStateCheckingDL         = "checkingDL"
  constant qbStateForcedDL (line 44) | qbStateForcedDL           = "forcedDL"
  constant qbStateCheckingResumeData (line 45) | qbStateCheckingResumeData = "checkingResumeData"
  constant qbStateMoving (line 46) | qbStateMoving             = "moving"
  constant qbStateUnknown (line 47) | qbStateUnknown            = "unknown"
  function NewQBittorrentClient (line 51) | func NewQBittorrentClient(cfg *Config) *QBittorrentClient {
  type torrentFile (line 158) | type torrentFile struct
  function extractHashFromMagnet (line 249) | func extractHashFromMagnet(magnet string) string {
  type qbTorrent (line 319) | type qbTorrent struct

FILE: internal/torrent/synology.go
  type SynologyClient (line 20) | type SynologyClient struct
    method Name (line 94) | func (c *SynologyClient) Name() string {
    method Connect (line 99) | func (c *SynologyClient) Connect() error {
    method Close (line 126) | func (c *SynologyClient) Close() error {
    method doRequest (line 144) | func (c *SynologyClient) doRequest(path string, params url.Values, bod...
    method convertError (line 187) | func (c *SynologyClient) convertError(err *synError) error {
    method AddMagnet (line 217) | func (c *SynologyClient) AddMagnet(magnetURL string, opts *AddOptions)...
    method AddTorrentURL (line 248) | func (c *SynologyClient) AddTorrentURL(torrentURL string, opts *AddOpt...
    method AddTorrentFile (line 268) | func (c *SynologyClient) AddTorrentFile(path string, opts *AddOptions)...
    method GetTorrent (line 325) | func (c *SynologyClient) GetTorrent(id string) (*TorrentInfo, error) {
    method ListTorrents (line 353) | func (c *SynologyClient) ListTorrents() ([]TorrentInfo, error) {
    method convertTask (line 416) | func (c *SynologyClient) convertTask(t *synTask) *TorrentInfo {
    method convertStatus (line 451) | func (c *SynologyClient) convertStatus(status string) TorrentState {
    method RemoveTorrent (line 475) | func (c *SynologyClient) RemoveTorrent(id string, deleteData bool) err...
    method PauseTorrent (line 488) | func (c *SynologyClient) PauseTorrent(id string) error {
    method ResumeTorrent (line 500) | func (c *SynologyClient) ResumeTorrent(id string) error {
  type synResponse (line 28) | type synResponse struct
  type synError (line 34) | type synError struct
  constant synErrUnknown (line 40) | synErrUnknown           = 100
  constant synErrInvalidParam (line 41) | synErrInvalidParam      = 101
  constant synErrAPINotExists (line 42) | synErrAPINotExists      = 102
  constant synErrMethodNotExists (line 43) | synErrMethodNotExists   = 103
  constant synErrVersionNotSupport (line 44) | synErrVersionNotSupport = 104
  constant synErrPermDenied (line 45) | synErrPermDenied        = 105
  constant synErrTimeout (line 46) | synErrTimeout           = 106
  constant synErrDuplicate (line 47) | synErrDuplicate         = 107
  constant synStatusWaiting (line 56) | synStatusWaiting     = "waiting"
  constant synStatusDownloading (line 57) | synStatusDownloading = "downloading"
  constant synStatusPaused (line 58) | synStatusPaused      = "paused"
  constant synStatusFinishing (line 59) | synStatusFinishing   = "finishing"
  constant synStatusFinished (line 60) | synStatusFinished    = "finished"
  constant synStatusHashChecking (line 61) | synStatusHashChecking = "hash_checking"
  constant synStatusSeeding (line 62) | synStatusSeeding     = "seeding"
  constant synStatusFileHosting (line 63) | synStatusFileHosting = "filehosting_waiting"
  constant synStatusExtracting (line 64) | synStatusExtracting  = "extracting"
  constant synStatusError (line 65) | synStatusError       = "error"
  function NewSynologyClient (line 69) | func NewSynologyClient(cfg *Config) *SynologyClient {
  type synTask (line 385) | type synTask struct
  type synAdditional (line 394) | type synAdditional struct
  type synDetail (line 399) | type synDetail struct
  type synTransfer (line 409) | type synTransfer struct

FILE: internal/torrent/transmission.go
  type TransmissionClient (line 16) | type TransmissionClient struct
    method Name (line 63) | func (c *TransmissionClient) Name() string {
    method Connect (line 68) | func (c *TransmissionClient) Connect() error {
    method Close (line 78) | func (c *TransmissionClient) Close() error {
    method doRequest (line 85) | func (c *TransmissionClient) doRequest(method string, args interface{}...
    method AddMagnet (line 160) | func (c *TransmissionClient) AddMagnet(magnetURL string, opts *AddOpti...
    method AddTorrentURL (line 182) | func (c *TransmissionClient) AddTorrentURL(url string, opts *AddOption...
    method AddTorrentFile (line 200) | func (c *TransmissionClient) AddTorrentFile(path string, opts *AddOpti...
    method addTorrent (line 222) | func (c *TransmissionClient) addTorrent(args map[string]interface{}) (...
    method GetTorrent (line 265) | func (c *TransmissionClient) GetTorrent(id string) (*TorrentInfo, erro...
    method ListTorrents (line 292) | func (c *TransmissionClient) ListTorrents() ([]TorrentInfo, error) {
    method convertTorrent (line 343) | func (c *TransmissionClient) convertTorrent(t *trTorrent) *TorrentInfo {
    method convertStatus (line 367) | func (c *TransmissionClient) convertStatus(status int) TorrentState {
    method RemoveTorrent (line 385) | func (c *TransmissionClient) RemoveTorrent(id string, deleteData bool)...
    method PauseTorrent (line 396) | func (c *TransmissionClient) PauseTorrent(id string) error {
    method ResumeTorrent (line 406) | func (c *TransmissionClient) ResumeTorrent(id string) error {
  type trRequest (line 24) | type trRequest struct
  type trResponse (line 30) | type trResponse struct
  constant trStatusStopped (line 38) | trStatusStopped      = 0
  constant trStatusQueuedVerify (line 39) | trStatusQueuedVerify = 1
  constant trStatusVerifying (line 40) | trStatusVerifying    = 2
  constant trStatusQueuedDown (line 41) | trStatusQueuedDown   = 3
  constant trStatusDownloading (line 42) | trStatusDownloading  = 4
  constant trStatusQueuedSeed (line 43) | trStatusQueuedSeed   = 5
  constant trStatusSeeding (line 44) | trStatusSeeding      = 6
  function NewTransmissionClient (line 48) | func NewTransmissionClient(cfg *Config) *TransmissionClient {
  type trTorrentAdded (line 258) | type trTorrentAdded struct
  type trTorrent (line 325) | type trTorrent struct

FILE: internal/updater/updater.go
  constant repoOwner (line 13) | repoOwner = "guiyumin"
  constant repoName (line 14) | repoName  = "vget"
  function CheckUpdate (line 18) | func CheckUpdate() (*selfupdate.Release, bool, error) {
  function Update (line 54) | func Update() error {
  function GetPlatformAssetName (line 103) | func GetPlatformAssetName() string {

FILE: tauri/src-tauri/build.rs
  function main (line 1) | fn main() {

FILE: tauri/src-tauri/src/auth.rs
  type SiteAuthStatus (line 10) | pub struct SiteAuthStatus {
  type QRSession (line 17) | pub struct QRSession {
  type QRPollResult (line 23) | pub struct QRPollResult {
  function config_dir (line 31) | fn config_dir() -> PathBuf {
  function xhs_cookies_path (line 38) | fn xhs_cookies_path() -> PathBuf {
  function bilibili_check_status (line 45) | pub async fn bilibili_check_status() -> Result<SiteAuthStatus, String> {
  function fetch_bilibili_user_info (line 79) | async fn fetch_bilibili_user_info(cookie: &str) -> Result<(String, Optio...
  function bilibili_qr_generate (line 105) | pub async fn bilibili_qr_generate() -> Result<QRSession, String> {
  function bilibili_qr_poll (line 134) | pub async fn bilibili_qr_poll(qrcode_key: String) -> Result<QRPollResult...
  function extract_bilibili_cookies (line 210) | fn extract_bilibili_cookies(set_cookie_headers: &[String]) -> String {
  function bilibili_save_cookie (line 246) | pub async fn bilibili_save_cookie(cookie: String) -> Result<(), String> {
  function bilibili_logout (line 263) | pub async fn bilibili_logout() -> Result<(), String> {
  function xhs_check_status (line 276) | pub async fn xhs_check_status() -> Result<SiteAuthStatus, String> {
  function xhs_logout (line 313) | pub async fn xhs_logout() -> Result<(), String> {
  function xhs_open_login_window (line 332) | pub async fn xhs_open_login_window(app: tauri::AppHandle) -> Result<(), ...

FILE: tauri/src-tauri/src/config.rs
  type WebDAVServer (line 7) | pub struct WebDAVServer {
  type TwitterConfig (line 14) | pub struct TwitterConfig {
  type ServerConfig (line 20) | pub struct ServerConfig {
  function default_max_concurrent (line 25) | fn default_max_concurrent() -> u32 {
  type BilibiliConfig (line 30) | pub struct BilibiliConfig {
  type Kuaidi100Config (line 36) | pub struct Kuaidi100Config {
  type ExpressConfig (line 44) | pub struct ExpressConfig {
  type Config (line 50) | pub struct Config {
  function default_language (line 73) | fn default_language() -> String {
  function default_output_dir (line 77) | fn default_output_dir() -> String {
  function default_format (line 83) | fn default_format() -> String {
  function default_quality (line 87) | fn default_quality() -> String {
  function default_theme (line 91) | fn default_theme() -> String {
  method default (line 96) | fn default() -> Self {
  function config_dir (line 112) | fn config_dir() -> PathBuf {
  function config_path (line 121) | fn config_path() -> PathBuf {
  function get_config (line 125) | pub fn get_config() -> Result<Config, Box<dyn std::error::Error>> {
  function save_config (line 136) | pub fn save_config(config: &Config) -> Result<(), Box<dyn std::error::Er...

FILE: tauri/src-tauri/src/downloader/mod.rs
  type DownloadProgress (line 11) | pub struct DownloadProgress {
  type DownloadStatus (line 21) | pub enum DownloadStatus {
  type DownloadJob (line 30) | pub struct DownloadJob {
  type DownloadManager (line 40) | pub struct DownloadManager {
    method new (line 46) | pub fn new() -> Self {
    method add_job (line 53) | pub async fn add_job(&self, job: DownloadJob) -> tokio::sync::watch::R...
    method update_job (line 63) | pub async fn update_job(&self, job_id: &str, status: DownloadStatus, p...
    method cancel_job (line 71) | pub async fn cancel_job(&self, job_id: &str) -> Result<(), String> {
    method get_job (line 79) | pub async fn get_job(&self, job_id: &str) -> Option<DownloadJob> {
    method remove_job (line 84) | pub async fn remove_job(&self, job_id: &str) {
  method default (line 91) | fn default() -> Self {

FILE: tauri/src-tauri/src/downloader/simple.rs
  type SimpleDownloader (line 12) | pub struct SimpleDownloader {
    method new (line 17) | pub fn new() -> Self {
    method download (line 26) | pub async fn download(
    method download_and_merge (line 141) | pub async fn download_and_merge(
    method download_file_with_progress (line 265) | async fn download_file_with_progress(
  method default (line 360) | fn default() -> Self {

FILE: tauri/src-tauri/src/extractor/bilibili.rs
  constant XOR_CODE (line 12) | const XOR_CODE: i64 = 23442827791579;
  constant MASK_CODE (line 13) | const MASK_CODE: i64 = (1 << 51) - 1;
  constant MAX_AID (line 14) | const MAX_AID: i64 = MASK_CODE + 1;
  constant BASE (line 15) | const BASE: i64 = 58;
  constant BV_LEN (line 16) | const BV_LEN: usize = 9;
  constant ALPHABET (line 18) | const ALPHABET: &[u8] = b"FcwAPNKTMug3GV5Lj7EJnHpWsx4tb8haYeviqBz6rkCy12...
  constant MIXIN_KEY_ENC_TAB (line 21) | const MIXIN_KEY_ENC_TAB: [usize; 32] = [
  function quality_map (line 27) | fn quality_map(id: i32) -> Option<&'static str> {
  function codec_name (line 44) | fn codec_name(codec_id: i32) -> &'static str {
  function rev_alphabet (line 64) | fn rev_alphabet() -> [i64; 256] {
  function bv_to_av (line 73) | pub fn bv_to_av(bvid: &str) -> Result<i64, String> {
  function av_to_bv (line 108) | pub fn av_to_bv(avid: i64) -> Result<String, String> {
  type BilibiliExtractor (line 134) | pub struct BilibiliExtractor {
    method new (line 141) | pub fn new() -> Self {
    method matches (line 158) | pub fn matches(url: &Url) -> bool {
    method extract (line 164) | pub async fn extract(url_str: &str) -> Result<MediaInfo, ExtractError> {
    method do_extract (line 169) | async fn do_extract(&mut self, url_str: &str) -> Result<MediaInfo, Ext...
    method resolve_video_id (line 209) | async fn resolve_video_id(&self, url_str: &str) -> Result<(i64, String...
    method resolve_short_url (line 242) | async fn resolve_short_url(&self, short_url: &str) -> Result<String, E...
    method fetch_wbi_keys (line 259) | async fn fetch_wbi_keys(&mut self) -> Result<(), ExtractError> {
    method fetch_video_info (line 281) | async fn fetch_video_info(&self, aid: i64) -> Result<VideoInfo, Extrac...
    method fetch_play_url (line 306) | async fn fetch_play_url(&self, aid: i64, cid: i64) -> Result<DashInfo,...
    method wbi_sign (line 344) | fn wbi_sign(&self, params: &mut BTreeMap<&str, String>) -> String {
    method build_formats (line 376) | fn build_formats(&self, streams: &DashInfo) -> Vec<Format> {
    method build_headers (line 431) | fn build_headers(&self) -> HeaderMap {
  function user_agent (line 455) | fn user_agent() -> &'static str {
  function extract_key_from_url (line 459) | fn extract_key_from_url(url: &str) -> String {
  function get_mixin_key (line 468) | fn get_mixin_key(orig: &str) -> String {
  function filter_wbi_value (line 477) | fn filter_wbi_value(s: &str) -> String {
  function build_query (line 484) | fn build_query(params: &BTreeMap<&str, String>) -> String {
  type NavResponse (line 495) | struct NavResponse {
  type NavData (line 500) | struct NavData {
  type WbiImg (line 505) | struct WbiImg {
  type VideoInfoResponse (line 511) | struct VideoInfoResponse {
  type VideoInfo (line 518) | struct VideoInfo {
  type Owner (line 528) | struct Owner {
  type Page (line 533) | struct Page {
  type PlayUrlResponse (line 538) | struct PlayUrlResponse {
  type PlayUrlData (line 545) | struct PlayUrlData {
  type DashInfo (line 550) | struct DashInfo {
  type VideoStream (line 556) | struct VideoStream {
  type AudioStream (line 568) | struct AudioStream {

FILE: tauri/src-tauri/src/extractor/direct.rs
  constant VIDEO_EXTENSIONS (line 5) | const VIDEO_EXTENSIONS: &[&str] = &["mp4", "mkv", "webm", "avi", "mov", ...
  constant AUDIO_EXTENSIONS (line 6) | const AUDIO_EXTENSIONS: &[&str] = &["mp3", "m4a", "aac", "flac", "wav", ...
  constant IMAGE_EXTENSIONS (line 7) | const IMAGE_EXTENSIONS: &[&str] = &["jpg", "jpeg", "png", "gif", "webp",...
  type DirectExtractor (line 9) | pub struct DirectExtractor;
    method matches (line 13) | pub fn matches(url: &Url) -> bool {
    method extract (line 21) | pub async fn extract(url: &str) -> Result<MediaInfo, ExtractError> {

FILE: tauri/src-tauri/src/extractor/mod.rs
  function extract_media (line 12) | pub async fn extract_media(url_str: &str) -> Result<MediaInfo, ExtractEr...

FILE: tauri/src-tauri/src/extractor/twitter.rs
  constant BEARER_TOKEN (line 9) | const BEARER_TOKEN: &str = "AAAAAAAAAAAAAAAAAAAAANRILgAAAAAAnNwIzUejRCOu...
  constant GUEST_TOKEN_URL (line 10) | const GUEST_TOKEN_URL: &str = "https://api.x.com/1.1/guest/activate.json";
  constant GRAPHQL_URL (line 11) | const GRAPHQL_URL: &str = "https://x.com/i/api/graphql/2ICDjqPd81tulZcYr...
  constant SYNDICATION_URL (line 12) | const SYNDICATION_URL: &str = "https://cdn.syndication.twimg.com/tweet-r...
  type TwitterExtractor (line 20) | pub struct TwitterExtractor {
    method new (line 27) | pub fn new(auth_token: Option<String>) -> Self {
    method matches (line 39) | pub fn matches(url: &Url) -> bool {
    method extract (line 48) | pub async fn extract(url_str: &str, auth_token: Option<String>) -> Res...
    method do_extract (line 53) | async fn do_extract(&mut self, url_str: &str) -> Result<MediaInfo, Ext...
    method fetch_guest_token (line 75) | async fn fetch_guest_token(&self) -> Result<String, ExtractError> {
    method fetch_from_syndication (line 99) | async fn fetch_from_syndication(&self, tweet_id: &str) -> Result<Media...
    method fetch_from_graphql (line 121) | async fn fetch_from_graphql(
    method fetch_csrf_token (line 155) | async fn fetch_csrf_token(&mut self) -> Result<(), ExtractError> {
    method fetch_from_graphql_auth (line 177) | async fn fetch_from_graphql_auth(&mut self, tweet_id: &str) -> Result<...
    method parse_syndication_response (line 220) | fn parse_syndication_response(
    method parse_graphql_response (line 336) | fn parse_graphql_response(
  function build_graphql_params (line 469) | fn build_graphql_params(tweet_id: &str) -> (String, String) {
  function truncate_text (line 502) | fn truncate_text(s: &str, max_len: usize) -> String {
  function extract_resolution (line 512) | fn extract_resolution(url: &str) -> (u32, u32) {
  function estimate_quality (line 522) | fn estimate_quality(bitrate: Option<i64>) -> Option<String> {
  function get_high_quality_image_url (line 536) | fn get_high_quality_image_url(image_url: &str) -> String {
  function get_image_extension (line 548) | fn get_image_extension(image_url: &str) -> String {
  type SyndicationResponse (line 564) | struct SyndicationResponse {
  type SyndicationUser (line 574) | struct SyndicationUser {
  type SyndicationMedia (line 579) | struct SyndicationMedia {
  type SyndicationVideoInfo (line 591) | struct SyndicationVideoInfo {
  type SyndicationVariant (line 596) | struct SyndicationVariant {
  type SyndicationVideo (line 604) | struct SyndicationVideo {
  type SyndicationVideoVariant (line 609) | struct SyndicationVideoVariant {
  type GraphQLResponse (line 616) | struct GraphQLResponse {
  type GraphQLData (line 621) | struct GraphQLData {
  type GraphQLTweetResult (line 627) | struct GraphQLTweetResult {
  type GraphQLResult (line 632) | struct GraphQLResult {
  type GraphQLCore (line 642) | struct GraphQLCore {
  type GraphQLUserResults (line 647) | struct GraphQLUserResults {
  type GraphQLUser (line 652) | struct GraphQLUser {
  type GraphQLUserLegacy (line 657) | struct GraphQLUserLegacy {
  type GraphQLLegacy (line 662) | struct GraphQLLegacy {
  type GraphQLExtendedEntities (line 668) | struct GraphQLExtendedEntities {
  type GraphQLMedia (line 673) | struct GraphQLMedia {
  type GraphQLOriginalInfo (line 682) | struct GraphQLOriginalInfo {
  type GraphQLVideoInfo (line 688) | struct GraphQLVideoInfo {
  type GraphQLVariant (line 695) | struct GraphQLVariant {

FILE: tauri/src-tauri/src/extractor/types.rs
  type ExtractError (line 6) | pub enum ExtractError {
  type MediaType (line 23) | pub enum MediaType {
  type Format (line 30) | pub struct Format {
  type MediaInfo (line 49) | pub struct MediaInfo {

FILE: tauri/src-tauri/src/ffmpeg.rs
  function parse_time_to_secs (line 8) | fn parse_time_to_secs(time_str: &str) -> Option<f32> {
  function merge_video_audio (line 23) | pub async fn merge_video_audio(
  type MediaInfoResult (line 112) | pub struct MediaInfoResult {
  type StreamInfo (line 123) | pub struct StreamInfo {
  function get_media_info (line 137) | pub async fn get_media_info(input_path: &str) -> Result<MediaInfoResult,...
  function convert_video_sync (line 204) | pub fn convert_video_sync(
  function compress_video_sync (line 257) | pub fn compress_video_sync(
  function trim_video_sync (line 312) | pub fn trim_video_sync(
  function extract_audio_sync (line 365) | pub fn extract_audio_sync(
  function extract_frames_sync (line 437) | pub fn extract_frames_sync(
  function convert_audio_sync (line 501) | pub fn convert_audio_sync(

FILE: tauri/src-tauri/src/lib.rs
  function get_config (line 23) | async fn get_config() -> Result<Config, String> {
  function save_config (line 32) | async fn save_config(config: Config) -> Result<(), String> {
  function extract_media (line 43) | async fn extract_media(url: String) -> Result<MediaInfo, String> {
  function open_output_folder (line 50) | async fn open_output_folder(path: String) -> Result<(), String> {
  function start_download (line 92) | async fn start_download(
  function cancel_download (line 176) | async fn cancel_download(
  function get_download_status (line 184) | async fn get_download_status(
  function ffmpeg_get_media_info (line 194) | async fn ffmpeg_get_media_info(input_path: String) -> Result<MediaInfoRe...
  function ffmpeg_convert_video (line 199) | async fn ffmpeg_convert_video(
  function ffmpeg_compress_video (line 263) | async fn ffmpeg_compress_video(
  function ffmpeg_trim_video (line 328) | async fn ffmpeg_trim_video(
  function ffmpeg_extract_audio (line 396) | async fn ffmpeg_extract_audio(
  function ffmpeg_extract_frames (line 462) | async fn ffmpeg_extract_frames(
  function ffmpeg_convert_audio (line 528) | async fn ffmpeg_convert_audio(
  function pdf_get_info (line 598) | async fn pdf_get_info(input_path: String) -> Result<pdf::PdfInfo, String> {
  function pdf_merge (line 605) | async fn pdf_merge(input_paths: Vec<String>, output_path: String) -> Res...
  function pdf_images_to_pdf (line 612) | async fn pdf_images_to_pdf(image_paths: Vec<String>, output_path: String...
  function pdf_delete_pages (line 619) | async fn pdf_delete_pages(
  function pdf_remove_watermark (line 632) | async fn pdf_remove_watermark(
  function pdf_print (line 642) | async fn pdf_print(input_path: String) -> Result<(), String> {
  function pdf_open_external (line 649) | async fn pdf_open_external(input_path: String) -> Result<(), String> {
  function read_text_file (line 658) | async fn read_text_file(path: String) -> Result<String, String> {
  function md_to_pdf (line 669) | async fn md_to_pdf(
  function run (line 685) | pub fn run() {

FILE: tauri/src-tauri/src/main.rs
  function main (line 4) | fn main() {

FILE: tauri/src-tauri/src/md2pdf.rs
  function convert_md_to_pdf (line 20) | pub fn convert_md_to_pdf(
  function markdown_to_html (line 43) | fn markdown_to_html(markdown: &str, theme: &str) -> String {
  function html_escape (line 250) | fn html_escape(text: &str) -> String {
  function heading_level_to_u8 (line 259) | fn heading_level_to_u8(level: HeadingLevel) -> u8 {
  function generate_font_css (line 271) | fn generate_font_css() -> String {
  function generate_styled_html (line 336) | fn generate_styled_html(content: &str, theme: &str) -> String {
  function get_theme_css (line 360) | fn get_theme_css(theme: &str) -> &'static str {
  function html_to_pdf (line 368) | fn html_to_pdf(html: &str, output_path: &str, page_size: &str) -> Result...
  constant LIGHT_THEME_CSS (line 442) | const LIGHT_THEME_CSS: &str = r#"
  constant DARK_THEME_CSS (line 789) | const DARK_THEME_CSS: &str = r#"

FILE: tauri/src-tauri/src/pdf.rs
  type PdfInfo (line 6) | pub struct PdfInfo {
  function get_pdf_info (line 14) | pub fn get_pdf_info(path: &str) -> Result<PdfInfo, String> {
  function merge_pdfs (line 51) | pub fn merge_pdfs(input_paths: &[String], output_path: &str) -> Result<(...
  function update_references (line 132) | fn update_references(obj: &mut Object, id_map: &HashMap<ObjectId, Object...
  function images_to_pdf (line 159) | pub fn images_to_pdf(image_paths: &[String], output_path: &str) -> Resul...
  type WatermarkRemovalResult (line 233) | pub struct WatermarkRemovalResult {
  function remove_watermark (line 241) | pub fn remove_watermark(input_path: &str, output_path: &str) -> Result<W...
  function delete_pages (line 413) | pub fn delete_pages(input_path: &str, output_path: &str, pages_to_delete...
  function print_pdf (line 450) | pub fn print_pdf(path: &str) -> Result<(), String> {
  function open_pdf_external (line 479) | pub fn open_pdf_external(path: &str) -> Result<(), String> {
  function test_get_pdf_info (line 512) | fn test_get_pdf_info() {

FILE: tauri/src/components/AppSidebar.tsx
  type NavItem (line 15) | interface NavItem {
  type AppSidebarProps (line 21) | interface AppSidebarProps {
  function AppSidebar (line 26) | function AppSidebar({ collapsed, onToggle }: AppSidebarProps) {

FILE: tauri/src/components/home/DownloadItem.tsx
  type DownloadItemProps (line 9) | interface DownloadItemProps {
  function DownloadItem (line 13) | function DownloadItem({ download }: DownloadItemProps) {

FILE: tauri/src/components/home/HomePage.tsx
  function HomePage (line 24) | function HomePage() {

FILE: tauri/src/components/home/types.ts
  type MediaInfo (line 1) | interface MediaInfo {
  type Config (line 19) | interface Config {
  function formatBytes (line 23) | function formatBytes(bytes: number): string {
  function formatSpeed (line 31) | function formatSpeed(bytesPerSecond: number): string {

FILE: tauri/src/components/icons/PdfIcon.tsx
  function PdfIcon (line 3) | function PdfIcon(props: SVGProps<SVGSVGElement>) {

FILE: tauri/src/components/media-tools/MediaToolsPage.tsx
  type Tool (line 26) | interface Tool {
  function MediaToolsPage (line 72) | function MediaToolsPage() {

FILE: tauri/src/components/media-tools/panels/AudioConvertPanel.tsx
  constant AUDIO_EXTENSIONS (line 18) | const AUDIO_EXTENSIONS = ["mp3", "aac", "flac", "wav", "ogg", "m4a"];
  function AudioConvertPanel (line 20) | function AudioConvertPanel({

FILE: tauri/src/components/media-tools/panels/CompressPanel.tsx
  constant VIDEO_EXTENSIONS (line 12) | const VIDEO_EXTENSIONS = ["mp4", "mkv", "webm", "mov", "avi"];
  function CompressPanel (line 14) | function CompressPanel({

FILE: tauri/src/components/media-tools/panels/ConvertPanel.tsx
  constant VIDEO_EXTENSIONS (line 18) | const VIDEO_EXTENSIONS = ["mp4", "mkv", "webm", "mov", "avi"];
  function ConvertPanel (line 20) | function ConvertPanel({

FILE: tauri/src/components/media-tools/panels/ExtractAudioPanel.tsx
  constant VIDEO_EXTENSIONS (line 18) | const VIDEO_EXTENSIONS = ["mp4", "mkv", "webm", "mov", "avi"];
  function ExtractAudioPanel (line 20) | function ExtractAudioPanel({

FILE: tauri/src/components/media-tools/panels/ExtractFramesPanel.tsx
  constant VIDEO_EXTENSIONS (line 12) | const VIDEO_EXTENSIONS = ["mp4", "mkv", "webm", "mov", "avi"];
  function ExtractFramesPanel (line 14) | function ExtractFramesPanel({

FILE: tauri/src/components/media-tools/panels/TrimPanel.tsx
  constant VIDEO_EXTENSIONS (line 12) | const VIDEO_EXTENSIONS = ["mp4", "mkv", "webm", "mov", "avi"];
  function TrimPanel (line 14) | function TrimPanel({

FILE: tauri/src/components/media-tools/types.ts
  type MediaInfo (line 1) | interface MediaInfo {
  type StreamInfo (line 11) | interface StreamInfo {
  type Config (line 24) | interface Config {
  type ToolId (line 28) | type ToolId =
  type PanelProps (line 36) | interface PanelProps {
  function formatBytes (line 49) | function formatBytes(bytes: number): string {
  function formatDuration (line 57) | function formatDuration(seconds: number): string {
  function getBasename (line 65) | function getBasename(filePath: string): string {
  function generateOutputPath (line 71) | function generateOutputPath(outputDir: string, inputFile: string, ext: s...

FILE: tauri/src/components/pdf-tools/PDFToolsPage.tsx
  type Tool (line 9) | interface Tool {
  function PDFToolsPage (line 49) | function PDFToolsPage() {

FILE: tauri/src/components/pdf-tools/panels/DeletePagesPanel.tsx
  constant PDF_EXTENSIONS (line 12) | const PDF_EXTENSIONS = ["pdf"];
  function DeletePagesPanel (line 14) | function DeletePagesPanel({ outputDir, loading, setLoading }: PdfPanelPr...

FILE: tauri/src/components/pdf-tools/panels/ImagesToPdfPanel.tsx
  constant IMAGE_EXTENSIONS (line 12) | const IMAGE_EXTENSIONS = ["png", "jpg", "jpeg", "gif", "bmp", "webp"];
  function ImagesToPdfPanel (line 14) | function ImagesToPdfPanel({ outputDir, loading, setLoading }: PdfPanelPr...

FILE: tauri/src/components/pdf-tools/panels/Md2PdfPanel.tsx
  function Md2PdfPanel (line 21) | function Md2PdfPanel({ outputDir, loading, setLoading }: PdfPanelProps) {

FILE: tauri/src/components/pdf-tools/panels/MergePdfPanel.tsx
  constant PDF_EXTENSIONS (line 12) | const PDF_EXTENSIONS = ["pdf"];
  function MergePdfPanel (line 14) | function MergePdfPanel({ outputDir, loading, setLoading }: PdfPanelProps) {

FILE: tauri/src/components/pdf-tools/panels/RemoveWatermarkPanel.tsx
  constant PDF_EXTENSIONS (line 12) | const PDF_EXTENSIONS = ["pdf"];
  function RemoveWatermarkPanel (line 14) | function RemoveWatermarkPanel({ outputDir, loading, setLoading }: PdfPan...

FILE: tauri/src/components/pdf-tools/types.ts
  type PdfInfo (line 1) | interface PdfInfo {
  type WatermarkRemovalResult (line 8) | interface WatermarkRemovalResult {
  type Config (line 14) | interface Config {
  type PdfToolId (line 18) | type PdfToolId = "merge" | "images-to-pdf" | "delete-pages" | "remove-wa...
  type PdfPanelProps (line 20) | interface PdfPanelProps {
  function getBasename (line 26) | function getBasename(filePath: string): string {
  function generateOutputPath (line 32) | function generateOutputPath(

FILE: tauri/src/components/settings/AboutSettings.tsx
  function AboutSettings (line 16) | function AboutSettings() {

FILE: tauri/src/components/settings/GeneralSettings.tsx
  type GeneralSettingsProps (line 28) | interface GeneralSettingsProps {
  function GeneralSettings (line 33) | function GeneralSettings({ config, onUpdate }: GeneralSettingsProps) {

FILE: tauri/src/components/settings/SettingsPage.tsx
  type SettingsSection (line 13) | type SettingsSection = "general" | "sites" | "about";
  function SettingsPage (line 21) | function SettingsPage() {

FILE: tauri/src/components/settings/SiteSettings.tsx
  type SiteSettingsProps (line 47) | interface SiteSettingsProps {
  type CookieFields (line 52) | interface CookieFields {
  function buildCookie (line 58) | function buildCookie(fields: CookieFields): string {
  function SiteSettings (line 66) | function SiteSettings({ config, onUpdate }: SiteSettingsProps) {
  function BilibiliQRLogin (line 239) | function BilibiliQRLogin({
  function BilibiliCookieLogin (line 379) | function BilibiliCookieLogin({
  function XiaohongshuLogin (line 473) | function XiaohongshuLogin() {
  function DockerServerSettings (line 527) | function DockerServerSettings() {

FILE: tauri/src/components/settings/types.ts
  type WebDAVServer (line 1) | interface WebDAVServer {
  type TwitterConfig (line 7) | interface TwitterConfig {
  type ServerConfig (line 11) | interface ServerConfig {
  type BilibiliConfig (line 15) | interface BilibiliConfig {
  type Kuaidi100Config (line 19) | interface Kuaidi100Config {
  type ExpressConfig (line 24) | interface ExpressConfig {
  type Config (line 28) | interface Config {

FILE: tauri/src/components/ui/accordion.tsx
  function Accordion (line 7) | function Accordion({
  function AccordionItem (line 13) | function AccordionItem({
  function AccordionTrigger (line 26) | function AccordionTrigger({
  function AccordionContent (line 48) | function AccordionContent({

FILE: tauri/src/components/ui/alert-dialog.tsx
  function AlertDialog (line 7) | function AlertDialog({
  function AlertDialogTrigger (line 13) | function AlertDialogTrigger({
  function AlertDialogPortal (line 21) | function AlertDialogPortal({
  function AlertDialogOverlay (line 29) | function AlertDialogOverlay({
  function AlertDialogContent (line 45) | function AlertDialogContent({
  function AlertDialogHeader (line 64) | function AlertDialogHeader({
  function AlertDialogFooter (line 77) | function AlertDialogFooter({
  function AlertDialogTitle (line 93) | function AlertDialogTitle({
  function AlertDialogDescription (line 106) | function AlertDialogDescription({
  function AlertDialogAction (line 119) | function AlertDialogAction({
  function AlertDialogCancel (line 131) | function AlertDialogCancel({

FILE: tauri/src/components/ui/alert.tsx
  function Alert (line 22) | function Alert({
  function AlertTitle (line 37) | function AlertTitle({ className, ...props }: React.ComponentProps<"div">) {
  function AlertDescription (line 50) | function AlertDescription({

FILE: tauri/src/components/ui/aspect-ratio.tsx
  function AspectRatio (line 5) | function AspectRatio({

FILE: tauri/src/components/ui/avatar.tsx
  function Avatar (line 6) | function Avatar({
  function AvatarImage (line 22) | function AvatarImage({
  function AvatarFallback (line 35) | function AvatarFallback({

FILE: tauri/src/components/ui/badge.tsx
  function Badge (line 28) | function Badge({

FILE: tauri/src/components/ui/breadcrumb.tsx
  function Breadcrumb (line 7) | function Breadcrumb({ ...props }: React.ComponentProps<"nav">) {
  function BreadcrumbList (line 11) | function BreadcrumbList({ className, ...props }: React.ComponentProps<"o...
  function BreadcrumbItem (line 24) | function BreadcrumbItem({ className, ...props }: React.ComponentProps<"l...
  function BreadcrumbLink (line 34) | function BreadcrumbLink({
  function BreadcrumbPage (line 52) | function BreadcrumbPage({ className, ...props }: React.ComponentProps<"s...
  function BreadcrumbSeparator (line 65) | function BreadcrumbSeparator({
  function BreadcrumbEllipsis (line 83) | function BreadcrumbEllipsis({

FILE: tauri/src/components/ui/button-group.tsx
  function ButtonGroup (line 24) | function ButtonGroup({
  function ButtonGroupText (line 40) | function ButtonGroupText({
  function ButtonGroupSeparator (line 60) | function ButtonGroupSeparator({

FILE: tauri/src/components/ui/button.tsx
  function Button (line 39) | function Button({

FILE: tauri/src/components/ui/calendar.tsx
  function Calendar (line 18) | function Calendar({
  function CalendarDayButton (line 182) | function CalendarDayButton({

FILE: tauri/src/components/ui/card.tsx
  function Card (line 5) | function Card({ className, ...props }: React.ComponentProps<"div">) {
  function CardHeader (line 18) | function CardHeader({ className, ...props }: React.ComponentProps<"div">) {
  function CardTitle (line 31) | function CardTitle({ className, ...props }: React.ComponentProps<"div">) {
  function CardDescription (line 41) | function CardDescription({ className, ...props }: React.ComponentProps<"...
  function CardAction (line 51) | function CardAction({ className, ...props }: React.ComponentProps<"div">) {
  function CardContent (line 64) | function CardContent({ className, ...props }: React.ComponentProps<"div"...
  function CardFooter (line 74) | function CardFooter({ className, ...props }: React.ComponentProps<"div">) {

FILE: tauri/src/components/ui/carousel.tsx
  type CarouselApi (line 10) | type CarouselApi = UseEmblaCarouselType[1]
  type UseCarouselParameters (line 11) | type UseCarouselParameters = Parameters<typeof useEmblaCarousel>
  type CarouselOptions (line 12) | type CarouselOptions = UseCarouselParameters[0]
  type CarouselPlugin (line 13) | type CarouselPlugin = UseCarouselParameters[1]
  type CarouselProps (line 15) | type CarouselProps = {
  type CarouselContextProps (line 22) | type CarouselContextProps = {
  function useCarousel (line 33) | function useCarousel() {
  function Carousel (line 43) | function Carousel({
  function CarouselContent (line 133) | function CarouselContent({ className, ...props }: React.ComponentProps<"...
  function CarouselItem (line 154) | function CarouselItem({ className, ...props }: React.ComponentProps<"div...
  function CarouselPrevious (line 172) | function CarouselPrevious({
  function CarouselNext (line 202) | function CarouselNext({

FILE: tauri/src/components/ui/chart.tsx
  constant THEMES (line 9) | const THEMES = { light: "", dark: ".dark" } as const
  type ChartConfig (line 11) | type ChartConfig = {
  type ChartContextProps (line 21) | type ChartContextProps = {
  function useChart (line 27) | function useChart() {
  function ChartContainer (line 37) | function ChartContainer({
  function ChartTooltipContent (line 107) | function ChartTooltipContent({
  function ChartLegendContent (line 255) | function ChartLegendContent({
  function getPayloadConfigFromPayload (line 312) | function getPayloadConfigFromPayload(

FILE: tauri/src/components/ui/checkbox.tsx
  function Checkbox (line 9) | function Checkbox({

FILE: tauri/src/components/ui/collapsible.tsx
  function Collapsible (line 3) | function Collapsible({
  function CollapsibleTrigger (line 9) | function CollapsibleTrigger({
  function CollapsibleContent (line 20) | function CollapsibleContent({

FILE: tauri/src/components/ui/command.tsx
  function Command (line 14) | function Command({
  function CommandDialog (line 30) | function CommandDialog({
  function CommandInput (line 61) | function CommandInput({
  function CommandList (line 83) | function CommandList({
  function CommandEmpty (line 99) | function CommandEmpty({
  function CommandGroup (line 111) | function CommandGroup({
  function CommandSeparator (line 127) | function CommandSeparator({
  function CommandItem (line 140) | function CommandItem({
  function CommandShortcut (line 156) | function CommandShortcut({

FILE: tauri/src/components/ui/context-menu.tsx
  function ContextMenu (line 9) | function ContextMenu({
  function ContextMenuTrigger (line 15) | function ContextMenuTrigger({
  function ContextMenuGroup (line 23) | function ContextMenuGroup({
  function ContextMenuPortal (line 31) | function ContextMenuPortal({
  function ContextMenuSub (line 39) | function ContextMenuSub({
  function ContextMenuRadioGroup (line 45) | function ContextMenuRadioGroup({
  function ContextMenuSubTrigger (line 56) | function ContextMenuSubTrigger({
  function ContextMenuSubContent (line 80) | function ContextMenuSubContent({
  function ContextMenuContent (line 96) | function ContextMenuContent({
  function ContextMenuItem (line 114) | function ContextMenuItem({
  function ContextMenuCheckboxItem (line 137) | function ContextMenuCheckboxItem({
  function ContextMenuRadioItem (line 163) | function ContextMenuRadioItem({
  function ContextMenuLabel (line 187) | function ContextMenuLabel({
  function ContextMenuSeparator (line 207) | function ContextMenuSeparator({
  function ContextMenuShortcut (line 220) | function ContextMenuShortcut({

FILE: tauri/src/components/ui/dialog.tsx
  function Dialog (line 7) | function Dialog({
  function DialogTrigger (line 13) | function DialogTrigger({
  function DialogPortal (line 19) | function DialogPortal({
  function DialogClose (line 25) | function DialogClose({
  function DialogOverlay (line 31) | function DialogOverlay({
  function DialogContent (line 47) | function DialogContent({
  function DialogHeader (line 81) | function DialogHeader({ className, ...props }: React.ComponentProps<"div...
  function DialogFooter (line 91) | function DialogFooter({ className, ...props }: React.ComponentProps<"div...
  function DialogTitle (line 104) | function DialogTitle({
  function DialogDescription (line 117) | function DialogDescription({

FILE: tauri/src/components/ui/drawer.tsx
  function Drawer (line 8) | function Drawer({
  function DrawerTrigger (line 14) | function DrawerTrigger({
  function DrawerPortal (line 20) | function DrawerPortal({
  function DrawerClose (line 26) | function DrawerClose({
  function DrawerOverlay (line 32) | function DrawerOverlay({
  function DrawerContent (line 48) | function DrawerContent({
  function DrawerHeader (line 75) | function DrawerHeader({ className, ...props }: React.ComponentProps<"div...
  function DrawerFooter (line 88) | function DrawerFooter({ className, ...props }: React.ComponentProps<"div...
  function DrawerTitle (line 98) | function DrawerTitle({
  function DrawerDescription (line 111) | function DrawerDescription({

FILE: tauri/src/components/ui/dropdown-menu.tsx
  function DropdownMenu (line 7) | function DropdownMenu({
  function DropdownMenuPortal (line 13) | function DropdownMenuPortal({
  function DropdownMenuTrigger (line 21) | function DropdownMenuTrigger({
  function DropdownMenuContent (line 32) | function DropdownMenuContent({
  function DropdownMenuGroup (line 52) | function DropdownMenuGroup({
  function DropdownMenuItem (line 60) | function DropdownMenuItem({
  function DropdownMenuCheckboxItem (line 83) | function DropdownMenuCheckboxItem({
  function DropdownMenuRadioGroup (line 109) | function DropdownMenuRadioGroup({
  function DropdownMenuRadioItem (line 120) | function DropdownMenuRadioItem({
  function DropdownMenuLabel (line 144) | function DropdownMenuLabel({
  function DropdownMenuSeparator (line 164) | function DropdownMenuSeparator({
  function DropdownMenuShortcut (line 177) | function DropdownMenuShortcut({
  function DropdownMenuSub (line 193) | function DropdownMenuSub({
  function DropdownMenuSubTrigger (line 199) | function DropdownMenuSubTrigger({
  function DropdownMenuSubContent (line 223) | function DropdownMenuSubContent({

FILE: tauri/src/components/ui/empty.tsx
  function Empty (line 5) | function Empty({ className, ...props }: React.ComponentProps<"div">) {
  function EmptyHeader (line 18) | function EmptyHeader({ className, ...props }: React.ComponentProps<"div"...
  function EmptyMedia (line 46) | function EmptyMedia({
  function EmptyTitle (line 61) | function EmptyTitle({ className, ...props }: React.ComponentProps<"div">) {
  function EmptyDescription (line 71) | function EmptyDescription({ className, ...props }: React.ComponentProps<...
  function EmptyContent (line 84) | function EmptyContent({ className, ...props }: React.ComponentProps<"div...

FILE: tauri/src/components/ui/field.tsx
  function FieldSet (line 8) | function FieldSet({ className, ...props }: React.ComponentProps<"fieldse...
  function FieldLegend (line 22) | function FieldLegend({
  function FieldGroup (line 42) | function FieldGroup({ className, ...props }: React.ComponentProps<"div">) {
  function Field (line 79) | function Field({
  function FieldContent (line 95) | function FieldContent({ className, ...props }: React.ComponentProps<"div...
  function FieldLabel (line 108) | function FieldLabel({
  function FieldTitle (line 126) | function FieldTitle({ className, ...props }: React.ComponentProps<"div">) {
  function FieldDescription (line 139) | function FieldDescription({ className, ...props }: React.ComponentProps<...
  function FieldSeparator (line 154) | function FieldSeparator({
  function FieldError (line 184) | function FieldError({

FILE: tauri/src/components/ui/file-drop-input.tsx
  type FileDropInputProps (line 7) | interface FileDropInputProps {
  function FileDropInput (line 30) | function FileDropInput({

FILE: tauri/src/components/ui/form.tsx
  type FormFieldContextValue (line 21) | type FormFieldContextValue<
  type FormItemContextValue (line 68) | type FormItemContextValue = {
  function FormItem (line 76) | function FormItem({ className, ...props }: React.ComponentProps<"div">) {
  function FormLabel (line 90) | function FormLabel({
  function FormControl (line 107) | function FormControl({ ...props }: React.ComponentProps<typeof Slot>) {
  function FormDescription (line 125) | function FormDescription({ className, ...props }: React.ComponentProps<"...
  function FormMessage (line 138) | function FormMessage({ className, ...props }: React.ComponentProps<"p">) {

FILE: tauri/src/components/ui/hover-card.tsx
  function HoverCard (line 8) | function HoverCard({
  function HoverCardTrigger (line 14) | function HoverCardTrigger({
  function HoverCardContent (line 22) | function HoverCardContent({

FILE: tauri/src/components/ui/input-group.tsx
  function InputGroup (line 11) | function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
  function InputGroupAddon (line 60) | function InputGroupAddon({
  function InputGroupButton (line 100) | function InputGroupButton({
  function InputGroupText (line 119) | function InputGroupText({ className, ...props }: React.ComponentProps<"s...
  function InputGroupInput (line 131) | function InputGroupInput({
  function InputGroupTextarea (line 147) | function InputGroupTextarea({

FILE: tauri/src/components/ui/input-otp.tsx
  function InputOTP (line 7) | function InputOTP({
  function InputOTPGroup (line 27) | function InputOTPGroup({ className, ...props }: React.ComponentProps<"di...
  function InputOTPSlot (line 37) | function InputOTPSlot({
  function InputOTPSeparator (line 67) | function InputOTPSeparator({ ...props }: React.ComponentProps<"div">) {

FILE: tauri/src/components/ui/input.tsx
  function Input (line 5) | function Input({ className, type, ...props }: React.ComponentProps<"inpu...

FILE: tauri/src/components/ui/item.tsx
  function ItemGroup (line 8) | function ItemGroup({ className, ...props }: React.ComponentProps<"div">) {
  function ItemSeparator (line 19) | function ItemSeparator({
  function Item (line 54) | function Item({
  function ItemMedia (line 91) | function ItemMedia({
  function ItemContent (line 106) | function ItemContent({ className, ...props }: React.ComponentProps<"div"...
  function ItemTitle (line 119) | function ItemTitle({ className, ...props }: React.ComponentProps<"div">) {
  function ItemDescription (line 132) | function ItemDescription({ className, ...props }: React.ComponentProps<"...
  function ItemActions (line 146) | function ItemActions({ className, ...props }: React.ComponentProps<"div"...
  function ItemHeader (line 156) | function ItemHeader({ className, ...props }: React.ComponentProps<"div">) {
  function ItemFooter (line 169) | function ItemFooter({ className, ...props }: React.ComponentProps<"div">) {

FILE: tauri/src/components/ui/kbd.tsx
  function Kbd (line 3) | function Kbd({ className, ...props }: React.ComponentProps<"kbd">) {
  function KbdGroup (line 18) | function KbdGroup({ className, ...props }: React.ComponentProps<"div">) {

FILE: tauri/src/components/ui/label.tsx
  function Label (line 8) | function Label({

FILE: tauri/src/components/ui/menubar.tsx
  function Menubar (line 7) | function Menubar({
  function MenubarMenu (line 23) | function MenubarMenu({
  function MenubarGroup (line 29) | function MenubarGroup({
  function MenubarPortal (line 35) | function MenubarPortal({
  function MenubarRadioGroup (line 41) | function MenubarRadioGroup({
  function MenubarTrigger (line 49) | function MenubarTrigger({
  function MenubarContent (line 65) | function MenubarContent({
  function MenubarItem (line 89) | function MenubarItem({
  function MenubarCheckboxItem (line 112) | function MenubarCheckboxItem({
  function MenubarRadioItem (line 138) | function MenubarRadioItem({
  function MenubarLabel (line 162) | function MenubarLabel({
  function MenubarSeparator (line 182) | function MenubarSeparator({
  function MenubarShortcut (line 195) | function MenubarShortcut({
  function MenubarSub (line 211) | function MenubarSub({
  function MenubarSubTrigger (line 217) | function MenubarSubTrigger({
  function MenubarSubContent (line 241) | function MenubarSubContent({

FILE: tauri/src/components/ui/navigation-menu.tsx
  function NavigationMenu (line 8) | function NavigationMenu({
  function NavigationMenuList (line 32) | function NavigationMenuList({
  function NavigationMenuItem (line 48) | function NavigationMenuItem({
  function NavigationMenuTrigger (line 65) | function NavigationMenuTrigger({
  function NavigationMenuContent (line 85) | function NavigationMenuContent({
  function NavigationMenuViewport (line 102) | function NavigationMenuViewport({
  function NavigationMenuLink (line 124) | function NavigationMenuLink({
  function NavigationMenuIndicator (line 140) | function NavigationMenuIndicator({

FILE: tauri/src/components/ui/pagination.tsx
  function Pagination (line 11) | function Pagination({ className, ...props }: React.ComponentProps<"nav">) {
  function PaginationContent (line 23) | function PaginationContent({
  function PaginationItem (line 36) | function PaginationItem({ ...props }: React.ComponentProps<"li">) {
  type PaginationLinkProps (line 40) | type PaginationLinkProps = {
  function PaginationLink (line 45) | function PaginationLink({
  function PaginationPrevious (line 68) | function PaginationPrevious({
  function PaginationNext (line 85) | function PaginationNext({
  function PaginationEllipsis (line 102) | function PaginationEllipsis({

FILE: tauri/src/components/ui/popover.tsx
  function Popover (line 8) | function Popover({
  function PopoverTrigger (line 14) | function PopoverTrigger({
  function PopoverContent (line 20) | function PopoverContent({
  function PopoverAnchor (line 42) | function PopoverAnchor({

FILE: tauri/src/components/ui/progress.tsx
  function Progress (line 6) | function Progress({

FILE: tauri/src/components/ui/radio-group.tsx
  function RadioGroup (line 9) | function RadioGroup({
  function RadioGroupItem (line 22) | function RadioGroupItem({

FILE: tauri/src/components/ui/resizable.tsx
  function ResizablePanelGroup (line 7) | function ResizablePanelGroup({
  function ResizablePanel (line 23) | function ResizablePanel({
  function ResizableHandle (line 29) | function ResizableHandle({

FILE: tauri/src/components/ui/scroll-area.tsx
  function ScrollArea (line 8) | function ScrollArea({
  function ScrollBar (line 31) | function ScrollBar({

FILE: tauri/src/components/ui/select.tsx
  function Select (line 7) | function Select({
  function SelectGroup (line 13) | function SelectGroup({
  function SelectValue (line 19) | function SelectValue({
  function SelectTrigger (line 25) | function SelectTrigger({
  function SelectContent (line 51) | function SelectContent({
  function SelectLabel (line 88) | function SelectLabel({
  function SelectItem (line 101) | function SelectItem({
  function SelectSeparator (line 128) | function SelectSeparator({
  function SelectScrollUpButton (line 141) | function SelectScrollUpButton({
  function SelectScrollDownButton (line 159) | function SelectScrollDownButton({

FILE: tauri/src/components/ui/separator.tsx
  function Separator (line 8) | function Separator({

FILE: tauri/src/components/ui/sheet.tsx
  function Sheet (line 7) | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive....
  function SheetTrigger (line 11) | function SheetTrigger({
  function SheetClose (line 17) | function SheetClose({
  function SheetPortal (line 23) | function SheetPortal({
  function SheetOverlay (line 29) | function SheetOverlay({
  function SheetContent (line 45) | function SheetContent({
  function SheetHeader (line 82) | function SheetHeader({ className, ...props }: React.ComponentProps<"div"...
  function SheetFooter (line 92) | function SheetFooter({ className, ...props }: React.ComponentProps<"div"...
  function SheetTitle (line 102) | function SheetTitle({
  function SheetDescription (line 115) | function SheetDescription({

FILE: tauri/src/components/ui/sidebar.tsx
  constant SIDEBAR_COOKIE_NAME (line 28) | const SIDEBAR_COOKIE_NAME = "sidebar_state"
  constant SIDEBAR_COOKIE_MAX_AGE (line 29) | const SIDEBAR_COOKIE_MAX_AGE = 60 * 60 * 24 * 7
  constant SIDEBAR_WIDTH (line 30) | const SIDEBAR_WIDTH = "16rem"
  constant SIDEBAR_WIDTH_MOBILE (line 31) | const SIDEBAR_WIDTH_MOBILE = "18rem"
  constant SIDEBAR_WIDTH_ICON (line 32) | const SIDEBAR_WIDTH_ICON = "3rem"
  constant SIDEBAR_KEYBOARD_SHORTCUT (line 33) | const SIDEBAR_KEYBOARD_SHORTCUT = "b"
  type SidebarContextProps (line 35) | type SidebarContextProps = {
  function useSidebar (line 47) | function useSidebar() {
  function SidebarProvider (line 56) | function SidebarProvider({
  function Sidebar (line 154) | function Sidebar({
  function SidebarTrigger (line 256) | function SidebarTrigger({
  function SidebarRail (line 282) | function SidebarRail({ className, ...props }: React.ComponentProps<"butt...
  function SidebarInset (line 307) | function SidebarInset({ className, ...props }: React.ComponentProps<"mai...
  function SidebarInput (line 321) | function SidebarInput({
  function SidebarHeader (line 335) | function SidebarHeader({ className, ...props }: React.ComponentProps<"di...
  function SidebarFooter (line 346) | function SidebarFooter({ className, ...props }: React.ComponentProps<"di...
  function SidebarSeparator (line 357) | function SidebarSeparator({
  function SidebarContent (line 371) | function SidebarContent({ className, ...props }: React.ComponentProps<"d...
  function SidebarGroup (line 385) | function SidebarGroup({ className, ...props }: React.ComponentProps<"div...
  function SidebarGroupLabel (line 396) | function SidebarGroupLabel({
  function SidebarGroupAction (line 417) | function SidebarGroupAction({
  function SidebarGroupContent (line 440) | function SidebarGroupContent({
  function SidebarMenu (line 454) | function SidebarMenu({ className, ...props }: React.ComponentProps<"ul">) {
  function SidebarMenuItem (line 465) | function SidebarMenuItem({ className, ...props }: React.ComponentProps<"...
  function SidebarMenuButton (line 498) | function SidebarMenuButton({
  function SidebarMenuAction (line 548) | function SidebarMenuAction({
  function SidebarMenuBadge (line 580) | function SidebarMenuBadge({
  function SidebarMenuSkeleton (line 602) | function SidebarMenuSkeleton({
  function SidebarMenuSub (line 640) | function SidebarMenuSub({ className, ...props }: React.ComponentProps<"u...
  function SidebarMenuSubItem (line 655) | function SidebarMenuSubItem({
  function SidebarMenuSubButton (line 669) | function SidebarMenuSubButton({

FILE: tauri/src/components/ui/skeleton.tsx
  function Skeleton (line 3) | function Skeleton({ className, ...props }: React.ComponentProps<"div">) {

FILE: tauri/src/components/ui/slider.tsx
  function Slider (line 8) | function Slider({

FILE: tauri/src/components/ui/spinner.tsx
  function Spinner (line 5) | function Spinner({ className, ...props }: React.ComponentProps<"svg">) {

FILE: tauri/src/components/ui/switch.tsx
  function Switch (line 8) | function Switch({

FILE: tauri/src/components/ui/table.tsx
  function Table (line 5) | function Table({ className, ...props }: React.ComponentProps<"table">) {
  function TableHeader (line 20) | function TableHeader({ className, ...props }: React.ComponentProps<"thea...
  function TableBody (line 30) | function TableBody({ className, ...props }: React.ComponentProps<"tbody"...
  function TableFooter (line 40) | function TableFooter({ className, ...props }: React.ComponentProps<"tfoo...
  function TableRow (line 53) | function TableRow({ className, ...props }: React.ComponentProps<"tr">) {
  function TableHead (line 66) | function TableHead({ className, ...props }: React.ComponentProps<"th">) {
  function TableCell (line 79) | function TableCell({ className, ...props }: React.ComponentProps<"td">) {
  function TableCaption (line 92) | function TableCaption({

FILE: tauri/src/components/ui/tabs.tsx
  function Tabs (line 8) | function Tabs({
  function TabsList (line 21) | function TabsList({
  function TabsTrigger (line 37) | function TabsTrigger({
  function TabsContent (line 53) | function TabsContent({

FILE: tauri/src/components/ui/textarea.tsx
  function Textarea (line 5) | function Textarea({ className, ...props }: React.ComponentProps<"textare...

FILE: tauri/src/components/ui/toggle-group.tsx
  function ToggleGroup (line 18) | function ToggleGroup({
  function ToggleGroupItem (line 49) | function ToggleGroupItem({

FILE: tauri/src/components/ui/toggle.tsx
  function Toggle (line 29) | function Toggle({

FILE: tauri/src/components/ui/tooltip.tsx
  function TooltipProvider (line 8) | function TooltipProvider({
  function Tooltip (line 21) | function Tooltip({
  function TooltipTrigger (line 31) | function TooltipTrigger({
  function TooltipContent (line 37) | function TooltipContent({

FILE: tauri/src/hooks/use-mobile.ts
  constant MOBILE_BREAKPOINT (line 3) | const MOBILE_BREAKPOINT = 768
  function useIsMobile (line 5) | function useIsMobile() {

FILE: tauri/src/hooks/useDropZone.ts
  type DragDropPayload (line 4) | interface DragDropPayload {
  type UseDropZoneOptions (line 9) | interface UseDropZoneOptions<T extends HTMLElement> {
  function isPointInElement (line 35) | function isPointInElement(x: number, y: number, element: HTMLElement): b...
  function useDropZone (line 44) | function useDropZone<T extends HTMLElement = HTMLDivElement>(

FILE: tauri/src/i18n/index.ts
  function changeLanguage (line 54) | function changeLanguage(lang: string) {

FILE: tauri/src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: tauri/src/main.tsx
  function applyTheme (line 14) | function applyTheme(theme: string) {
  type Register (line 51) | interface Register {

FILE: tauri/src/routeTree.gen.ts
  type FileRoutesByFullPath (line 38) | interface FileRoutesByFullPath {
  type FileRoutesByTo (line 44) | interface FileRoutesByTo {
  type FileRoutesById (line 50) | interface FileRoutesById {
  type FileRouteTypes (line 57) | interface FileRouteTypes {
  type RootRouteChildren (line 65) | interface RootRouteChildren {
  type FileRoutesByPath (line 73) | interface FileRoutesByPath {

FILE: tauri/src/routes/__root.tsx
  function RootLayout (line 6) | function RootLayout() {

FILE: tauri/src/services/dockerApi.ts
  constant DEFAULT_DOCKER_SERVER_URL (line 6) | const DEFAULT_DOCKER_SERVER_URL = "http://localhost:8080";
  constant JWT_STORAGE_KEY (line 7) | const JWT_STORAGE_KEY = "docker_server_jwt";
  constant JWT_EXPIRY_KEY (line 8) | const JWT_EXPIRY_KEY = "docker_server_jwt_expiry";
  type DockerServerConfig (line 10) | interface DockerServerConfig {
  type DockerHealthResponse (line 15) | interface DockerHealthResponse {
  type DockerAuthStatusResponse (line 24) | interface DockerAuthStatusResponse {
  type DockerTokenResponse (line 32) | interface DockerTokenResponse {
  type DockerDownloadJob (line 40) | interface DockerDownloadJob {
  type DockerDownloadResponse (line 51) | interface DockerDownloadResponse {
  type DockerJobStatusResponse (line 60) | interface DockerJobStatusResponse {
  function isYouTubeUrl (line 69) | function isYouTubeUrl(url: string): boolean {
  function getDockerServerUrl (line 89) | function getDockerServerUrl(): string {
  function setDockerServerUrl (line 97) | function setDockerServerUrl(url: string): void {
  function getStoredJwt (line 107) | function getStoredJwt(): string | null {
  function storeJwt (line 128) | function storeJwt(jwt: string): void {
  function getDockerJwtToken (line 139) | function getDockerJwtToken(): string {
  function setDockerJwtToken (line 146) | function setDockerJwtToken(token: string): void {
  function checkAuthRequired (line 158) | async function checkAuthRequired(): Promise<boolean> {
  function generateJwtToken (line 178) | async function generateJwtToken(): Promise<string | null> {
  function getAuthHeaders (line 203) | function getAuthHeaders(): Record<string, string> {
  function checkDockerHealth (line 220) | async function checkDockerHealth(): Promise<boolean> {
  function startDockerDownload (line 240) | async function startDockerDownload(
  function getDockerJobStatus (line 265) | async function getDockerJobStatus(
  function getDockerJobs (line 284) | async function getDockerJobs(): Promise<DockerDownloadJob[]> {
  function cancelDockerJob (line 302) | async function cancelDockerJob(jobId: string): Promise<void> {

FILE: tauri/src/stores/auth.ts
  type AuthStatus (line 4) | type AuthStatus = "logged_out" | "checking" | "logged_in";
  type SiteAuthStatus (line 6) | interface SiteAuthStatus {
  type QRSession (line 12) | interface QRSession {
  type QRPollResult (line 17) | interface QRPollResult {
  constant QR_WAITING (line 24) | const QR_WAITING = 86101;
  constant QR_SCANNED (line 25) | const QR_SCANNED = 86090;
  constant QR_EXPIRED (line 26) | const QR_EXPIRED = 86038;
  constant QR_CONFIRMED (line 27) | const QR_CONFIRMED = 0;
  type AuthState (line 29) | interface AuthState {
  function generateBilibiliQR (line 106) | async function generateBilibiliQR(): Promise<QRSession> {
  function pollBilibiliQR (line 110) | async function pollBilibiliQR(qrcodeKey: string): Promise<QRPollResult> {
  function saveBilibiliCookie (line 114) | async function saveBilibiliCookie(cookie: string): Promise<void> {
  function openXhsLoginWindow (line 119) | async function openXhsLoginWindow(): Promise<void> {

FILE: tauri/src/stores/downloads.ts
  type DownloadStatus (line 5) | type DownloadStatus =
  type DownloadProgress (line 12) | interface DownloadProgress {
  type Download (line 20) | interface Download {
  type DownloadsState (line 30) | interface DownloadsState {
  function setupDownloadListeners (line 69) | async function setupDownloadListeners() {
  function startDownload (line 102) | async function startDownload(
  function cancelDownload (line 130) | async function cancelDownload(jobId: string): Promise<void> {

FILE: ui/src/components/ConfigEditor.tsx
  type WebDAVServer (line 4) | interface WebDAVServer {
  type UITranslations (line 10) | interface UITranslations {
  type ConfigEditorProps (line 31) | interface ConfigEditorProps {
  type ConfigValues (line 57) | interface ConfigValues {
  function ConfigEditor (line 69) | function ConfigEditor({

FILE: ui/src/components/ConfigRow.tsx
  type ConfigRowProps (line 1) | interface ConfigRowProps {
  function ConfigRow (line 9) | function ConfigRow({

FILE: ui/src/components/DownloadJobCard.tsx
  type DownloadJobCardProps (line 7) | interface DownloadJobCardProps {
  function DownloadJobCard (line 14) | function DownloadJobCard({

FILE: ui/src/components/Kuaidi100.tsx
  type TrackingRecord (line 4) | interface TrackingRecord {
  type TrackingResult (line 11) | interface TrackingResult {
  type Kuaidi100Props (line 22) | interface Kuaidi100Props {
  function queryKuaidi100 (line 26) | async function queryKuaidi100(
  function Kuaidi100 (line 38) | function Kuaidi100({ isConnected }: Kuaidi100Props) {

FILE: ui/src/components/Layout.tsx
  function Layout (line 10) | function Layout() {

FILE: ui/src/components/Sidebar.tsx
  type SidebarProps (line 19) | interface SidebarProps {
  type NavItem (line 24) | interface NavItem {
  function Sidebar (line 32) | function Sidebar({ lang, onClose }: SidebarProps) {

FILE: ui/src/components/Toast.tsx
  type ToastType (line 5) | type ToastType = "success" | "error" | "info" | "warning";
  type ToastData (line 7) | interface ToastData {
  type ToastProps (line 13) | interface ToastProps {
  function Toast (line 18) | function Toast({ toast, onDismiss }: ToastProps) {
  type ToastContainerProps (line 55) | interface ToastContainerProps {
  function ToastContainer (line 60) | function ToastContainer({ toasts, onDismiss }: ToastContainerProps) {

FILE: ui/src/components/Torrent.tsx
  type TorrentProps (line 6) | interface TorrentProps {
  function Torrent (line 11) | function Torrent({ isConnected, torrentEnabled }: TorrentProps) {

FILE: ui/src/components/TorrentSettings.tsx
  type TorrentSettingsProps (line 10) | interface TorrentSettingsProps {
  function TorrentSettings (line 14) | function TorrentSettings({ isConnected }: TorrentSettingsProps) {

FILE: ui/src/context/AppContext.tsx
  type AppContextType (line 38) | interface AppContextType {
  function AppProvider (line 90) | function AppProvider({ children }: { children: ReactNode }) {
  function useApp (line 325) | function useApp() {

FILE: ui/src/main.tsx
  type Register (line 14) | interface Register {

FILE: ui/src/pages/BilibiliPage.tsx
  type LoginMethod (line 6) | type LoginMethod = "qr" | "cookie";
  constant QR_WAITING (line 9) | const QR_WAITING = 86101;
  constant QR_SCANNED (line 10) | const QR_SCANNED = 86090;
  constant QR_EXPIRED (line 11) | const QR_EXPIRED = 86038;
  constant QR_CONFIRMED (line 12) | const QR_CONFIRMED = 0;
  type CookieFields (line 14) | interface CookieFields {
  type QRSession (line 20) | interface QRSession {
  type BilibiliStatus (line 25) | interface BilibiliStatus {
  function parseCookie (line 31) | function parseCookie(cookieStr: string): CookieFields {
  function buildCookie (line 46) | function buildCookie(fields: CookieFields): string {
  function BilibiliPage (line 54) | function BilibiliPage() {
  function QRLogin (line 173) | function QRLogin({ onSuccess }: { onSuccess: () => void }) {
  function CookieLogin (line 363) | function CookieLogin({ onSuccess }: { onSuccess: () => void }) {

FILE: ui/src/pages/BulkDownloadPage.tsx
  function BulkDownloadPage (line 7) | function BulkDownloadPage() {

FILE: ui/src/pages/ConfigPage.tsx
  function ConfigPage (line 5) | function ConfigPage() {

FILE: ui/src/pages/DownloadPage.tsx
  function DownloadPage (line 6) | function DownloadPage() {

FILE: ui/src/pages/HistoryPage.tsx
  function formatDuration (line 20) | function formatDuration(seconds: number): string {
  function formatDate (line 28) | function formatDate(unixTimestamp: number): string {
  function extractFilename (line 33) | function extractFilename(record: HistoryRecord): string {
  function HistoryPage (line 48) | function HistoryPage() {

FILE: ui/src/pages/Kuaidi100Page.tsx
  function Kuaidi100Page (line 4) | function Kuaidi100Page() {

FILE: ui/src/pages/PodcastPage.tsx
  type ViewState (line 13) | type ViewState =
  function PodcastPage (line 18) | function PodcastPage() {

FILE: ui/src/pages/TokenPage.tsx
  type GenerateTokenResponse (line 4) | interface GenerateTokenResponse {
  function TokenPage (line 10) | function TokenPage() {

FILE: ui/src/pages/TorrentPage.tsx
  function TorrentPage (line 4) | function TorrentPage() {

FILE: ui/src/pages/WebDAVPage.tsx
  function formatSize (line 20) | function formatSize(bytes: number): string {
  function WebDAVPage (line 27) | function WebDAVPage() {

FILE: ui/src/routeTree.gen.ts
  type FileRoutesByFullPath (line 74) | interface FileRoutesByFullPath {
  type FileRoutesByTo (line 86) | interface FileRoutesByTo {
  type FileRoutesById (line 98) | interface FileRoutesById {
  type FileRouteTypes (line 111) | interface FileRouteTypes {
  type RootRouteChildren (line 150) | interface RootRouteChildren {
  type FileRoutesByPath (line 164) | interface FileRoutesByPath {

FILE: ui/src/utils/apis.ts
  type JobStatus (line 3) | type JobStatus =
  type Job (line 10) | interface Job {
  type ApiResponse (line 21) | interface ApiResponse<T> {
  type HealthData (line 27) | interface HealthData {
  type WebDAVServer (line 32) | interface WebDAVServer {
  type ConfigData (line 38) | interface ConfigData {
  type TorrentConfig (line 54) | interface TorrentConfig {
  type TorrentAddResult (line 64) | interface TorrentAddResult {
  type JobsData (line 71) | interface JobsData {
  type I18nData (line 75) | interface I18nData {
  function fetchHealth (line 82) | async function fetchHealth(): Promise<ApiResponse<HealthData>> {
  type AuthStatusData (line 89) | interface AuthStatusData {
  type GenerateTokenData (line 93) | interface GenerateTokenData {
  function fetchAuthStatus (line 97) | async function fetchAuthStatus(): Promise<ApiResponse<AuthStatusData>> {
  function generateApiToken (line 102) | async function generateApiToken(): Promise<ApiResponse<GenerateTokenData...
  function fetchJobs (line 107) | async function fetchJobs(): Promise<ApiResponse<JobsData>> {
  function fetchConfig (line 112) | async function fetchConfig(): Promise<ApiResponse<ConfigData>> {
  function fetchI18n (line 117) | async function fetchI18n(): Promise<ApiResponse<I18nData>> {
  function updateConfig (line 122) | async function updateConfig(
  function setConfigValue (line 133) | async function setConfigValue(
  function postDownload (line 145) | async function postDownload(
  type BulkDownloadJob (line 157) | interface BulkDownloadJob {
  type BulkDownloadResult (line 164) | interface BulkDownloadResult {
  function postBulkDownload (line 170) | async function postBulkDownload(
  function addWebDAVServer (line 181) | async function addWebDAVServer(
  function deleteWebDAVServer (line 195) | async function deleteWebDAVServer(
  function deleteJob (line 204) | async function deleteJob(
  function clearHistory (line 211) | async function clearHistory(): Promise<
  function fetchTorrentConfig (line 220) | async function fetchTorrentConfig(): Promise<
  function saveTorrentConfig (line 227) | async function saveTorrentConfig(
  function testTorrentConnection (line 238) | async function testTorrentConnection(): Promise<
  function addTorrent (line 247) | async function addTorrent(
  type WebDAVRemote (line 261) | interface WebDAVRemote {
  type WebDAVFile (line 267) | interface WebDAVFile {
  type WebDAVListData (line 274) | interface WebDAVListData {
  function fetchWebDAVRemotes (line 280) | async function fetchWebDAVRemotes(): Promise<
  function fetchWebDAVList (line 287) | async function fetchWebDAVList(
  function submitWebDAVDownload (line 296) | async function submitWebDAVDownload(
  type PodcastChannel (line 310) | interface PodcastChannel {
  type PodcastEpisode (line 320) | interface PodcastEpisode {
  type PodcastSearchResult (line 330) | interface PodcastSearchResult {
  function searchPodcasts (line 336) | async function searchPodcasts(
  function fetchPodcastEpisodes (line 348) | async function fetchPodcastEpisodes(
  type HistoryRecord (line 362) | interface HistoryRecord {
  type HistoryStats (line 374) | interface HistoryStats {
  type HistoryData (line 380) | interface HistoryData {
  function fetchHistory (line 388) | async function fetchHistory(
  function deleteHistoryRecord (line 400) | async function deleteHistoryRecord(
  function clearAllHistory (line 407) | async function clearAllHistory(): Promise<

FILE: ui/src/utils/translations.ts
  type UITranslations (line 1) | interface UITranslations {
  type ServerTranslations (line 104) | interface ServerTranslations {
Condensed preview — 304 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (2,028K chars).
[
  {
    "path": ".dockerignore",
    "chars": 512,
    "preview": "# Build artifacts\nbuild/\ntmp/\n*.exe\n*.dll\n*.so\n*.dylib\n\n# UI build artifacts (will be built in container)\nui/node_module"
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 997,
    "preview": "name: CI\n\non:\n  push:\n    branches: [main]\n  pull_request:\n    branches: [main]\n\njobs:\n  test:\n    name: Test\n    runs-o"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 5092,
    "preview": "name: Release\n\non:\n  push:\n    tags:\n      - \"v*.*.*\"\n\npermissions:\n  contents: write\n  packages: write\n\njobs:\n  release"
  },
  {
    "path": ".gitignore",
    "chars": 215,
    "preview": "# Build output\n/build/\n/dist/\n/internal/server/dist/\n\n# Binary\n/vget\n\n# IDE\n.idea/\n*.swp\n*.swo\n\n# OS\n.DS_Store\nThumbs.db"
  },
  {
    "path": ".goreleaser.yaml",
    "chars": 1551,
    "preview": "version: 2\n\nproject_name: vget\n\nbefore:\n  hooks:\n    - go mod tidy\n\nbuilds:\n  - main: ./cmd/vget\n    binary: vget\n    en"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 200,
    "preview": "{\n  \"makefile.configureOnOpen\": false,\n  \"diffEditor.renderSideBySide\": false,\n  \"diffEditor.hideUnchangedRegions.enable"
  },
  {
    "path": "CLAUDE.md",
    "chars": 4290,
    "preview": "# CLAUDE.md\n\nThis file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.\n\n## "
  },
  {
    "path": "LICENSE",
    "chars": 545,
    "preview": "Copyright 2025 Yumin\n\nLicensed under the Apache License, Version 2.0 (the \"License\");\nyou may not use this file except i"
  },
  {
    "path": "Makefile",
    "chars": 3737,
    "preview": ".PHONY: build build-ui build-metal build-cuda build-nocgo build-whisper push version patch minor major\n\nBUILD_DIR := ./b"
  },
  {
    "path": "README.md",
    "chars": 3873,
    "preview": "# vget\n\nVersatile downloader for audio, video, podcasts, PDFs and more. Available as CLI and Docker\n\n[简体中文](README_zh.md"
  },
  {
    "path": "README_de.md",
    "chars": 3888,
    "preview": "# vget\n\nVielseitiger Downloader für Audio, Video, Podcasts, PDFs und mehr. Verfügbar als CLI und Docker.\n\n[English](READ"
  },
  {
    "path": "README_es.md",
    "chars": 3812,
    "preview": "# vget\n\nDescargador versátil para audio, video, podcasts, PDFs y más. Disponible como CLI y Docker.\n\n[English](README.md"
  },
  {
    "path": "README_fr.md",
    "chars": 3911,
    "preview": "# vget\n\nTéléchargeur polyvalent pour audio, vidéo, podcasts, PDFs et plus. Disponible en CLI et Docker.\n\n[English](READM"
  },
  {
    "path": "README_jp.md",
    "chars": 3256,
    "preview": "# vget\n\nオーディオ、ビデオ、ポッドキャスト、PDFなどをダウンロードする多機能ツール。CLI と Docker で利用可能。\n\n[English](README.md) | [简体中文](README_zh.md) | [한국어]("
  },
  {
    "path": "README_kr.md",
    "chars": 3272,
    "preview": "# vget\n\n오디오, 비디오, 팟캐스트, PDF 등을 다운로드하는 다목적 도구. CLI 및 Docker로 사용 가능.\n\n[English](README.md) | [简体中文](README_zh.md) | [日本語]("
  },
  {
    "path": "README_zh.md",
    "chars": 3640,
    "preview": "# vget\n\n多功能下载工具,支持音频、视频、播客、PDF等。提供 CLI 和 Docker 两种方式。\n\n[English](README.md) | [日本語](README_jp.md) | [한국어](README_kr.md) "
  },
  {
    "path": "TODO.md",
    "chars": 2918,
    "preview": "# TODO\n\n## Tomorrow's Tasks\n\n3. [x] kuaidi100 - Bring Your Own Key (API is expensive)\n\n## Features\n\n- [x] `vget init` co"
  },
  {
    "path": "cmd/vget/main.go",
    "chars": 145,
    "preview": "package main\n\nimport (\n\t\"os\"\n\n\t\"github.com/guiyumin/vget/internal/cli\"\n)\n\nfunc main() {\n\tif err := cli.Execute(); err !="
  },
  {
    "path": "cmd/vget-server/main.go",
    "chars": 1987,
    "preview": "package main\n\nimport (\n\t\"context\"\n\t\"flag\"\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/signal\"\n\t\"path/filepath\"\n\t\"syscall\"\n\t\"time\"\n\n\t\"github"
  },
  {
    "path": "compose.yml",
    "chars": 1461,
    "preview": "# vget Docker Compose\n#\n# Image variants (choose based on your system):\n#   ghcr.io/guiyumin/vget:latest      - No model"
  },
  {
    "path": "docker/vget/Dockerfile",
    "chars": 1419,
    "preview": "# vget Docker Image\n\n# Build stage for UI\nFROM node:22-slim AS ui-builder\nWORKDIR /app/ui\nCOPY ui/package*.json ./\nRUN n"
  },
  {
    "path": "docker/vget/Dockerfile.arm64",
    "chars": 1491,
    "preview": "# vget Docker Image for ARM64 (Apple Silicon, Raspberry Pi, ARM servers)\n\n# Build stage for UI\nFROM node:22-slim AS ui-b"
  },
  {
    "path": "docker/vget/entrypoint-arm64.sh",
    "chars": 247,
    "preview": "#!/bin/bash\nset -e\n\n# Fix ownership of mounted volumes if running as root\nif [ \"$(id -u)\" = \"0\" ]; then\n    chown -R 100"
  },
  {
    "path": "docker/vget/entrypoint.sh",
    "chars": 247,
    "preview": "#!/bin/bash\nset -e\n\n# Fix ownership of mounted volumes if running as root\nif [ \"$(id -u)\" = \"0\" ]; then\n    chown -R 100"
  },
  {
    "path": "docs/FAQs.md",
    "chars": 1398,
    "preview": "# FAQs & Troubleshooting\n\n## FFmpeg Merge Failed: thread_create failed\n\n**Error message:**\n\n```\nffmpeg merge failed: thr"
  },
  {
    "path": "docs/PRD.md",
    "chars": 15769,
    "preview": "# vget – Product Requirement Document (PRD)\n\n**Version:** 1.2\n**Author:** Yumin\n**Language:** Golang\n**UI:** Bubble Tea "
  },
  {
    "path": "docs/YOUTUBE_NOTES.md",
    "chars": 4687,
    "preview": "# YouTube Support Notes\n\n## Status: Delegated to yt-dlp (Docker Only)\n\nAfter extensive research and failed attempts, we'"
  },
  {
    "path": "docs/bilibili-port-plan.md",
    "chars": 24276,
    "preview": "# BBDown Go Port Plan\n\nThis document outlines the plan for porting [BBDown](https://github.com/nilaoda/BBDown) (a C# Bil"
  },
  {
    "path": "docs/bugfix/docker-browser-launch.md",
    "chars": 1845,
    "preview": "# Docker Browser Launch Hang Bug\n\n## Problem\n\nBrowser-based extractors (m3u8 detection, XHS, etc.) would hang indefinite"
  },
  {
    "path": "docs/homebrew-distribution.md",
    "chars": 3175,
    "preview": "# Homebrew Distribution\n\nThis document explains how to distribute vget via Homebrew.\n\n## Option 1: Own Tap (Recommended)"
  },
  {
    "path": "docs/http-server-mode.md",
    "chars": 16837,
    "preview": "# HTTP Server Mode (`vget server`)\n\n## Overview\n\nHTTP server mode that accepts download requests via API, with an embedd"
  },
  {
    "path": "docs/multi-binary-architecture.md",
    "chars": 2762,
    "preview": "# Multi-Binary Architecture\n\n## Overview\n\nvget is split into separate binaries with a shared core module:\n\n| Binary | Pu"
  },
  {
    "path": "docs/seedbox.md",
    "chars": 17823,
    "preview": "# Seedbox Support\n\n## Overview\n\nExtend vget to support seedboxes as remote torrent clients. Unlike NAS mode (dispatch on"
  },
  {
    "path": "docs/tauri.md",
    "chars": 13090,
    "preview": "# vget Desktop App - Tauri Implementation Plan\n\n## Goal\nBuild a desktop app version of vget using Tauri 2.0 with React f"
  },
  {
    "path": "docs/telegram.md",
    "chars": 12678,
    "preview": "# Telegram Support\n\nImplementation plan for Telegram media download support in vget.\n\n## Overview\n\nvget aims to be an al"
  },
  {
    "path": "docs/torrent-dispatch.md",
    "chars": 4290,
    "preview": "# Torrent Dispatch Feature\n\n## Overview\n\nAllow vget to dispatch magnet links and .torrent files to remote torrent client"
  },
  {
    "path": "docs/tui-file-browser.md",
    "chars": 1302,
    "preview": "# TUI File Browser for Remote Paths\n\n## Overview\n\nWhen user runs `vget <remote>:/path/to/directory/`, instead of showing"
  },
  {
    "path": "docs/webdav-browsing.md",
    "chars": 16823,
    "preview": "# WebDAV File Browsing (Web UI)\n\n## Overview\n\nAdd file browsing capability to the vget Web UI for WebDAV remotes. Curren"
  },
  {
    "path": "docs/webdav.md",
    "chars": 3021,
    "preview": "# WebDAV Support\n\nvget supports downloading files from WebDAV servers and browsing remote directories.\n\n## Configuration"
  },
  {
    "path": "docs/xhs-mcp-analysis.md",
    "chars": 9290,
    "preview": "# Xiaohongshu MCP Analysis\n\nAnalysis of [xpzouying/xiaohongshu-mcp](https://github.com/xpzouying/xiaohongshu-mcp) for im"
  },
  {
    "path": "docs/zsh-completion-limit.md",
    "chars": 1854,
    "preview": "# Zsh Completion Limit\n\n## Issue\n\nWhen using zsh shell completion for remote paths (e.g., `pikpak:/电影/`), directories wi"
  },
  {
    "path": "go.mod",
    "chars": 5795,
    "preview": "module github.com/guiyumin/vget\n\ngo 1.25.4\n\nrequire (\n\tcodeberg.org/gruf/go-ffmpreg v0.6.16\n\tgithub.com/charmbracelet/bu"
  },
  {
    "path": "go.sum",
    "chars": 30097,
    "preview": "code.gitea.io/sdk/gitea v0.22.0 h1:HCKq7bX/HQ85Nw7c/HAhWgRye+vBp5nQOE8Md1+9Ef0=\ncode.gitea.io/sdk/gitea v0.22.0/go.mod h"
  },
  {
    "path": "internal/cli/batch.go",
    "chars": 3218,
    "preview": "package cli\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/guiyumin/vget/internal/core/extractor\"\n)\n\n// runBat"
  },
  {
    "path": "internal/cli/browse.go",
    "chars": 8072,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"path\"\n\t\"sort\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github"
  },
  {
    "path": "internal/cli/completion.go",
    "chars": 5774,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/guiyumin/vget/internal/core/config\"\n\t\"g"
  },
  {
    "path": "internal/cli/config.go",
    "chars": 15054,
    "preview": "package cli\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\t\"syscall\"\n\n\t\"github.com/guiyumin/vget/internal/core/config\"\n\t\"gi"
  },
  {
    "path": "internal/cli/extract.go",
    "chars": 4940,
    "preview": "package cli\n\nimport (\n\t\"errors\"\n\t\"fmt\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\ttea \"github.com/cha"
  },
  {
    "path": "internal/cli/init.go",
    "chars": 585,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\n\t\"github.com/guiyumin/vget/internal/core/config\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar initCmd "
  },
  {
    "path": "internal/cli/kuaidi100.go",
    "chars": 4298,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"github.com/fatih/color\"\n\t\"github.com/guiyumin/vget/internal/core/config"
  },
  {
    "path": "internal/cli/login/bilibili.go",
    "chars": 14887,
    "preview": "package login\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/charmbracelet/bubbles/spinner\"\n\t\"github.com/charmbracele"
  },
  {
    "path": "internal/cli/login/qrwriter.go",
    "chars": 3080,
    "preview": "package login\n\nimport (\n\t\"github.com/mattn/go-runewidth\"\n\ttermbox \"github.com/nsf/termbox-go\"\n\t\"github.com/yeqown/go-qrc"
  },
  {
    "path": "internal/cli/login.go",
    "chars": 576,
    "preview": "package cli\n\nimport (\n\t\"github.com/guiyumin/vget/internal/cli/login\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar loginCmd = &cobra."
  },
  {
    "path": "internal/cli/ls.go",
    "chars": 3575,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"sort\"\n\n\t\"github.com/guiyumin/vget/internal/core/config\"\n\t\"git"
  },
  {
    "path": "internal/cli/root.go",
    "chars": 21347,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"errors\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\n\t\"github.com/guiyumin/vget/internal"
  },
  {
    "path": "internal/cli/search.go",
    "chars": 19513,
    "preview": "package cli\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t\"t"
  },
  {
    "path": "internal/cli/search_tui.go",
    "chars": 12383,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/charmbracelet/bubbles/key\"\n\ttea \"github.com/charmbracelet/bubblete"
  },
  {
    "path": "internal/cli/telegram.go",
    "chars": 9491,
    "preview": "package cli\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"time\"\n\n\t\"github.com/gotd/td/session\"\n\t\"githu"
  },
  {
    "path": "internal/cli/update.go",
    "chars": 326,
    "preview": "package cli\n\nimport (\n\t\"github.com/guiyumin/vget/internal/updater\"\n\t\"github.com/spf13/cobra\"\n)\n\nvar updateCmd = &cobra.C"
  },
  {
    "path": "internal/cli/version.go",
    "chars": 392,
    "preview": "package cli\n\nimport (\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"github.com/guiyumin/vget/internal/core/version\"\n\t\"github.com/spf13/cobra\"\n)\n\n"
  },
  {
    "path": "internal/core/config/config.go",
    "chars": 10280,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"runtime\"\n\t\"strings\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst (\n\tConfigFile"
  },
  {
    "path": "internal/core/config/config_test.go",
    "chars": 1346,
    "preview": "package config\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n\t\"testing\"\n)\n\nfunc TestExpandPath(t *testing.T) {\n\thome, err := os.UserH"
  },
  {
    "path": "internal/core/config/sites.go",
    "chars": 2220,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\t\"strings\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\nconst SitesFileName = \"sites.yml\"\n\n// Site repr"
  },
  {
    "path": "internal/core/config/wizard.go",
    "chars": 8680,
    "preview": "package config\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\ttea \"github.com/charmbracelet/bubbletea\"\n\t\"github.com/charmbracelet/lipglos"
  },
  {
    "path": "internal/core/downloader/downloader.go",
    "chars": 1841,
    "preview": "package downloader\n\nimport (\n\t\"fmt\"\n\t\"io\"\n\t\"time\"\n)\n\n// DefaultUserAgent is the default User-Agent header used for downl"
  },
  {
    "path": "internal/core/downloader/ffmpeg.go",
    "chars": 3344,
    "preview": "package downloader\n\nimport (\n\t\"fmt\"\n\t\"log\"\n\t\"os\"\n\t\"os/exec\"\n\t\"strings\"\n)\n\n// FFmpegAvailable checks if ffmpeg is install"
  },
  {
    "path": "internal/core/downloader/hls.go",
    "chars": 14504,
    "preview": "package downloader\n\nimport (\n\t\"context\"\n\t\"crypto/aes\"\n\t\"crypto/cipher\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\""
  },
  {
    "path": "internal/core/downloader/hls_parser.go",
    "chars": 6578,
    "preview": "package downloader\n\nimport (\n\t\"bufio\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"strings\"\n\t\"time\"\n)\n\n// "
  },
  {
    "path": "internal/core/downloader/magic.go",
    "chars": 1797,
    "preview": "package downloader\n\nimport (\n\t\"bytes\"\n\t\"io\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n)\n\n// DetectFileType reads the first few b"
  },
  {
    "path": "internal/core/downloader/multistream.go",
    "chars": 21938,
    "preview": "package downloader\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"sync\"\n\t\"sync/atomic\"\n\t\"time\"\n\n\ttea \"github.com/"
  },
  {
    "path": "internal/core/downloader/progress.go",
    "chars": 12273,
    "preview": "package downloader\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/ch"
  },
  {
    "path": "internal/core/extractor/bilibili.go",
    "chars": 14376,
    "preview": "package extractor\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\""
  },
  {
    "path": "internal/core/extractor/browser.go",
    "chars": 9644,
    "preview": "package extractor\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.c"
  },
  {
    "path": "internal/core/extractor/direct.go",
    "chars": 5188,
    "preview": "package extractor\n\nimport (\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n)\n\n// DirectExtractor handles direc"
  },
  {
    "path": "internal/core/extractor/instagram.go",
    "chars": 512,
    "preview": "package extractor\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n)\n\n// InstagramExtractor handles Instagram video downloads\ntype InstagramE"
  },
  {
    "path": "internal/core/extractor/itunes.go",
    "chars": 3463,
    "preview": "package extractor\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n)\n\n// iTunesExtractor handles Apple"
  },
  {
    "path": "internal/core/extractor/m3u8.go",
    "chars": 1694,
    "preview": "package extractor\n\nimport (\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\t\"time\"\n)\n\n// M3U8Extractor handles direct m3u8 pl"
  },
  {
    "path": "internal/core/extractor/registry.go",
    "chars": 3911,
    "preview": "package extractor\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n)\n\n// extractorsByHost maps hostnames to their extracto"
  },
  {
    "path": "internal/core/extractor/telegram/constants.go",
    "chars": 289,
    "preview": "// Package telegram provides Telegram media extraction and download functionality.\npackage telegram\n\nconst (\n\t// Telegra"
  },
  {
    "path": "internal/core/extractor/telegram/download.go",
    "chars": 10484,
    "preview": "package telegram\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"os\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gotd/td/session\"\n\t\"g"
  },
  {
    "path": "internal/core/extractor/telegram/extractor.go",
    "chars": 3895,
    "preview": "package telegram\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gotd/td/session\"\n\t\"github.com/g"
  },
  {
    "path": "internal/core/extractor/telegram/media.go",
    "chars": 2323,
    "preview": "package telegram\n\nimport (\n\t\"fmt\"\n\t\"strings\"\n\n\t\"github.com/gotd/td/tg\"\n)\n\n// ExtractedMedia contains extracted media inf"
  },
  {
    "path": "internal/core/extractor/telegram/parser.go",
    "chars": 1717,
    "preview": "package telegram\n\nimport (\n\t\"fmt\"\n\t\"regexp\"\n\t\"strconv\"\n)\n\nvar (\n\t// t.me/channel/123 or t.me/username/123\n\tpublicURLRege"
  },
  {
    "path": "internal/core/extractor/telegram/session.go",
    "chars": 654,
    "preview": "package telegram\n\nimport (\n\t\"os\"\n\t\"path/filepath\"\n)\n\n// SessionPath returns the path where Telegram session is stored\n//"
  },
  {
    "path": "internal/core/extractor/telegram/takeout.go",
    "chars": 2420,
    "preview": "package telegram\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\n\t\"github.com/gotd/td/bin\"\n\t\"github.com/gotd/td/telegram\"\n\t\"github.com/gotd"
  },
  {
    "path": "internal/core/extractor/telegram.go",
    "chars": 1910,
    "preview": "package extractor\n\nimport (\n\t\"net/url\"\n\n\t\"github.com/guiyumin/vget/internal/core/extractor/telegram\"\n)\n\n// Re-export tel"
  },
  {
    "path": "internal/core/extractor/tiktok.go",
    "chars": 501,
    "preview": "package extractor\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n)\n\n// TikTokExtractor handles TikTok video downloads\ntype TikTokExtractor "
  },
  {
    "path": "internal/core/extractor/twitter.go",
    "chars": 23277,
    "preview": "package extractor\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"sort\"\n\t\"strconv\"\n\t\"strings\""
  },
  {
    "path": "internal/core/extractor/types.go",
    "chars": 6198,
    "preview": "package extractor\n\nimport (\n\t\"fmt\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"slices\"\n\t\"strings\"\n)\n\n// MediaType represents the type of medi"
  },
  {
    "path": "internal/core/extractor/types_test.go",
    "chars": 2413,
    "preview": "package extractor\n\nimport \"testing\"\n\nfunc TestSanitizeFilename(t *testing.T) {\n\ttests := []struct {\n\t\tname     string\n\t\t"
  },
  {
    "path": "internal/core/extractor/xiaohongshu.go",
    "chars": 12214,
    "preview": "package extractor\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strings\"\n\t\"time\"\n\n\t\"gi"
  },
  {
    "path": "internal/core/extractor/xiaoyuzhou.go",
    "chars": 3508,
    "preview": "package extractor\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"regexp\"\n\t\"strings\"\n)\n\n// XiaoyuzhouEx"
  },
  {
    "path": "internal/core/extractor/youtube.go",
    "chars": 4538,
    "preview": "package extractor\n\nimport (\n\t\"bufio\"\n\t\"context\"\n\t\"net/url\"\n\t\"os\"\n\t\"os/exec\"\n\t\"path/filepath\"\n\t\"regexp\"\n\t\"strconv\"\n\t\"stri"
  },
  {
    "path": "internal/core/i18n/i18n.go",
    "chars": 13370,
    "preview": "package i18n\n\nimport (\n\t\"embed\"\n\t\"fmt\"\n\t\"sync\"\n\n\t\"gopkg.in/yaml.v3\"\n)\n\n//go:embed locales/*.yml\nvar localesFS embed.FS\n\n"
  },
  {
    "path": "internal/core/i18n/locales/de.yml",
    "chars": 7180,
    "preview": "# Deutsche Übersetzungen\n\nconfig:\n  step_of: \"Schritt %d von %d\"\n  language: \"Sprache\"\n  language_desc: \"Bevorzugte Spra"
  },
  {
    "path": "internal/core/i18n/locales/en.yml",
    "chars": 6884,
    "preview": "# English translations\n\nconfig:\n  step_of: \"Step %d of %d\"\n  language: \"Language\"\n  language_desc: \"Preferred language f"
  },
  {
    "path": "internal/core/i18n/locales/es.yml",
    "chars": 7029,
    "preview": "# Traducciones en español\n\nconfig:\n  step_of: \"Paso %d de %d\"\n  language: \"Idioma\"\n  language_desc: \"Idioma preferido pa"
  },
  {
    "path": "internal/core/i18n/locales/fr.yml",
    "chars": 7220,
    "preview": "# Traductions françaises\n\nconfig:\n  step_of: \"Étape %d sur %d\"\n  language: \"Langue\"\n  language_desc: \"Langue préférée po"
  },
  {
    "path": "internal/core/i18n/locales/jp.yml",
    "chars": 5311,
    "preview": "# 日本語翻訳\n\nconfig:\n  step_of: \"ステップ %d / %d\"\n  language: \"言語\"\n  language_desc: \"メタデータの言語設定\"\n  output_dir: \"出力ディレクトリ\"\n  out"
  },
  {
    "path": "internal/core/i18n/locales/kr.yml",
    "chars": 5275,
    "preview": "# 한국어 번역\n\nconfig:\n  step_of: \"%d단계 / %d단계\"\n  language: \"언어\"\n  language_desc: \"메타데이터 언어 설정\"\n  output_dir: \"출력 디렉토리\"\n  out"
  },
  {
    "path": "internal/core/i18n/locales/zh.yml",
    "chars": 5041,
    "preview": "# 中文翻译\n\nconfig:\n  step_of: \"第 %d 步,共 %d 步\"\n  language: \"语言\"\n  language_desc: \"元数据的首选语言\"\n  output_dir: \"输出目录\"\n  output_di"
  },
  {
    "path": "internal/core/site/bilibili/auth.go",
    "chars": 6905,
    "preview": "package bilibili\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/guiyumi"
  },
  {
    "path": "internal/core/tracker/kuaidi100.go",
    "chars": 14333,
    "preview": "package tracker\n\nimport (\n\t\"crypto/md5\"\n\t\"encoding/hex\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n"
  },
  {
    "path": "internal/core/version/version.go",
    "chars": 90,
    "preview": "package version\n\nvar (\n\tVersion = \"0.13.5\"\n\tCommit  = \"unknown\"\n\tDate    = \"2026-03-14\"\n)\n"
  },
  {
    "path": "internal/core/webdav/client.go",
    "chars": 7271,
    "preview": "package webdav\n\nimport (\n\t\"context\"\n\t\"encoding/base64\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"path\"\n\t\"strings\"\n\n\t\"github."
  },
  {
    "path": "internal/server/auth.go",
    "chars": 5002,
    "preview": "package server\n\nimport (\n\t\"net/http\"\n\t\"strings\"\n\t\"time\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/golang-jwt/jwt/v5\"\n)\n\n"
  },
  {
    "path": "internal/server/bilibili.go",
    "chars": 3540,
    "preview": "package server\n\nimport (\n\t\"log\"\n\t\"net/http\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/guiyumin/vget/internal/core/config"
  },
  {
    "path": "internal/server/embed.go",
    "chars": 307,
    "preview": "package server\n\nimport (\n\t\"embed\"\n\t\"io/fs\"\n)\n\n//go:embed all:dist\nvar distFS embed.FS\n\n// GetDistFS returns the embedded"
  },
  {
    "path": "internal/server/history.go",
    "chars": 5160,
    "preview": "package server\n\nimport (\n\t\"database/sql\"\n\t\"fmt\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"sync\"\n\n\t\"github.com/guiyumin/vget/internal/core"
  },
  {
    "path": "internal/server/job.go",
    "chars": 8685,
    "preview": "package server\n\nimport (\n\t\"context\"\n\t\"crypto/rand\"\n\t\"encoding/hex\"\n\t\"fmt\"\n\t\"log\"\n\t\"sync\"\n\t\"time\"\n\n\t\"github.com/guiyumin/"
  },
  {
    "path": "internal/server/podcast.go",
    "chars": 13786,
    "preview": "package server\n\nimport (\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"strings\"\n\t\"sync\"\n\t\"unicode\"\n\n\t\"github.co"
  },
  {
    "path": "internal/server/server.go",
    "chars": 47030,
    "preview": "package server\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"io\"\n\t\"io/fs\"\n\t\"log\"\n\t\"net/http\"\n\t\"os\"\n\t\"path/filepath\"\n\t\"strings\"\n\t\"sync\"\n\t"
  },
  {
    "path": "internal/server/webdav_browse.go",
    "chars": 4430,
    "preview": "package server\n\nimport (\n\t\"net/http\"\n\t\"sort\"\n\n\t\"github.com/gin-gonic/gin\"\n\t\"github.com/guiyumin/vget/internal/core/confi"
  },
  {
    "path": "internal/torrent/client.go",
    "chars": 5426,
    "preview": "// Package torrent provides integration with various torrent clients for remote download management.\n// vget doesn't dow"
  },
  {
    "path": "internal/torrent/qbittorrent.go",
    "chars": 10933,
    "preview": "package torrent\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/http/cookiejar\"\n\t\"n"
  },
  {
    "path": "internal/torrent/synology.go",
    "chars": 13449,
    "preview": "package torrent\n\nimport (\n\t\"bytes\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"mime/multipart\"\n\t\"net/http\"\n\t\"net/url\"\n\t\"os\"\n\t\"path/f"
  },
  {
    "path": "internal/torrent/transmission.go",
    "chars": 10098,
    "preview": "package torrent\n\nimport (\n\t\"bytes\"\n\t\"encoding/base64\"\n\t\"encoding/json\"\n\t\"fmt\"\n\t\"io\"\n\t\"net/http\"\n\t\"os\"\n\t\"time\"\n)\n\n// Tran"
  },
  {
    "path": "internal/updater/updater.go",
    "chars": 2528,
    "preview": "package updater\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"runtime\"\n\n\t\"github.com/creativeprojects/go-selfupdate\"\n\t\"github.com/guiyum"
  },
  {
    "path": "sites.md",
    "chars": 2156,
    "preview": "# Supported Sites\n\n## General\n\n| Source                    | URL                      | Type            |\n| ------------"
  },
  {
    "path": "tauri/.gitignore",
    "chars": 263,
    "preview": "# Dependencies\nnode_modules/\n\n# Build\ndist/\n.tanstack/\n\n# Logs\n*.log\n\n# Editor\n.vscode/\n.idea/\n\n# OS\n.DS_Store\n\n# Tauri\n"
  },
  {
    "path": "tauri/Makefile",
    "chars": 380,
    "preview": ".PHONY: dev build install clean\n\n# Development\ndev:\n\tbun tauri dev\n\n# Build for release\nbuild:\n\tbun tauri build\n\n# Insta"
  },
  {
    "path": "tauri/components.json",
    "chars": 444,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": "
  },
  {
    "path": "tauri/index.html",
    "chars": 353,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\" />\n    <link rel=\"icon\" type=\"image/png\" href=\"/app-"
  },
  {
    "path": "tauri/package.json",
    "chars": 2752,
    "preview": "{\n  \"name\": \"vget-desktop\",\n  \"private\": true,\n  \"version\": \"0.1.0\",\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"vite"
  },
  {
    "path": "tauri/src/components/AppSidebar.tsx",
    "chars": 4910,
    "preview": "import { Link, useLocation } from \"@tanstack/react-router\";\nimport { Download, Settings, ChevronLeft, Wrench } from \"luc"
  },
  {
    "path": "tauri/src/components/home/DownloadItem.tsx",
    "chars": 2756,
    "preview": "import { X, CheckCircle2, AlertCircle, Loader2 } from \"lucide-react\";\nimport { useTranslation } from \"react-i18next\";\nim"
  },
  {
    "path": "tauri/src/components/home/HomePage.tsx",
    "chars": 15086,
    "preview": "import { useEffect, useState, useRef } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { open } from"
  },
  {
    "path": "tauri/src/components/home/types.ts",
    "chars": 800,
    "preview": "export interface MediaInfo {\n  id: string;\n  title: string;\n  uploader: string | null;\n  thumbnail: string | null;\n  dur"
  },
  {
    "path": "tauri/src/components/icons/PdfIcon.tsx",
    "chars": 778,
    "preview": "import { SVGProps } from \"react\";\n\nexport function PdfIcon(props: SVGProps<SVGSVGElement>) {\n  return (\n    <svg\n      x"
  },
  {
    "path": "tauri/src/components/media-tools/MediaToolsPage.tsx",
    "chars": 7364,
    "preview": "import { useState, useEffect } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { listen } from \"@tau"
  },
  {
    "path": "tauri/src/components/media-tools/panels/AudioConvertPanel.tsx",
    "chars": 3077,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { Button } from \"@/components/ui"
  },
  {
    "path": "tauri/src/components/media-tools/panels/CompressPanel.tsx",
    "chars": 2950,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { Button } from \"@/components/ui"
  },
  {
    "path": "tauri/src/components/media-tools/panels/ConvertPanel.tsx",
    "chars": 2903,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { Button } from \"@/components/ui"
  },
  {
    "path": "tauri/src/components/media-tools/panels/ExtractAudioPanel.tsx",
    "chars": 2929,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { Button } from \"@/components/ui"
  },
  {
    "path": "tauri/src/components/media-tools/panels/ExtractFramesPanel.tsx",
    "chars": 2713,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { Button } from \"@/components/ui"
  },
  {
    "path": "tauri/src/components/media-tools/panels/TrimPanel.tsx",
    "chars": 3087,
    "preview": "import { useState, useEffect } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { Button } from \"@/co"
  },
  {
    "path": "tauri/src/components/media-tools/panels/index.ts",
    "chars": 310,
    "preview": "export { ConvertPanel } from \"./ConvertPanel\";\nexport { CompressPanel } from \"./CompressPanel\";\nexport { TrimPanel } fro"
  },
  {
    "path": "tauri/src/components/media-tools/types.ts",
    "chars": 2161,
    "preview": "export interface MediaInfo {\n  filename: string;\n  format_name: string;\n  format_long_name: string;\n  duration: number |"
  },
  {
    "path": "tauri/src/components/pdf-tools/PDFToolsPage.tsx",
    "chars": 4538,
    "preview": "import { useState, useEffect } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { useTranslation } fr"
  },
  {
    "path": "tauri/src/components/pdf-tools/panels/DeletePagesPanel.tsx",
    "chars": 5098,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { open } from \"@tauri-apps/plugi"
  },
  {
    "path": "tauri/src/components/pdf-tools/panels/ImagesToPdfPanel.tsx",
    "chars": 5204,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { open } from \"@tauri-apps/plugi"
  },
  {
    "path": "tauri/src/components/pdf-tools/panels/Md2PdfPanel.tsx",
    "chars": 7199,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { open } from \"@tauri-apps/plugi"
  },
  {
    "path": "tauri/src/components/pdf-tools/panels/MergePdfPanel.tsx",
    "chars": 5035,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { open } from \"@tauri-apps/plugi"
  },
  {
    "path": "tauri/src/components/pdf-tools/panels/RemoveWatermarkPanel.tsx",
    "chars": 4016,
    "preview": "import { useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { open } from \"@tauri-apps/plugi"
  },
  {
    "path": "tauri/src/components/pdf-tools/panels/index.ts",
    "chars": 267,
    "preview": "export { MergePdfPanel } from \"./MergePdfPanel\";\nexport { ImagesToPdfPanel } from \"./ImagesToPdfPanel\";\nexport { DeleteP"
  },
  {
    "path": "tauri/src/components/pdf-tools/types.ts",
    "chars": 975,
    "preview": "export interface PdfInfo {\n  path: string;\n  pages: number;\n  title: string | null;\n  author: string | null;\n}\n\nexport i"
  },
  {
    "path": "tauri/src/components/settings/AboutSettings.tsx",
    "chars": 3975,
    "preview": "import { useState } from \"react\";\nimport { check } from \"@tauri-apps/plugin-updater\";\nimport { relaunch } from \"@tauri-a"
  },
  {
    "path": "tauri/src/components/settings/GeneralSettings.tsx",
    "chars": 5967,
    "preview": "import { useEffect } from \"react\";\nimport { open } from \"@tauri-apps/plugin-dialog\";\nimport { useTranslation } from \"rea"
  },
  {
    "path": "tauri/src/components/settings/SettingsPage.tsx",
    "chars": 5726,
    "preview": "import { useEffect, useState } from \"react\";\nimport { invoke } from \"@tauri-apps/api/core\";\nimport { useTranslation } fr"
  },
  {
    "path": "tauri/src/components/settings/SiteSettings.tsx",
    "chars": 21453,
    "preview": "import { useState, useEffect, useRef, useCallback } from \"react\";\nimport { QRCodeSVG } from \"qrcode.react\";\nimport { use"
  },
  {
    "path": "tauri/src/components/settings/index.ts",
    "chars": 235,
    "preview": "export { SettingsPage } from \"./SettingsPage\";\nexport { GeneralSettings } from \"./GeneralSettings\";\nexport { SiteSetting"
  },
  {
    "path": "tauri/src/components/settings/types.ts",
    "chars": 714,
    "preview": "export interface WebDAVServer {\n  url: string;\n  username: string;\n  password: string;\n}\n\nexport interface TwitterConfig"
  },
  {
    "path": "tauri/src/components/ui/accordion.tsx",
    "chars": 2039,
    "preview": "import * as React from \"react\"\nimport * as AccordionPrimitive from \"@radix-ui/react-accordion\"\nimport { ChevronDownIcon "
  },
  {
    "path": "tauri/src/components/ui/alert-dialog.tsx",
    "chars": 3850,
    "preview": "import * as React from \"react\"\nimport * as AlertDialogPrimitive from \"@radix-ui/react-alert-dialog\"\n\nimport { cn } from "
  },
  {
    "path": "tauri/src/components/ui/alert.tsx",
    "chars": 1614,
    "preview": "import * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/"
  },
  {
    "path": "tauri/src/components/ui/aspect-ratio.tsx",
    "chars": 280,
    "preview": "\"use client\"\n\nimport * as AspectRatioPrimitive from \"@radix-ui/react-aspect-ratio\"\n\nfunction AspectRatio({\n  ...props\n}:"
  },
  {
    "path": "tauri/src/components/ui/avatar.tsx",
    "chars": 1083,
    "preview": "import * as React from \"react\"\nimport * as AvatarPrimitive from \"@radix-ui/react-avatar\"\n\nimport { cn } from \"@/lib/util"
  },
  {
    "path": "tauri/src/components/ui/badge.tsx",
    "chars": 1633,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "tauri/src/components/ui/breadcrumb.tsx",
    "chars": 2357,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { ChevronRight, MoreHorizontal } from "
  },
  {
    "path": "tauri/src/components/ui/button-group.tsx",
    "chars": 2209,
    "preview": "import { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { "
  },
  {
    "path": "tauri/src/components/ui/button.tsx",
    "chars": 2218,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "tauri/src/components/ui/calendar.tsx",
    "chars": 7793,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport {\n  ChevronDownIcon,\n  ChevronLeftIcon,\n  ChevronRightIcon,\n} from \""
  },
  {
    "path": "tauri/src/components/ui/card.tsx",
    "chars": 1987,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Card({ className, ...props }: React.Component"
  },
  {
    "path": "tauri/src/components/ui/carousel.tsx",
    "chars": 5542,
    "preview": "import * as React from \"react\"\nimport useEmblaCarousel, {\n  type UseEmblaCarouselType,\n} from \"embla-carousel-react\"\nimp"
  },
  {
    "path": "tauri/src/components/ui/chart.tsx",
    "chars": 10069,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RechartsPrimitive from \"recharts\"\n\nimport { cn } from \"@/lib/ut"
  },
  {
    "path": "tauri/src/components/ui/checkbox.tsx",
    "chars": 1219,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as CheckboxPrimitive from \"@radix-ui/react-checkbox\"\nimport { Chec"
  },
  {
    "path": "tauri/src/components/ui/collapsible.tsx",
    "chars": 786,
    "preview": "import * as CollapsiblePrimitive from \"@radix-ui/react-collapsible\"\n\nfunction Collapsible({\n  ...props\n}: React.Componen"
  },
  {
    "path": "tauri/src/components/ui/command.tsx",
    "chars": 4804,
    "preview": "import * as React from \"react\"\nimport { Command as CommandPrimitive } from \"cmdk\"\nimport { SearchIcon } from \"lucide-rea"
  },
  {
    "path": "tauri/src/components/ui/context-menu.tsx",
    "chars": 8274,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ContextMenuPrimitive from \"@radix-ui/react-context-menu\"\nimport"
  },
  {
    "path": "tauri/src/components/ui/dialog.tsx",
    "chars": 3981,
    "preview": "import * as React from \"react\"\nimport * as DialogPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-r"
  },
  {
    "path": "tauri/src/components/ui/drawer.tsx",
    "chars": 4255,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { Drawer as DrawerPrimitive } from \"vaul\"\n\nimport { cn } from \"@/lib"
  },
  {
    "path": "tauri/src/components/ui/dropdown-menu.tsx",
    "chars": 8410,
    "preview": "import * as React from \"react\"\nimport * as DropdownMenuPrimitive from \"@radix-ui/react-dropdown-menu\"\nimport { CheckIcon"
  },
  {
    "path": "tauri/src/components/ui/empty.tsx",
    "chars": 2396,
    "preview": "import { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Empty({ cl"
  },
  {
    "path": "tauri/src/components/ui/field.tsx",
    "chars": 6145,
    "preview": "import { useMemo } from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport { cn } from \"@"
  },
  {
    "path": "tauri/src/components/ui/file-drop-input.tsx",
    "chars": 3328,
    "preview": "import { Button } from \"./button\";\nimport { Upload, FileText, X } from \"lucide-react\";\nimport { cn } from \"@/lib/utils\";"
  },
  {
    "path": "tauri/src/components/ui/form.tsx",
    "chars": 3764,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport type * as LabelPrimitive from \"@radix-ui/react-label\"\nimport { Slot "
  },
  {
    "path": "tauri/src/components/ui/hover-card.tsx",
    "chars": 1532,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as HoverCardPrimitive from \"@radix-ui/react-hover-card\"\n\nimport { "
  },
  {
    "path": "tauri/src/components/ui/input-group.tsx",
    "chars": 5065,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { cva, type VariantProps } from \"class-variance-authority\"\n\nimport {"
  },
  {
    "path": "tauri/src/components/ui/input-otp.tsx",
    "chars": 2240,
    "preview": "import * as React from \"react\"\nimport { OTPInput, OTPInputContext } from \"input-otp\"\nimport { MinusIcon } from \"lucide-r"
  },
  {
    "path": "tauri/src/components/ui/input.tsx",
    "chars": 962,
    "preview": "import * as React from \"react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Input({ className, type, ...props }: React.Co"
  },
  {
    "path": "tauri/src/components/ui/item.tsx",
    "chars": 4494,
    "preview": "import * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps } from \"class"
  },
  {
    "path": "tauri/src/components/ui/kbd.tsx",
    "chars": 862,
    "preview": "import { cn } from \"@/lib/utils\"\n\nfunction Kbd({ className, ...props }: React.ComponentProps<\"kbd\">) {\n  return (\n    <k"
  },
  {
    "path": "tauri/src/components/ui/label.tsx",
    "chars": 611,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as LabelPrimitive from \"@radix-ui/react-label\"\n\nimport { cn } from"
  },
  {
    "path": "tauri/src/components/ui/menubar.tsx",
    "chars": 8380,
    "preview": "import * as React from \"react\"\nimport * as MenubarPrimitive from \"@radix-ui/react-menubar\"\nimport { CheckIcon, ChevronRi"
  },
  {
    "path": "tauri/src/components/ui/navigation-menu.tsx",
    "chars": 6664,
    "preview": "import * as React from \"react\"\nimport * as NavigationMenuPrimitive from \"@radix-ui/react-navigation-menu\"\nimport { cva }"
  },
  {
    "path": "tauri/src/components/ui/pagination.tsx",
    "chars": 2717,
    "preview": "import * as React from \"react\"\nimport {\n  ChevronLeftIcon,\n  ChevronRightIcon,\n  MoreHorizontalIcon,\n} from \"lucide-reac"
  },
  {
    "path": "tauri/src/components/ui/popover.tsx",
    "chars": 1635,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as PopoverPrimitive from \"@radix-ui/react-popover\"\n\nimport { cn } "
  },
  {
    "path": "tauri/src/components/ui/progress.tsx",
    "chars": 726,
    "preview": "import * as React from \"react\"\nimport * as ProgressPrimitive from \"@radix-ui/react-progress\"\n\nimport { cn } from \"@/lib/"
  },
  {
    "path": "tauri/src/components/ui/radio-group.tsx",
    "chars": 1466,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as RadioGroupPrimitive from \"@radix-ui/react-radio-group\"\nimport {"
  },
  {
    "path": "tauri/src/components/ui/resizable.tsx",
    "chars": 1851,
    "preview": "import * as React from \"react\"\nimport { GripVerticalIcon } from \"lucide-react\"\nimport { Group, Panel, Separator } from \""
  },
  {
    "path": "tauri/src/components/ui/scroll-area.tsx",
    "chars": 1645,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as ScrollAreaPrimitive from \"@radix-ui/react-scroll-area\"\n\nimport "
  },
  {
    "path": "tauri/src/components/ui/select.tsx",
    "chars": 6344,
    "preview": "import * as React from \"react\"\nimport * as SelectPrimitive from \"@radix-ui/react-select\"\nimport { CheckIcon, ChevronDown"
  },
  {
    "path": "tauri/src/components/ui/separator.tsx",
    "chars": 699,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SeparatorPrimitive from \"@radix-ui/react-separator\"\n\nimport { c"
  },
  {
    "path": "tauri/src/components/ui/sheet.tsx",
    "chars": 4076,
    "preview": "import * as React from \"react\"\nimport * as SheetPrimitive from \"@radix-ui/react-dialog\"\nimport { XIcon } from \"lucide-re"
  },
  {
    "path": "tauri/src/components/ui/sidebar.tsx",
    "chars": 21638,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport { Slot } from \"@radix-ui/react-slot\"\nimport { cva, type VariantProps"
  },
  {
    "path": "tauri/src/components/ui/skeleton.tsx",
    "chars": 276,
    "preview": "import { cn } from \"@/lib/utils\"\n\nfunction Skeleton({ className, ...props }: React.ComponentProps<\"div\">) {\n  return (\n "
  },
  {
    "path": "tauri/src/components/ui/slider.tsx",
    "chars": 1996,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SliderPrimitive from \"@radix-ui/react-slider\"\n\nimport { cn } fr"
  },
  {
    "path": "tauri/src/components/ui/sonner.tsx",
    "chars": 1020,
    "preview": "import {\n  CircleCheckIcon,\n  InfoIcon,\n  Loader2Icon,\n  OctagonXIcon,\n  TriangleAlertIcon,\n} from \"lucide-react\"\nimport"
  },
  {
    "path": "tauri/src/components/ui/spinner.tsx",
    "chars": 331,
    "preview": "import { Loader2Icon } from \"lucide-react\"\n\nimport { cn } from \"@/lib/utils\"\n\nfunction Spinner({ className, ...props }: "
  },
  {
    "path": "tauri/src/components/ui/switch.tsx",
    "chars": 1177,
    "preview": "\"use client\"\n\nimport * as React from \"react\"\nimport * as SwitchPrimitive from \"@radix-ui/react-switch\"\n\nimport { cn } fr"
  }
]

// ... and 104 more files (download for full content)

About this extraction

This page contains the full source code of the guiyumin/vget GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 304 files (1.8 MB), approximately 487.8k tokens, and a symbol index with 1622 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!