Full Code of nexmoe/VidBee for AI

main e9d7a99e1bd4 cached
366 files
1.6 MB
439.2k tokens
730 symbols
1 requests
Download .txt
Showing preview only (1,725K chars total). Download the full file or copy to clipboard to get everything.
Repository: nexmoe/VidBee
Branch: main
Commit: e9d7a99e1bd4
Files: 366
Total size: 1.6 MB

Directory structure:
gitextract_plngy_re/

├── .agents/
│   └── skills/
│       ├── orpc-contract-first/
│       │   └── SKILL.md
│       └── release-skills/
│           └── SKILL.md
├── .claude/
│   └── CLAUDE.md
├── .cursor/
│   └── hooks.json
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── build.yml
│       ├── ci.yml
│       ├── docker-publish.yml
│       ├── extension-build.yml
│       ├── extension-publish.yml
│       ├── release.yml
│       ├── translator.yaml
│       └── ytdlp-auto-release.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── AGENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps/
│   ├── api/
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   ├── lib/
│   │   │   │   ├── database-migrate.ts
│   │   │   │   ├── downloader.ts
│   │   │   │   ├── history-record-mapper.ts
│   │   │   │   ├── history-store.ts
│   │   │   │   ├── rpc-router.ts
│   │   │   │   ├── sse.ts
│   │   │   │   └── web-settings-store.ts
│   │   │   └── server.ts
│   │   └── tsconfig.json
│   ├── desktop/
│   │   ├── build/
│   │   │   ├── after-pack.cjs
│   │   │   ├── entitlements.mac.plist
│   │   │   └── icon.icns
│   │   ├── changelogs/
│   │   │   ├── CHANGELOG.fr.md
│   │   │   ├── CHANGELOG.md
│   │   │   ├── CHANGELOG.ru.md
│   │   │   └── CHANGELOG.zh.md
│   │   ├── components.json
│   │   ├── dev-app-update.yml
│   │   ├── drizzle.config.ts
│   │   ├── electron-builder.yml
│   │   ├── electron.vite.config.ts
│   │   ├── package.json
│   │   ├── release-metadata.json
│   │   ├── resources/
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   └── drizzle/
│   │   │       ├── 0000_swift_aaron_stack.sql
│   │   │       ├── 0001_smiling_agent_zero.sql
│   │   │       ├── 0002_smooth_impossible_man.sql
│   │   │       └── meta/
│   │   │           ├── 0000_snapshot.json
│   │   │           ├── 0001_snapshot.json
│   │   │           ├── 0002_snapshot.json
│   │   │           └── _journal.json
│   │   ├── scripts/
│   │   │   ├── check-locales.js
│   │   │   ├── check-ytdlp.js
│   │   │   ├── ensure-native-deps.mjs
│   │   │   ├── postinstall.mjs
│   │   │   ├── set-console-encoding.js
│   │   │   ├── setup-dev-binaries.js
│   │   │   └── ytdlp-auto-release.mjs
│   │   ├── src/
│   │   │   ├── main/
│   │   │   │   ├── assets.d.ts
│   │   │   │   ├── config/
│   │   │   │   │   └── logger-config.ts
│   │   │   │   ├── download-engine/
│   │   │   │   │   ├── args-builder.ts
│   │   │   │   │   └── format-utils.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── ipc/
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── services/
│   │   │   │   │       ├── app-service.ts
│   │   │   │   │       ├── browser-cookies-service.ts
│   │   │   │   │       ├── download-service.ts
│   │   │   │   │       ├── file-system-service.ts
│   │   │   │   │       ├── history-service.ts
│   │   │   │   │       ├── settings-service.ts
│   │   │   │   │       ├── subscription-service.ts
│   │   │   │   │       ├── thumbnail-service.ts
│   │   │   │   │       ├── update-service.ts
│   │   │   │   │       └── window-service.ts
│   │   │   │   ├── lib/
│   │   │   │   │   ├── command-utils.ts
│   │   │   │   │   ├── database/
│   │   │   │   │   │   ├── migrate.ts
│   │   │   │   │   │   └── schema.ts
│   │   │   │   │   ├── database-path.ts
│   │   │   │   │   ├── database.ts
│   │   │   │   │   ├── download-engine.ts
│   │   │   │   │   ├── download-queue.ts
│   │   │   │   │   ├── download-session-store.ts
│   │   │   │   │   ├── ffmpeg-manager.ts
│   │   │   │   │   ├── history-manager.ts
│   │   │   │   │   ├── path-resolver.ts
│   │   │   │   │   ├── progress-utils.ts
│   │   │   │   │   ├── subscription-manager.ts
│   │   │   │   │   ├── subscription-scheduler.ts
│   │   │   │   │   ├── thumbnail-cache.ts
│   │   │   │   │   ├── watermark-utils.ts
│   │   │   │   │   ├── youtube-extractor-args.ts
│   │   │   │   │   └── ytdlp-manager.ts
│   │   │   │   ├── local-api.ts
│   │   │   │   ├── settings.ts
│   │   │   │   ├── tray.ts
│   │   │   │   └── utils/
│   │   │   │       ├── auto-launch.ts
│   │   │   │       ├── dock.ts
│   │   │   │       ├── logger.ts
│   │   │   │       └── path-helpers.ts
│   │   │   ├── preload/
│   │   │   │   ├── index.d.ts
│   │   │   │   └── index.ts
│   │   │   ├── renderer/
│   │   │   │   ├── index.html
│   │   │   │   └── src/
│   │   │   │       ├── App.tsx
│   │   │   │       ├── assets/
│   │   │   │       │   ├── global.css
│   │   │   │       │   ├── main.css
│   │   │   │       │   ├── theme.css
│   │   │   │       │   └── title-bar.css
│   │   │   │       ├── components/
│   │   │   │       │   ├── download/
│   │   │   │       │   │   ├── DownloadDialog.tsx
│   │   │   │       │   │   ├── DownloadItem.tsx
│   │   │   │       │   │   ├── PlaylistDownload.tsx
│   │   │   │       │   │   ├── PlaylistDownloadGroup.tsx
│   │   │   │       │   │   ├── SingleVideoDownload.tsx
│   │   │   │       │   │   └── UnifiedDownloadHistory.tsx
│   │   │   │       │   ├── error/
│   │   │   │       │   │   ├── ErrorBoundary.tsx
│   │   │   │       │   │   └── ErrorPage.tsx
│   │   │   │       │   ├── feedback/
│   │   │   │       │   │   └── FeedbackLinks.tsx
│   │   │   │       │   ├── playlist/
│   │   │   │       │   │   └── PlaylistPreviewCard.tsx
│   │   │   │       │   ├── subscription/
│   │   │   │       │   │   └── SubscriptionFormDialog.tsx
│   │   │   │       │   ├── ui/
│   │   │   │       │   │   ├── accordion.tsx
│   │   │   │       │   │   ├── add-url-popover.tsx
│   │   │   │       │   │   ├── badge.tsx
│   │   │   │       │   │   ├── button.tsx
│   │   │   │       │   │   ├── card.tsx
│   │   │   │       │   │   ├── checkbox.tsx
│   │   │   │       │   │   ├── context-menu.tsx
│   │   │   │       │   │   ├── dialog.tsx
│   │   │   │       │   │   ├── download-dialog-layout.tsx
│   │   │   │       │   │   ├── dropdown-menu.tsx
│   │   │   │       │   │   ├── hover-card.tsx
│   │   │   │       │   │   ├── image-with-placeholder.tsx
│   │   │   │       │   │   ├── input.tsx
│   │   │   │       │   │   ├── item.tsx
│   │   │   │       │   │   ├── label.tsx
│   │   │   │       │   │   ├── popover.tsx
│   │   │   │       │   │   ├── progress.tsx
│   │   │   │       │   │   ├── radio-group.tsx
│   │   │   │       │   │   ├── remote-image.tsx
│   │   │   │       │   │   ├── scroll-area.tsx
│   │   │   │       │   │   ├── select.tsx
│   │   │   │       │   │   ├── separator.tsx
│   │   │   │       │   │   ├── sheet.tsx
│   │   │   │       │   │   ├── sidebar.tsx
│   │   │   │       │   │   ├── sonner.tsx
│   │   │   │       │   │   ├── switch.tsx
│   │   │   │       │   │   ├── table.tsx
│   │   │   │       │   │   ├── tabs.tsx
│   │   │   │       │   │   ├── textarea.tsx
│   │   │   │       │   │   ├── title-bar.tsx
│   │   │   │       │   │   └── tooltip.tsx
│   │   │   │       │   └── video/
│   │   │   │       │       └── AdvancedOptions.tsx
│   │   │   │       ├── data/
│   │   │   │       │   └── popularSites.ts
│   │   │   │       ├── env.d.ts
│   │   │   │       ├── hooks/
│   │   │   │       │   ├── use-cached-thumbnail.ts
│   │   │   │       │   ├── use-download-events.ts
│   │   │   │       │   ├── use-history-sync.ts
│   │   │   │       │   └── use-ipc-example.ts
│   │   │   │       ├── i18n.ts
│   │   │   │       ├── lib/
│   │   │   │       │   ├── ipc.ts
│   │   │   │       │   ├── logger.ts
│   │   │   │       │   └── utils.ts
│   │   │   │       ├── main.tsx
│   │   │   │       ├── pages/
│   │   │   │       │   ├── About.tsx
│   │   │   │       │   ├── Home.tsx
│   │   │   │       │   ├── Settings.tsx
│   │   │   │       │   └── Subscriptions.tsx
│   │   │   │       └── store/
│   │   │   │           ├── downloads.ts
│   │   │   │           ├── settings.ts
│   │   │   │           ├── subscriptions.ts
│   │   │   │           ├── update.ts
│   │   │   │           └── video.ts
│   │   │   └── shared/
│   │   │       ├── constants.ts
│   │   │       ├── types/
│   │   │       │   ├── index.ts
│   │   │       │   └── ipc.ts
│   │   │       └── utils/
│   │   │           ├── download-file.ts
│   │   │           └── format-preferences.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── tsconfig.web.json
│   ├── docs/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── biome.config.json
│   │   ├── content/
│   │   │   ├── cookies.mdx
│   │   │   ├── faq.mdx
│   │   │   ├── fr/
│   │   │   │   ├── cookies.mdx
│   │   │   │   ├── faq.mdx
│   │   │   │   ├── index.mdx
│   │   │   │   ├── meta.json
│   │   │   │   ├── protocol.mdx
│   │   │   │   └── rss.mdx
│   │   │   ├── index.mdx
│   │   │   ├── meta.json
│   │   │   ├── protocol.mdx
│   │   │   ├── rss.mdx
│   │   │   ├── ru/
│   │   │   │   ├── cookies.mdx
│   │   │   │   ├── faq.mdx
│   │   │   │   ├── index.mdx
│   │   │   │   ├── meta.json
│   │   │   │   ├── protocol.mdx
│   │   │   │   └── rss.mdx
│   │   │   └── zh/
│   │   │       ├── cookies.mdx
│   │   │       ├── faq.mdx
│   │   │       ├── index.mdx
│   │   │       ├── meta.json
│   │   │       ├── protocol.mdx
│   │   │       └── rss.mdx
│   │   ├── next.config.mjs
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── public/
│   │   │   └── ICONS.md
│   │   ├── scripts/
│   │   │   └── post-export.js
│   │   ├── source.config.ts
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── (docs)/
│   │   │   │   │   ├── [[...slug]]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── layout.tsx
│   │   │   │   ├── [lang]/
│   │   │   │   │   ├── (docs)/
│   │   │   │   │   │   ├── [[...slug]]/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   └── layout.tsx
│   │   │   │   │   └── layout.tsx
│   │   │   │   ├── api/
│   │   │   │   │   └── search/
│   │   │   │   │       └── route.ts
│   │   │   │   ├── global.css
│   │   │   │   ├── layout.tsx
│   │   │   │   └── sitemap.ts
│   │   │   ├── components/
│   │   │   │   └── ai/
│   │   │   │       └── page-actions.tsx
│   │   │   ├── lib/
│   │   │   │   ├── cn.ts
│   │   │   │   ├── i18n.ts
│   │   │   │   ├── layout.shared.tsx
│   │   │   │   └── source.ts
│   │   │   ├── mdx-components.tsx
│   │   │   └── middleware.ts
│   │   └── tsconfig.json
│   ├── extension/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── assets/
│   │   │   └── content.css
│   │   ├── entrypoints/
│   │   │   ├── background.ts
│   │   │   └── popup/
│   │   │       ├── App.css
│   │   │       ├── App.tsx
│   │   │       ├── index.html
│   │   │       └── main.tsx
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── _locales/
│   │   │       └── en/
│   │   │           └── messages.json
│   │   ├── tsconfig.json
│   │   └── wxt.config.ts
│   └── web/
│       ├── Dockerfile
│       ├── README.md
│       ├── biome.json
│       ├── package.json
│       ├── public/
│       │   ├── manifest.json
│       │   └── robots.txt
│       ├── src/
│       │   ├── components/
│       │   │   ├── download/
│       │   │   │   ├── download-dialog.tsx
│       │   │   │   ├── download-item.tsx
│       │   │   │   ├── playlist-download-group.tsx
│       │   │   │   ├── playlist-download.tsx
│       │   │   │   ├── single-video-download.tsx
│       │   │   │   └── types.ts
│       │   │   ├── layout/
│       │   │   │   └── app-shell.tsx
│       │   │   └── pages/
│       │   │       ├── about-page.tsx
│       │   │       ├── download-page.tsx
│       │   │       └── settings-page.tsx
│       │   ├── env.d.ts
│       │   ├── hooks/
│       │   │   ├── use-web-download-settings.ts
│       │   │   └── use-web-settings.ts
│       │   ├── lib/
│       │   │   ├── download-format-preferences.ts
│       │   │   ├── i18n.ts
│       │   │   ├── orpc-client.ts
│       │   │   ├── orpc-download-settings.ts
│       │   │   ├── remote-image-proxy.ts
│       │   │   └── web-settings.ts
│       │   ├── routeTree.gen.ts
│       │   ├── router.tsx
│       │   ├── routes/
│       │   │   ├── __root.tsx
│       │   │   ├── about.tsx
│       │   │   ├── index.tsx
│       │   │   └── settings.tsx
│       │   └── styles.css
│       ├── tsconfig.json
│       └── vite.config.ts
├── biome.json
├── conductor.json
├── docker-compose.yml
├── package.json
├── packages/
│   ├── db/
│   │   ├── package.json
│   │   ├── src/
│   │   │   └── history.ts
│   │   └── tsconfig.json
│   ├── downloader-core/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── browser-cookies-setting.ts
│   │   │   ├── contract.ts
│   │   │   ├── download-file.ts
│   │   │   ├── downloader-core.ts
│   │   │   ├── format-preferences.ts
│   │   │   ├── index.ts
│   │   │   ├── schemas.ts
│   │   │   ├── types.ts
│   │   │   └── yt-dlp-args.ts
│   │   └── tsconfig.json
│   ├── i18n/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   ├── init.ts
│   │   │   ├── languages.ts
│   │   │   ├── locales/
│   │   │   │   ├── ar.json
│   │   │   │   ├── de.json
│   │   │   │   ├── en.json
│   │   │   │   ├── es.json
│   │   │   │   ├── fr.json
│   │   │   │   ├── id.json
│   │   │   │   ├── it.json
│   │   │   │   ├── ja.json
│   │   │   │   ├── ko.json
│   │   │   │   ├── pt.json
│   │   │   │   ├── ru.json
│   │   │   │   ├── tr.json
│   │   │   │   ├── zh-TW.json
│   │   │   │   └── zh.json
│   │   │   └── resources.ts
│   │   └── tsconfig.json
│   └── ui/
│       ├── package.json
│       ├── src/
│       │   ├── base.css
│       │   ├── components/
│       │   │   └── ui/
│       │   │       ├── accordion.tsx
│       │   │       ├── add-url-popover.tsx
│       │   │       ├── app-sidebar-icons.tsx
│       │   │       ├── app-sidebar.tsx
│       │   │       ├── badge.tsx
│       │   │       ├── button.tsx
│       │   │       ├── card.tsx
│       │   │       ├── checkbox.tsx
│       │   │       ├── context-menu.tsx
│       │   │       ├── dialog.tsx
│       │   │       ├── download-dialog-layout.tsx
│       │   │       ├── download-empty-state.tsx
│       │   │       ├── download-filter-bar.tsx
│       │   │       ├── dropdown-menu.tsx
│       │   │       ├── feedback-link-buttons.tsx
│       │   │       ├── hover-card.tsx
│       │   │       ├── image-with-placeholder.tsx
│       │   │       ├── input.tsx
│       │   │       ├── item.tsx
│       │   │       ├── label.tsx
│       │   │       ├── popover.tsx
│       │   │       ├── progress.tsx
│       │   │       ├── radio-group.tsx
│       │   │       ├── remote-image.tsx
│       │   │       ├── scroll-area.tsx
│       │   │       ├── select.tsx
│       │   │       ├── separator.tsx
│       │   │       ├── sheet.tsx
│       │   │       ├── sidebar.tsx
│       │   │       ├── sonner.tsx
│       │   │       ├── switch.tsx
│       │   │       ├── table.tsx
│       │   │       ├── tabs.tsx
│       │   │       ├── textarea.tsx
│       │   │       ├── title-bar.tsx
│       │   │       └── tooltip.tsx
│       │   ├── lib/
│       │   │   ├── cn.ts
│       │   │   └── use-add-url-interaction.ts
│       │   └── theme.css
│       └── tsconfig.json
├── pnpm-workspace.yaml
└── skills-lock.json

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

================================================
FILE: .agents/skills/orpc-contract-first/SKILL.md
================================================
---
name: orpc-contract-first
description: Guide for implementing oRPC contract-first API patterns in Dify frontend. Triggers when creating new API contracts, adding service endpoints, integrating TanStack Query with typed contracts, or migrating legacy service calls to oRPC. Use for all API layer work in web/contract and web/service directories.
---

# oRPC Contract-First Development

## Project Structure

```
web/contract/
├── base.ts           # Base contract (inputStructure: 'detailed')
├── router.ts         # Router composition & type exports
├── marketplace.ts    # Marketplace contracts
└── console/          # Console contracts by domain
    ├── system.ts
    └── billing.ts
```

## Workflow

1. **Create contract** in `web/contract/console/{domain}.ts`
   - Import `base` from `../base` and `type` from `@orpc/contract`
   - Define route with `path`, `method`, `input`, `output`

2. **Register in router** at `web/contract/router.ts`
   - Import directly from domain file (no barrel files)
   - Nest by API prefix: `billing: { invoices, bindPartnerStack }`

3. **Create hooks** in `web/service/use-{domain}.ts`
   - Use `consoleQuery.{group}.{contract}.queryKey()` for query keys
   - Use `consoleClient.{group}.{contract}()` for API calls

## Key Rules

- **Input structure**: Always use `{ params, query?, body? }` format
- **Path params**: Use `{paramName}` in path, match in `params` object
- **Router nesting**: Group by API prefix (e.g., `/billing/*` → `billing: {}`)
- **No barrel files**: Import directly from specific files
- **Types**: Import from `@/types/`, use `type<T>()` helper

## Type Export

```typescript
export type ConsoleInputs = InferContractRouterInputs<typeof consoleRouterContract>
```


================================================
FILE: .agents/skills/release-skills/SKILL.md
================================================
---
name: release-skills
description: Universal release workflow. Auto-detects version files and changelogs. Supports Node.js, Python, Rust, Claude Plugin, and generic projects. Use when user says "release", "发布", "new version", "bump version", "push", "推送".
---

# Release Skills

Universal release workflow supporting any project type with multi-language changelog.

## Quick Start

Just run `/release-skills` - auto-detects your project configuration.

## Supported Projects

| Project Type | Version File | Auto-Detected |
|--------------|--------------|---------------|
| Node.js | package.json | ✓ |
| Python | pyproject.toml | ✓ |
| Rust | Cargo.toml | ✓ |
| Claude Plugin | marketplace.json | ✓ |
| Generic | VERSION / version.txt | ✓ |

## Options

| Flag | Description |
|------|-------------|
| `--dry-run` | Preview changes without executing |
| `--major` | Force major version bump |
| `--minor` | Force minor version bump |
| `--patch` | Force patch version bump |

## Workflow

### Step 1: Detect Project Configuration

1. Check for `.releaserc.yml` (optional config override)
2. Auto-detect version file by scanning (priority order):
   - `package.json` (Node.js)
   - `pyproject.toml` (Python)
   - `Cargo.toml` (Rust)
   - `marketplace.json` or `.claude-plugin/marketplace.json` (Claude Plugin)
   - `VERSION` or `version.txt` (Generic)
3. Scan for changelog files using glob patterns:
   - `CHANGELOG*.md`
   - `HISTORY*.md`
   - `CHANGES*.md`
4. Identify language of each changelog by filename suffix
5. Display detected configuration

**Language Detection Rules**:

| Filename Pattern | Language |
|------------------|----------|
| `CHANGELOG.md` (no suffix) | en (default) |
| `CHANGELOG.zh.md` / `CHANGELOG_CN.md` / `CHANGELOG.zh-CN.md` | zh |
| `CHANGELOG.ja.md` / `CHANGELOG_JP.md` | ja |
| `CHANGELOG.ko.md` / `CHANGELOG_KR.md` | ko |
| `CHANGELOG.de.md` / `CHANGELOG_DE.md` | de |
| `CHANGELOG.fr.md` / `CHANGELOG_FR.md` | fr |
| `CHANGELOG.es.md` / `CHANGELOG_ES.md` | es |
| `CHANGELOG.{lang}.md` | Corresponding language code |

**Output Example**:
```
Project detected:
  Version file: package.json (1.2.3)
  Changelogs:
    - CHANGELOG.md (en)
    - CHANGELOG.zh.md (zh)
    - CHANGELOG.ja.md (ja)
```

### Step 2: Analyze Changes Since Last Tag

```bash
LAST_TAG=$(git tag --sort=-v:refname | head -1)
git log ${LAST_TAG}..HEAD --oneline
git diff ${LAST_TAG}..HEAD --stat
```

Categorize by conventional commit types:

| Type | Description |
|------|-------------|
| feat | New features |
| fix | Bug fixes |
| docs | Documentation |
| refactor | Code refactoring |
| perf | Performance improvements |
| test | Test changes |
| style | Formatting, styling |
| chore | Maintenance (skip in changelog) |

**Breaking Change Detection**:
- Commit message starts with `BREAKING CHANGE`
- Commit body/footer contains `BREAKING CHANGE:`
- Removed public APIs, renamed exports, changed interfaces

If breaking changes detected, warn user: "Breaking changes detected. Consider major version bump (--major flag)."

### Step 3: Determine Version Bump

Rules (in priority order):
1. User flag `--major/--minor/--patch` → Use specified
2. BREAKING CHANGE detected → Major bump (1.x.x → 2.0.0)
3. `feat:` commits present → Minor bump (1.2.x → 1.3.0)
4. Otherwise → Patch bump (1.2.3 → 1.2.4)

Display version change: `1.2.3 → 1.3.0`

### Step 4: Generate Multi-language Changelogs

For each detected changelog file:

1. **Identify language** from filename suffix
2. **Detect third-party contributors**:
   - Check merge commits: `git log ${LAST_TAG}..HEAD --merges --pretty=format:"%H %s"`
   - For each merged PR, identify the PR author via `gh pr view <number> --json author --jq '.author.login'`
   - Compare against repo owner (`gh repo view --json owner --jq '.owner.login'`)
   - If PR author ≠ repo owner → third-party contributor
3. **Generate content in that language**:
   - Section titles in target language
   - Change descriptions written naturally in target language (not translated)
   - Date format: YYYY-MM-DD (universal)
   - **Third-party contributions**: Append contributor attribution `(by @username)` to the changelog entry
4. **Insert at file head** (preserve existing content)

**Section Title Translations** (built-in):

| Type | en | zh | ja | ko | de | fr | es |
|------|----|----|----|----|----|----|-----|
| feat | Features | 新功能 | 新機能 | 새로운 기능 | Funktionen | Fonctionnalités | Características |
| fix | Fixes | 修复 | 修正 | 수정 | Fehlerbehebungen | Corrections | Correcciones |
| docs | Documentation | 文档 | ドキュメント | 문서 | Dokumentation | Documentation | Documentación |
| refactor | Refactor | 重构 | リファクタリング | 리팩토링 | Refactoring | Refactorisation | Refactorización |
| perf | Performance | 性能优化 | パフォーマンス | 성능 | Leistung | Performance | Rendimiento |
| breaking | Breaking Changes | 破坏性变更 | 破壊的変更 | 주요 변경사항 | Breaking Changes | Changements majeurs | Cambios importantes |

**Changelog Format**:

```markdown
## {VERSION} - {YYYY-MM-DD}

### Features
- Description of new feature
- Description of third-party contribution (by @username)

### Fixes
- Description of fix

### Documentation
- Description of docs changes
```

Only include sections that have changes. Omit empty sections.

**Third-Party Attribution Rules**:
- Only add `(by @username)` for contributors who are NOT the repo owner
- Use GitHub username with `@` prefix
- Place at the end of the changelog entry line
- Apply to all languages consistently (always use `(by @username)` format, not translated)

**Multi-language Example**:

English (CHANGELOG.md):
```markdown
## 1.3.0 - 2026-01-22

### Features
- Add user authentication module (by @contributor1)
- Support OAuth2 login

### Fixes
- Fix memory leak in connection pool
```

Chinese (CHANGELOG.zh.md):
```markdown
## 1.3.0 - 2026-01-22

### 新功能
- 新增用户认证模块 (by @contributor1)
- 支持 OAuth2 登录

### 修复
- 修复连接池内存泄漏问题
```

Japanese (CHANGELOG.ja.md):
```markdown
## 1.3.0 - 2026-01-22

### 新機能
- ユーザー認証モジュールを追加 (by @contributor1)
- OAuth2 ログインをサポート

### 修正
- コネクションプールのメモリリークを修正
```

### Step 5: Group Changes by Skill/Module

Analyze commits since last tag and group by affected skill/module:

1. **Identify changed files** per commit
2. **Group by skill/module**:
   - `skills/<skill-name>/*` → Group under that skill
   - Root files (CLAUDE.md, etc.) → Group as "project"
   - Multiple skills in one commit → Split into multiple groups
3. **For each group**, identify related README updates needed

**Example Grouping**:
```
baoyu-cover-image:
  - feat: add new style options
  - fix: handle transparent backgrounds
  → README updates: options table

baoyu-comic:
  - refactor: improve panel layout algorithm
  → No README updates needed

project:
  - docs: update CLAUDE.md architecture section
```

### Step 6: Commit Each Skill/Module Separately

For each skill/module group (in order of changes):

1. **Check README updates needed**:
   - Scan `README*.md` for mentions of this skill/module
   - Verify options/flags documented correctly
   - Update usage examples if syntax changed
   - Update feature descriptions if behavior changed

2. **Stage and commit**:
   ```bash
   git add skills/<skill-name>/*
   git add README.md README.zh.md  # If updated for this skill
   git commit -m "<type>(<skill-name>): <meaningful description>"
   ```

3. **Commit message format**:
   - Use conventional commit format: `<type>(<scope>): <description>`
   - `<type>`: feat, fix, refactor, docs, perf, etc.
   - `<scope>`: skill name or "project"
   - `<description>`: Clear, meaningful description of changes

**Example Commits**:
```bash
git commit -m "feat(baoyu-cover-image): add watercolor and minimalist styles"
git commit -m "fix(baoyu-comic): improve panel layout for long dialogues"
git commit -m "docs(project): update architecture documentation"
```

**Common README Updates Needed**:
| Change Type | README Section to Check |
|-------------|------------------------|
| New options/flags | Options table, usage examples |
| Renamed options | Options table, usage examples |
| New features | Feature description, examples |
| Breaking changes | Migration notes, deprecation warnings |
| Restructured internals | Architecture section (if exposed to users) |

### Step 7: Generate Changelog and Update Version

1. **Generate multi-language changelogs** (as described in Step 4)
2. **Update version file**:
   - Read version file (JSON/TOML/text)
   - Update version number
   - Write back (preserve formatting)

**Version Paths by File Type**:

| File | Path |
|------|------|
| package.json | `$.version` |
| pyproject.toml | `project.version` |
| Cargo.toml | `package.version` |
| marketplace.json | `$.metadata.version` |
| VERSION / version.txt | Direct content |

### Step 8: User Confirmation

Before creating the release commit, ask user to confirm:

**Use AskUserQuestion with two questions**:

1. **Version bump** (single select):
   - Show recommended version based on Step 3 analysis
   - Options: recommended (with label), other semver options
   - Example: `1.2.3 → 1.3.0 (Recommended)`, `1.2.3 → 1.2.4`, `1.2.3 → 2.0.0`

2. **Push to remote** (single select):
   - Options: "Yes, push after commit", "No, keep local only"

**Example Output Before Confirmation**:
```
Commits created:
  1. feat(baoyu-cover-image): add watercolor and minimalist styles
  2. fix(baoyu-comic): improve panel layout for long dialogues
  3. docs(project): update architecture documentation

Changelog preview (en):
  ## 1.3.0 - 2026-01-22
  ### Features
  - Add watercolor and minimalist styles to cover-image
  ### Fixes
  - Improve panel layout for long dialogues in comic

Ready to create release commit and tag.
```

### Step 9: Create Release Commit and Tag

After user confirmation:

1. **Stage version and changelog files**:
   ```bash
   git add <version-file>
   git add CHANGELOG*.md
   ```

2. **Create release commit**:
   ```bash
   git commit -m "chore: release v{VERSION}"
   ```

3. **Create tag**:
   ```bash
   git tag v{VERSION}
   ```

4. **Push if user confirmed** (Step 8):
   ```bash
   git push origin main
   git push origin v{VERSION}
   ```

**Note**: Do NOT add Co-Authored-By line. This is a release commit, not a code contribution.

**Post-Release Output**:
```
Release v1.3.0 created.

Commits:
  1. feat(baoyu-cover-image): add watercolor and minimalist styles
  2. fix(baoyu-comic): improve panel layout for long dialogues
  3. docs(project): update architecture documentation
  4. chore: release v1.3.0

Tag: v1.3.0
Status: Pushed to origin  # or "Local only - run git push when ready"
```

## Configuration (.releaserc.yml)

Optional config file in project root to override defaults:

```yaml
# .releaserc.yml - Optional configuration

# Version file (auto-detected if not specified)
version:
  file: package.json
  path: $.version  # JSONPath for JSON, dotted path for TOML

# Changelog files (auto-detected if not specified)
changelog:
  files:
    - path: CHANGELOG.md
      lang: en
    - path: CHANGELOG.zh.md
      lang: zh
    - path: CHANGELOG.ja.md
      lang: ja

  # Section mapping (conventional commit type → changelog section)
  # Use null to skip a type in changelog
  sections:
    feat: Features
    fix: Fixes
    docs: Documentation
    refactor: Refactor
    perf: Performance
    test: Tests
    chore: null

# Commit message format
commit:
  message: "chore: release v{version}"

# Tag format
tag:
  prefix: v  # Results in v1.0.0
  sign: false

# Additional files to include in release commit
include:
  - README.md
  - package.json
```

## Dry-Run Mode

When `--dry-run` is specified:

```
=== DRY RUN MODE ===

Project detected:
  Version file: package.json (1.2.3)
  Changelogs: CHANGELOG.md (en), CHANGELOG.zh.md (zh)

Last tag: v1.2.3
Proposed version: v1.3.0

Changes grouped by skill/module:
  baoyu-cover-image:
    - feat: add watercolor style
    - feat: add minimalist style
    → Commit: feat(baoyu-cover-image): add watercolor and minimalist styles
    → README updates: options table

  baoyu-comic:
    - fix: panel layout for long dialogues
    → Commit: fix(baoyu-comic): improve panel layout for long dialogues
    → No README updates

Changelog preview (en):
  ## 1.3.0 - 2026-01-22
  ### Features
  - Add watercolor and minimalist styles to cover-image
  ### Fixes
  - Improve panel layout for long dialogues in comic

Changelog preview (zh):
  ## 1.3.0 - 2026-01-22
  ### 新功能
  - 为 cover-image 添加水彩和极简风格
  ### 修复
  - 改进 comic 长对话的面板布局

Commits to create:
  1. feat(baoyu-cover-image): add watercolor and minimalist styles
  2. fix(baoyu-comic): improve panel layout for long dialogues
  3. chore: release v1.3.0

No changes made. Run without --dry-run to execute.
```

## Example Usage

```
/release-skills              # Auto-detect version bump
/release-skills --dry-run    # Preview only
/release-skills --minor      # Force minor bump
/release-skills --patch      # Force patch bump
/release-skills --major      # Force major bump (with confirmation)
```

## When to Use

Trigger this skill when user requests:
- "release", "发布", "create release", "new version", "新版本"
- "bump version", "update version", "更新版本"
- "prepare release"
- "push to remote" (with uncommitted changes)

**Important**: If user says "just push" or "直接 push" with uncommitted changes, STILL follow all steps above first.


================================================
FILE: .claude/CLAUDE.md
================================================
# Ultracite Code Standards

This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting.

## Quick Reference

- **Format code**: `pnpm dlx ultracite fix`
- **Check for issues**: `pnpm dlx ultracite check`
- **Diagnose setup**: `pnpm dlx ultracite doctor`

Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.

---

## Core Principles

Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity.

### Type Safety & Explicitness

- Use explicit types for function parameters and return values when they enhance clarity
- Prefer `unknown` over `any` when the type is genuinely unknown
- Use const assertions (`as const`) for immutable values and literal types
- Leverage TypeScript's type narrowing instead of type assertions
- Use meaningful variable names instead of magic numbers - extract constants with descriptive names

### Modern JavaScript/TypeScript

- Use arrow functions for callbacks and short functions
- Prefer `for...of` loops over `.forEach()` and indexed `for` loops
- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access
- Prefer template literals over string concatenation
- Use destructuring for object and array assignments
- Use `const` by default, `let` only when reassignment is needed, never `var`

### Async & Promises

- Always `await` promises in async functions - don't forget to use the return value
- Use `async/await` syntax instead of promise chains for better readability
- Handle errors appropriately in async code with try-catch blocks
- Don't use async functions as Promise executors

### React & JSX

- Use function components over class components
- Call hooks at the top level only, never conditionally
- Specify all dependencies in hook dependency arrays correctly
- Use the `key` prop for elements in iterables (prefer unique IDs over array indices)
- Nest children between opening and closing tags instead of passing as props
- Don't define components inside other components
- Use semantic HTML and ARIA attributes for accessibility:
  - Provide meaningful alt text for images
  - Use proper heading hierarchy
  - Add labels for form inputs
  - Include keyboard event handlers alongside mouse events
  - Use semantic elements (`<button>`, `<nav>`, etc.) instead of divs with roles

### Error Handling & Debugging

- Remove `console.log`, `debugger`, and `alert` statements from production code
- Throw `Error` objects with descriptive messages, not strings or other values
- Use `try-catch` blocks meaningfully - don't catch errors just to rethrow them
- Prefer early returns over nested conditionals for error cases

### Code Organization

- Keep functions focused and under reasonable cognitive complexity limits
- Extract complex conditions into well-named boolean variables
- Use early returns to reduce nesting
- Prefer simple conditionals over nested ternary operators
- Group related code together and separate concerns

### Security

- Add `rel="noopener"` when using `target="_blank"` on links
- Avoid `dangerouslySetInnerHTML` unless absolutely necessary
- Don't use `eval()` or assign directly to `document.cookie`
- Validate and sanitize user input

### Performance

- Avoid spread syntax in accumulators within loops
- Use top-level regex literals instead of creating them in loops
- Prefer specific imports over namespace imports
- Avoid barrel files (index files that re-export everything)
- Use proper image components (e.g., Next.js `<Image>`) over `<img>` tags

### Framework-Specific Guidance

**Next.js:**
- Use Next.js `<Image>` component for images
- Use `next/head` or App Router metadata API for head elements
- Use Server Components for async data fetching instead of async Client Components

**React 19+:**
- Use ref as a prop instead of `React.forwardRef`

**Solid/Svelte/Vue/Qwik:**
- Use `class` and `for` attributes (not `className` or `htmlFor`)

---

## Testing

- Write assertions inside `it()` or `test()` blocks
- Avoid done callbacks in async tests - use async/await instead
- Don't use `.only` or `.skip` in committed code
- Keep test suites reasonably flat - avoid excessive `describe` nesting

## When Biome Can't Help

Biome's linter will catch most issues automatically. Focus your attention on:

1. **Business logic correctness** - Biome can't validate your algorithms
2. **Meaningful naming** - Use descriptive names for functions, variables, and types
3. **Architecture decisions** - Component structure, data flow, and API design
4. **Edge cases** - Handle boundary conditions and error states
5. **User experience** - Accessibility, performance, and usability considerations
6. **Documentation** - Add comments for complex logic, but prefer self-documenting code

---

Most formatting and common issues are automatically fixed by Biome. Run `pnpm dlx ultracite fix` before committing to ensure compliance.


================================================
FILE: .cursor/hooks.json
================================================
{
  "version": 1,
  "hooks": {
    "afterFileEdit": [
      {
        "command": "pnpm dlx ultracite fix"
      }
    ]
  }
}


================================================
FILE: .dockerignore
================================================
node_modules
.git
.context
dist
out
build
**/node_modules
**/dist
**/out
**/.turbo


================================================
FILE: .editorconfig
================================================
root = true

[*]
charset = utf-8
indent_style = space
indent_size = 2
end_of_line = lf
insert_final_newline = true
trim_trailing_whitespace = true



================================================
FILE: .gitattributes
================================================
* text=auto eol=lf
*.{cmd,[cC][mM][dD]} text eol=crlf
*.{bat,[bB][aA][tT]} text eol=crlf



================================================
FILE: .github/ISSUE_TEMPLATE/bug_report.yml
================================================
name: Bug Report
description: Report a problem or regression
title: "[Bug]: "
labels:
  - bug
body:
  - type: textarea
    id: actual
    attributes:
      label: Observed behavior and screenshots
      placeholder: What actually happened
    validations:
      required: true
  - type: textarea
    id: logs
    attributes:
      label: Logs or screenshots
      description: Paste relevant logs or add screenshots
      placeholder: Attach files or paste logs here
    validations:
      required: false
  - type: input
    id: app_version
    attributes:
      label: App version
      description: The VidBee version (e.g., 1.2.3)
      placeholder: 1.2.3
    validations:
      required: true
  - type: input
    id: os_version
    attributes:
      label: OS version
      description: Your operating system and version (e.g., macOS 14.2, Windows 11 23H2)
      placeholder: macOS 14.2
    validations:
      required: true


================================================
FILE: .github/ISSUE_TEMPLATE/feature_request.yml
================================================
name: Feature Request
description: Suggest an idea or improvement
title: "[Feature]: "
labels:
  - enhancement
body:
  - type: textarea
    id: problem
    attributes:
      label: Problem to solve
      description: What problem are you trying to solve?
      placeholder: I want to...
    validations:
      required: true
  - type: textarea
    id: proposal
    attributes:
      label: Proposed solution
      description: Describe the feature or change you want
      placeholder: It would be great if...
    validations:
      required: true
  - type: textarea
    id: alternatives
    attributes:
      label: Alternatives considered
      description: Other solutions or workarounds you considered
      placeholder: I tried...
    validations:
      required: false
  - type: textarea
    id: extra
    attributes:
      label: Additional context
      description: Add any other context or screenshots
      placeholder: Links, screenshots, or related issues
    validations:
      required: false


================================================
FILE: .github/workflows/build.yml
================================================
name: Build

on:
  workflow_call:
    inputs:
      upload_artifacts:
        required: false
        type: boolean
        default: false
        description: 'Whether to upload build artifacts'
    secrets:
      MAC_CERT_P12_BASE64:
        required: false
      MAC_CERT_P12_PASSWORD:
        required: false
      APPLE_API_KEY_ID:
        required: false
      APPLE_API_ISSUER:
        required: false
      APPLE_API_KEY_P8_BASE64:
        required: false

jobs:
  build:
    runs-on: ${{ matrix.os }}
    strategy:
      matrix:
        include:
          - platform: windows
            os: windows-latest
            build_script: pnpm run build:win
            mac_ffmpeg_mode: native
          - platform: macos
            os: macos-latest
            build_script: pnpm run build:mac
            mac_ffmpeg_mode: universal
          - platform: linux
            os: ubuntu-latest
            build_script: pnpm run build:linux
            mac_ffmpeg_mode: native
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v4

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 8

      - name: Install Dependencies
        run: pnpm install --filter ./apps/desktop...

      - name: Lint and format check
        run: pnpm run check && pnpm run typecheck

      - name: Setup macOS signing
        if: matrix.platform == 'macos'
        shell: bash
        env:
          MAC_CERT_P12_BASE64: ${{ secrets.MAC_CERT_P12_BASE64 }}
          MAC_CERT_P12_PASSWORD: ${{ secrets.MAC_CERT_P12_PASSWORD }}
          APPLE_API_KEY_P8_BASE64: ${{ secrets.APPLE_API_KEY_P8_BASE64 }}
          APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
          APPLE_API_ISSUER: ${{ secrets.APPLE_API_ISSUER }}
        run: |
          set -euo pipefail
          echo "SIGNING_AVAILABLE=false" >> "$GITHUB_ENV"

          # Check if all required secrets are present
          if [[ -z "$MAC_CERT_P12_BASE64" ]] || [[ -z "$MAC_CERT_P12_PASSWORD" ]] || \
             [[ -z "$APPLE_API_KEY_ID" ]] || [[ -z "$APPLE_API_ISSUER" ]] || \
             [[ -z "$APPLE_API_KEY_P8_BASE64" ]]; then
            echo "::notice::macOS signing secrets not available, skipping code signing setup"
            exit 0
          fi

          CERT_PATH="$RUNNER_TEMP/mac_cert.p12"
          KEYCHAIN_PATH="$RUNNER_TEMP/build.keychain"
          API_KEY_PATH="$RUNNER_TEMP/AuthKey.p8"

          echo "$MAC_CERT_P12_BASE64" | base64 --decode > "$CERT_PATH"
          echo "$APPLE_API_KEY_P8_BASE64" | base64 --decode > "$API_KEY_PATH"

          security create-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
          security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
          security unlock-keychain -p "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"
          security import "$CERT_PATH" -k "$KEYCHAIN_PATH" -P "$MAC_CERT_P12_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productbuild
          security list-keychain -d user -s "$KEYCHAIN_PATH"
          security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$MAC_CERT_P12_PASSWORD" "$KEYCHAIN_PATH"

          echo "CSC_KEYCHAIN=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
          echo "CSC_KEY_PASSWORD=$MAC_CERT_P12_PASSWORD" >> "$GITHUB_ENV"
          echo "APPLE_API_KEY=$API_KEY_PATH" >> "$GITHUB_ENV"
          echo "APPLE_API_KEY_ID=$APPLE_API_KEY_ID" >> "$GITHUB_ENV"
          echo "APPLE_API_ISSUER=$APPLE_API_ISSUER" >> "$GITHUB_ENV"
          echo "SIGNING_AVAILABLE=true" >> "$GITHUB_ENV"

      - name: Build application
        env:
          VIDBEE_MAC_FFMPEG_MODE: ${{ matrix.mac_ffmpeg_mode }}
        run: ${{ matrix.build_script }}

      - name: Verify macOS codesign and notarization
        if: matrix.platform == 'macos' && env.SIGNING_AVAILABLE == 'true'
        shell: bash
        run: |
          set -euo pipefail
          apps_found=0
          while IFS= read -r app; do
            apps_found=1
            echo "Verifying codesign for $app"
            codesign --verify --deep --strict --verbose=2 "$app"
            spctl -a -t exec -vv "$app"
            echo "Validating notarization ticket for $app"
            xcrun stapler validate "$app"
          done < <(find apps/desktop/dist -type d -name "*.app" -prune -print)

          if [[ "$apps_found" -eq 0 ]]; then
            echo "::error::No .app bundles found in dist"
            exit 1
          fi

          dmgs_found=0
          while IFS= read -r dmg; do
            dmgs_found=1
            echo "Submitting DMG for notarization: $dmg"
            xcrun notarytool submit "$dmg" --key "$APPLE_API_KEY" --key-id "$APPLE_API_KEY_ID" --issuer "$APPLE_API_ISSUER" --wait
            echo "Stapling notarization ticket for $dmg"
            xcrun stapler staple "$dmg"
            echo "Validating notarization ticket for $dmg"
            xcrun stapler validate "$dmg"
          done < <(find apps/desktop/dist -type f -name "*.dmg" -print)

          if [[ "$dmgs_found" -eq 0 ]]; then
            echo "::notice::No DMG artifacts found to validate"
          fi

      - name: Upload build artifacts
        if: inputs.upload_artifacts == true
        uses: actions/upload-artifact@v4
        with:
          name: dist-${{ matrix.os }}
          path: |
            apps/desktop/dist/*.exe
            apps/desktop/dist/*.zip
            apps/desktop/dist/*.dmg
            apps/desktop/dist/*.AppImage
            apps/desktop/dist/*.snap
            apps/desktop/dist/*.deb
            apps/desktop/dist/*.rpm
            apps/desktop/dist/*.tar.gz
            apps/desktop/dist/*.yml
            apps/desktop/dist/*.blockmap
          retention-days: 1


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

on:
  pull_request:
    branches: [ main ]

jobs:
  build:
    uses: ./.github/workflows/build.yml
    with:
      upload_artifacts: true


================================================
FILE: .github/workflows/docker-publish.yml
================================================
name: Docker Publish

on:
  push:
    branches:
      - main
    paths:
      - 'apps/api/**'
      - 'apps/desktop/resources/drizzle/**'
      - 'apps/web/**'
      - 'packages/**'
      - 'pnpm-lock.yaml'
      - 'pnpm-workspace.yaml'
      - 'package.json'
      - '.github/workflows/docker-publish.yml'
  workflow_dispatch:

jobs:
  docker:
    runs-on: ubuntu-latest
    strategy:
      fail-fast: false
      matrix:
        include:
          - app: api
            dockerfile: apps/api/Dockerfile
            image_suffix: api
          - app: web
            dockerfile: apps/web/Dockerfile
            image_suffix: web
    permissions:
      contents: read
      packages: write

    steps:
      - name: Check out repository
        uses: actions/checkout@v4

      - name: Set up QEMU
        uses: docker/setup-qemu-action@v3

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

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

      - name: Extract image metadata
        id: meta
        uses: docker/metadata-action@v5
        with:
          images: ghcr.io/${{ github.repository_owner }}/vidbee-${{ matrix.image_suffix }}
          tags: |
            type=raw,value=latest,enable={{is_default_branch}}
            type=sha,format=short

      - name: Build and push image
        uses: docker/build-push-action@v6
        with:
          context: .
          file: ${{ matrix.dockerfile }}
          platforms: linux/amd64,linux/arm64
          push: true
          tags: ${{ steps.meta.outputs.tags }}
          labels: ${{ steps.meta.outputs.labels }}
          build-args: |
            VITE_API_URL=${{ vars.VITE_API_URL || 'http://localhost:3100' }}


================================================
FILE: .github/workflows/extension-build.yml
================================================
name: Build Extension

on:
  workflow_call:
    inputs:
      upload_artifacts:
        required: false
        type: boolean
        default: false
        description: 'Whether to upload build artifacts'

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 8

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
          cache-dependency-path: pnpm-lock.yaml

      - name: Install dependencies
        run: pnpm install --filter ./apps/extension...

      - name: Build extension
        run: pnpm --filter ./apps/extension build

      - name: Build extension zip
        run: pnpm --filter ./apps/extension zip

      - name: Upload extension artifacts
        if: inputs.upload_artifacts == true
        uses: actions/upload-artifact@v4
        with:
          name: dist-extension
          path: apps/extension/.output/*.zip
          retention-days: 1
          if-no-files-found: error


================================================
FILE: .github/workflows/extension-publish.yml
================================================
name: Publish Extension

on:
  push:
    branches: [ main ]
    paths:
      - apps/extension/package.json

jobs:
  detect-version:
    if: vars.ENABLE_EXTENSION_CI == 'true'
    runs-on: ubuntu-latest
    outputs:
      changed: ${{ steps.version_check.outputs.changed }}
      current_version: ${{ steps.version_check.outputs.current_version }}
      previous_version: ${{ steps.version_check.outputs.previous_version }}
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Check if extension version changed
        id: version_check
        shell: bash
        run: |
          set -euo pipefail
          before_sha="${{ github.event.before }}"
          current_version=$(node -e "const fs = require('fs'); const v = JSON.parse(fs.readFileSync('apps/extension/package.json', 'utf8')).version; process.stdout.write(v);")
          previous_version=""
          if git cat-file -e "${before_sha}:apps/extension/package.json" 2>/dev/null; then
            previous_version=$(git show "${before_sha}:apps/extension/package.json" | node -e "let data=''; process.stdin.on('data', d => data += d); process.stdin.on('end', () => { const v = JSON.parse(data).version; process.stdout.write(v); });")
          fi

          echo "current_version=${current_version}" >> "$GITHUB_OUTPUT"
          echo "previous_version=${previous_version}" >> "$GITHUB_OUTPUT"

          if [[ -n "${previous_version}" && "${current_version}" == "${previous_version}" ]]; then
            echo "changed=false" >> "$GITHUB_OUTPUT"
            exit 0
          fi

          echo "changed=true" >> "$GITHUB_OUTPUT"

  submit:
    if: vars.ENABLE_EXTENSION_CI == 'true' && needs.detect-version.outputs.changed == 'true'
    needs: detect-version
    runs-on: ubuntu-latest
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v4

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 8

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 22
          cache: 'pnpm'
          cache-dependency-path: pnpm-lock.yaml

      - name: Install dependencies
        run: pnpm install --filter ./apps/extension...

      - name: Build extension
        run: pnpm --filter ./apps/extension build

      - name: Zip extensions
        run: pnpm --filter ./apps/extension zip

      - name: Submit to stores
        run: |
          pnpm --dir apps/extension wxt submit \
            --chrome-zip .output/*-chrome.zip
        env:
          CHROME_EXTENSION_ID: ${{ secrets.CHROME_EXTENSION_ID }}
          CHROME_CLIENT_ID: ${{ secrets.CHROME_CLIENT_ID }}
          CHROME_CLIENT_SECRET: ${{ secrets.CHROME_CLIENT_SECRET }}
          CHROME_REFRESH_TOKEN: ${{ secrets.CHROME_REFRESH_TOKEN }}
          CHROME_PUBLISH_TARGET: "default"
          CHROME_SKIP_SUBMIT_REVIEW: false


================================================
FILE: .github/workflows/release.yml
================================================
name: Build and Release Electron App

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

jobs:
  build:
    uses: ./.github/workflows/build.yml
    with:
      upload_artifacts: true
    secrets: inherit

  release:
    needs: [build]
    runs-on: ubuntu-latest
    steps:
      - name: Check out Git repository
        uses: actions/checkout@v4

      - name: Detect release type
        id: release_meta
        shell: bash
        run: |
          if [[ "${GITHUB_REF_NAME}" == *-preview.* ]]; then
            echo "is_preview=true" >> "$GITHUB_OUTPUT"
          else
            echo "is_preview=false" >> "$GITHUB_OUTPUT"
          fi

      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          pattern: dist-*
          merge-multiple: true
          path: dist/

      - name: Release
        uses: softprops/action-gh-release@v1
        with:
          generate_release_notes: true
          prerelease: ${{ steps.release_meta.outputs.is_preview == 'true' }}
          files: |
            dist/*.exe
            dist/*.zip
            dist/*.dmg
            dist/*.AppImage
            dist/*.snap
            dist/*.deb
            dist/*.rpm
            dist/*.tar.gz
            dist/*.yml
            dist/*.blockmap
        env:
          GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}

      - name: Notify Cloudflare Pages
        if: steps.release_meta.outputs.is_preview != 'true'
        env:
          CLOUDFLARE_WEBHOOK_URL: ${{ secrets.CLOUDFLARE_WEBHOOK_URL }}
        run: |
          curl -X POST "$CLOUDFLARE_WEBHOOK_URL"


================================================
FILE: .github/workflows/translator.yaml
================================================
name: 'translator'
on:
  issues:
    types: [opened, edited]
  issue_comment:
    types: [created, edited]
  discussion:
    types: [created, edited]
  discussion_comment:
    types: [created, edited]

jobs:
  translate:
    if: ${{ !github.event.issue.pull_request }}
    permissions:
      issues: write
      discussions: write
      pull-requests: write
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: lizheming/github-translate-action@c55aac477e98562d4faed9f77c54ab8306ae6ebf
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          IS_MODIFY_TITLE: true


================================================
FILE: .github/workflows/ytdlp-auto-release.yml
================================================
name: Auto Release yt-dlp Patch

on:
  schedule:
    - cron: '17 */6 * * *'
  workflow_dispatch:

permissions:
  contents: write

concurrency:
  group: ytdlp-auto-release
  cancel-in-progress: false

jobs:
  release:
    runs-on: ubuntu-latest
    env:
      DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
      GH_TOKEN: ${{ secrets.ACCESS_TOKEN }}
      GITHUB_TOKEN: ${{ secrets.ACCESS_TOKEN }}
      YTDLP_RELEASE_TOKEN: ${{ secrets.ACCESS_TOKEN }}
    steps:
      - name: Check release token
        run: |
          if [ -z "${GH_TOKEN}" ]; then
            echo "ACCESS_TOKEN secret is required to push the release commit and tag."
            exit 1
          fi

      - name: Check out Git repository
        uses: actions/checkout@v4
        with:
          token: ${{ secrets.ACCESS_TOKEN }}
          ref: ${{ github.event.repository.default_branch }}

      - name: Install Node.js
        uses: actions/setup-node@v4
        with:
          node-version: 20

      - name: Install pnpm
        uses: pnpm/action-setup@v4
        with:
          version: 8

      - name: Install Dependencies
        run: pnpm install --filter ./apps/desktop...

      - name: Check for yt-dlp updates
        id: check
        run: node apps/desktop/scripts/ytdlp-auto-release.mjs check

      - name: Prepare patch release
        if: steps.check.outputs.update_available == 'true'
        id: prepare
        run: node apps/desktop/scripts/ytdlp-auto-release.mjs prepare

      - name: Validate release changes
        if: steps.check.outputs.update_available == 'true'
        run: pnpm run check

      - name: Commit release changes
        if: steps.check.outputs.update_available == 'true'
        env:
          RELEASE_VERSION: ${{ steps.prepare.outputs.release_version }}
          LATEST_YTDLP_VERSION: ${{ steps.prepare.outputs.latest_ytdlp_version }}
        run: |
          git config user.name "github-actions[bot]"
          git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
          git add apps/desktop/package.json apps/desktop/changelogs/CHANGELOG.md apps/desktop/release-metadata.json
          git commit -m "chore(release): publish v${RELEASE_VERSION} with yt-dlp ${LATEST_YTDLP_VERSION}"
          git tag "v${RELEASE_VERSION}"

      - name: Push release commit and tag
        if: steps.check.outputs.update_available == 'true'
        env:
          RELEASE_VERSION: ${{ steps.prepare.outputs.release_version }}
        run: |
          git push origin HEAD:${DEFAULT_BRANCH}
          git push origin "v${RELEASE_VERSION}"

      - name: Report up-to-date status
        if: steps.check.outputs.update_available != 'true'
        run: echo "yt-dlp is already up to date. No patch release is needed."


================================================
FILE: .gitignore
================================================
node_modules
/dist
apps/desktop/dist
apps/web/dist
apps/api/.data/
out
.conductor/
.wxt
.output
.DS_Store
.eslintcache
*.log*


================================================
FILE: .husky/pre-commit
================================================
#!/bin/sh
# Exit on any error
set -e

# Check if there are any staged files
if [ -z "$(git diff --cached --name-only)" ]; then
  echo "No staged files to format"
  exit 0
fi

# Store the hash of staged changes to detect modifications
STAGED_HASH=$(git diff --cached | sha256sum | cut -d' ' -f1)

# Save list of staged files (handling all file states)
STAGED_FILES=$(git diff --cached --name-only --diff-filter=ACMR)
PARTIALLY_STAGED=$(git diff --name-only)

# Stash unstaged changes to preserve working directory
# --keep-index keeps staged changes in working tree
git stash push --quiet --keep-index --message "pre-commit-stash" || true
STASHED=$?

# Run formatter on the staged files
pnpm --filter ./apps/desktop run fix
FORMAT_EXIT_CODE=$?

# Restore working directory state
if [ $STASHED -eq 0 ]; then
  # Re-stage the formatted files
  if [ -n "$STAGED_FILES" ]; then
    echo "$STAGED_FILES" | while IFS= read -r file; do
      if [ -f "$file" ]; then
        git add "$file"
      fi
    done
  fi
  
  # Restore unstaged changes
  git stash pop --quiet || true
  
  # Restore partial staging if files were partially staged
  if [ -n "$PARTIALLY_STAGED" ]; then
    for file in $PARTIALLY_STAGED; do
      if [ -f "$file" ] && echo "$STAGED_FILES" | grep -q "^$file$"; then
        # File was partially staged - need to unstage the unstaged parts
        git restore --staged "$file" 2>/dev/null || true
        git add -p "$file" < /dev/null 2>/dev/null || git add "$file"
      fi
    done
  fi
else
  # No stash was created, just re-add the formatted files
  if [ -n "$STAGED_FILES" ]; then
    echo "$STAGED_FILES" | while IFS= read -r file; do
      if [ -f "$file" ]; then
        git add "$file"
      fi
    done
  fi
fi

# Check if staged files actually changed
NEW_STAGED_HASH=$(git diff --cached | sha256sum | cut -d' ' -f1)
if [ "$STAGED_HASH" != "$NEW_STAGED_HASH" ]; then
  echo "✨ Files formatted by Ultracite"
fi

exit $FORMAT_EXIT_CODE


================================================
FILE: .npmrc
================================================
electron_mirror=https://npmmirror.com/mirrors/electron/
electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/
shamefully-hoist=true



================================================
FILE: .vscode/extensions.json
================================================
{
  "recommendations": ["biomejs.biome", "bradlc.vscode-tailwindcss"]
}


================================================
FILE: .vscode/settings.json
================================================
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode",
  "editor.codeActionsOnSave": {
    "source.fixAll.biome": "explicit",
    "source.organizeImports.biome": "explicit"
  },
  "[javascript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescript]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[javascriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[typescriptreact]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[json]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "typescript.tsdk": "node_modules/typescript/lib",
  "tailwindCSS.experimental.classRegex": [
    ["cva\\(([^)]*)\\)", "[\"'`]([^\"'`]*).*?[\"'`]"],
    ["cn\\(([^)]*)\\)", "(?:'|\"|`)([^']*)(?:'|\"|`)"]
  ],
  "i18n-ally.localesPaths": ["apps/desktop/src/renderer/src/locales"],
  "i18n-ally.keystyle": "nested",
  "[css]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "editor.formatOnPaste": true,
  "emmet.showExpandedAbbreviation": "never",
  "[jsonc]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[html]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[vue]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[svelte]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[yaml]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[graphql]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[markdown]": {
    "editor.defaultFormatter": "biomejs.biome"
  },
  "[mdx]": {
    "editor.defaultFormatter": "biomejs.biome"
  }
}


================================================
FILE: AGENTS.md
================================================
1. use pnpm instead of npm
2. use pnpm run check after tasks to check code
3. Support i18n. When writing business logic, initially only translate the English version of en.json
4. use English for comments&console
5. Follow the ✅ KISS (Keep It Simple, Stupid) & ✅ YAGNI (You Aren't Gonna Need It) principles
6. Use Conventional Commits format for commit messages: `type(scope): subject`. Common types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert. PR titles should also follow this format.
7. If there is an error when running `pnpm run check:i18n`, please complete the missing corresponding translation files and fields. Ensure the translation is done into the corresponding language, rather than directly copying the English version.
8. After pushing each commit for a pull request, use the gh command to add a comment to the corresponding pull request, with the content being: "@codex review"

# Ultracite Code Standards

This project uses **Ultracite**, a zero-config preset that enforces strict code quality standards through automated formatting and linting.

## Quick Reference

- **Format code**: `pnpm dlx ultracite fix`
- **Check for issues**: `pnpm dlx ultracite check`
- **Diagnose setup**: `pnpm dlx ultracite doctor`

Biome (the underlying engine) provides robust linting and formatting. Most issues are automatically fixable.

---

## Core Principles

Write code that is **accessible, performant, type-safe, and maintainable**. Focus on clarity and explicit intent over brevity.

### Type Safety & Explicitness

- Use explicit types for function parameters and return values when they enhance clarity
- Prefer `unknown` over `any` when the type is genuinely unknown
- Use const assertions (`as const`) for immutable values and literal types
- Leverage TypeScript's type narrowing instead of type assertions
- Use meaningful variable names instead of magic numbers - extract constants with descriptive names

### Modern JavaScript/TypeScript

- Use arrow functions for callbacks and short functions
- Prefer `for...of` loops over `.forEach()` and indexed `for` loops
- Use optional chaining (`?.`) and nullish coalescing (`??`) for safer property access
- Prefer template literals over string concatenation
- Use destructuring for object and array assignments
- Use `const` by default, `let` only when reassignment is needed, never `var`

### Async & Promises

- Always `await` promises in async functions - don't forget to use the return value
- Use `async/await` syntax instead of promise chains for better readability
- Handle errors appropriately in async code with try-catch blocks
- Don't use async functions as Promise executors

### React & JSX

- Use function components over class components
- Call hooks at the top level only, never conditionally
- Specify all dependencies in hook dependency arrays correctly
- Use the `key` prop for elements in iterables (prefer unique IDs over array indices)
- Nest children between opening and closing tags instead of passing as props
- Don't define components inside other components
- Use semantic HTML and ARIA attributes for accessibility:
  - Provide meaningful alt text for images
  - Use proper heading hierarchy
  - Add labels for form inputs
  - Include keyboard event handlers alongside mouse events
  - Use semantic elements (`<button>`, `<nav>`, etc.) instead of divs with roles

### Error Handling & Debugging

- Remove `console.log`, `debugger`, and `alert` statements from production code
- Throw `Error` objects with descriptive messages, not strings or other values
- Use `try-catch` blocks meaningfully - don't catch errors just to rethrow them
- Prefer early returns over nested conditionals for error cases

### Code Organization

- Keep functions focused and under reasonable cognitive complexity limits
- Extract complex conditions into well-named boolean variables
- Use early returns to reduce nesting
- Prefer simple conditionals over nested ternary operators
- Group related code together and separate concerns

### Security

- Add `rel="noopener"` when using `target="_blank"` on links
- Avoid `dangerouslySetInnerHTML` unless absolutely necessary
- Don't use `eval()` or assign directly to `document.cookie`
- Validate and sanitize user input

### Performance

- Avoid spread syntax in accumulators within loops
- Use top-level regex literals instead of creating them in loops
- Prefer specific imports over namespace imports
- Avoid barrel files (index files that re-export everything)
- Use proper image components (e.g., Next.js `<Image>`) over `<img>` tags

### Framework-Specific Guidance

**Next.js:**
- Use Next.js `<Image>` component for images
- Use `next/head` or App Router metadata API for head elements
- Use Server Components for async data fetching instead of async Client Components

**React 19+:**
- Use ref as a prop instead of `React.forwardRef`

**Solid/Svelte/Vue/Qwik:**
- Use `class` and `for` attributes (not `className` or `htmlFor`)

---

## Testing

- Write assertions inside `it()` or `test()` blocks
- Avoid done callbacks in async tests - use async/await instead
- Don't use `.only` or `.skip` in committed code
- Keep test suites reasonably flat - avoid excessive `describe` nesting

## When Biome Can't Help

Biome's linter will catch most issues automatically. Focus your attention on:

1. **Business logic correctness** - Biome can't validate your algorithms
2. **Meaningful naming** - Use descriptive names for functions, variables, and types
3. **Architecture decisions** - Component structure, data flow, and API design
4. **Edge cases** - Handle boundary conditions and error states
5. **User experience** - Accessibility, performance, and usability considerations
6. **Documentation** - Add comments for complex logic, but prefer self-documenting code

---

Most formatting and common issues are automatically fixed by Biome. Run `pnpm dlx ultracite fix` before committing to ensure compliance.


================================================
FILE: CONTRIBUTING.md
================================================
# Contributing to VidBee

Thank you for taking the time to improve VidBee. These notes keep the project maintainable and easy to review.

## Getting Ready
- Use Node.js 18+ and pnpm 8+.
- Install dependencies with `pnpm install`.
- Run `pnpm dev` to test changes locally.

## Tech Stack
- Runtime: Electron 38, electron-vite, electron-builder.
- Frontend: React 19, React Router, Jotai, React Hook Form, Tailwind CSS 4, shadcn/ui, Lucide icons.
- Tooling: TypeScript 5, pnpm, Biome, dayjs, electron-log, electron-store, electron-updater, i18next, next-themes.

## Local Development
- Use `pnpm install` to pull dependencies after cloning.
- Start the Electron and Vite development environment with `pnpm dev`; hot module replacement is already configured.
- Preview the production build locally with `pnpm start`.

## Useful Scripts

| Command | Purpose |
| --- | --- |
| `pnpm run typecheck` | Type-check the main and renderer projects. |
| `pnpm build` | Run type checks and produce production bundles. |
| `pnpm build:win` / `pnpm build:mac` / `pnpm build:linux` | Create platform-specific distributables. |
| `pnpm build:unpack` | Produce unpacked output directories for inspection. |
| `pnpm run check` | Format and lint the codebase with Biome. |

## Project Structure

```text
apps/desktop/src/
|-- main/            # Electron main process, IPC services, configuration
|-- preload/         # Context bridge and preload helpers
`-- renderer/
    |-- src/
    |   |-- pages/      # Application routes (Home, Settings, Playlist, etc.)
    |   |-- components/ # UI components, download views, shared controls
    |   |-- data/       # Static datasets such as popularSites.ts
    |   |-- hooks/      # Custom hooks and global atoms
    |   |-- lib/        # Utilities shared across the renderer
    |   `-- assets/     # Global styles and icons
    `-- index.html
```

## Internationalization
- i18next drives localization with English (`en`) and Simplified Chinese (`zh-CN`) namespaces.
- Only update strings in `apps/desktop/src/renderer/src/locales/en.json`; maintainers handle the other locales.
- Keep copy edits focused and avoid removing translation keys without discussion.

## Configuration and Storage
- Persistent settings are stored with `electron-store` and exposed through IPC helpers.
- User-facing preferences such as download paths and themes live in `apps/desktop/src/main/settings.ts` and related services.
- Logs are recorded with `electron-log` to simplify troubleshooting.

## Packaging
- Build production bundles with `pnpm build`.
- Create platform-specific artifacts with `pnpm build:win`, `pnpm build:mac`, or `pnpm build:linux`.
- Use `pnpm build:unpack` to generate unpacked directories under `apps/desktop/dist/` for manual inspection.
- Bundle `yt-dlp` under `apps/desktop/resources/` and `ffmpeg/ffprobe` under `apps/desktop/resources/ffmpeg/` before packaging so merges and audio extraction work out of the box.

## Working on Changes
- Keep each pull request focused on a single problem or feature.
- Run `pnpm run check` before committing to ensure formatting and linting stay consistent.
- Write comments and console messages in English only.
- When updating copy in the app, adjust strings in `apps/desktop/src/renderer/src/locales/en.json`; other locale files are handled by maintainers.

## Opening Issues
- Search existing issues to avoid duplicates.
- Describe the problem clearly with steps to reproduce, expected behaviour, and screenshots or logs when useful.

## Submitting Pull Requests
- Explain the motivation and impact of the change in the description.
- Mention any user facing updates or migrations.
- Confirm that `pnpm run check` passes and note any follow-up work that is out of scope.

We appreciate every contribution that keeps VidBee simple and reliable.


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

Copyright (c) 2025 VidBee

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

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

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


================================================
FILE: README.md
================================================
<div align="left">
  <a href="https://github.com/nexmoe/VidBee">
    <img src="apps/desktop/build/icon.png" alt="Logo" width="80" height="80">
  </a>

  <h3>VidBee</h3>
  <p>
    <a href="https://github.com/nexmoe/VidBee/stargazers"><img src="https://img.shields.io/github/stars/nexmoe/VidBee?color=ffcb47&labelColor=black&logo=github&label=Stars" /></a>
    <a href="https://github.com/nexmoe/VidBee/graphs/contributors"><img src="https://img.shields.io/github/contributors/nexmoe/VidBee?ogo=github&label=Contributors&labelColor=black" /></a>
    <a href="https://github.com/nexmoe/VidBee/releases"><img src="https://img.shields.io/github/downloads/nexmoe/VidBee/total?color=369eff&labelColor=black&logo=github&label=Downloads" /></a>
    <a href="https://github.com/nexmoe/VidBee/releases/latest"><img src="https://img.shields.io/github/v/release/nexmoe/VidBee?color=369eff&labelColor=black&logo=github&label=Latest%20Release" /></a>
    <a href="https://x.com/intent/follow?screen_name=nexmoex"><img src="https://img.shields.io/badge/Follow-blue?color=1d9bf0&logo=x&labelColor=black" /></a>
    <a href="https://deepwiki.com/nexmoe/VidBee"><img src="https://deepwiki.com/badge.svg" alt="Ask DeepWiki"></a>
    <br />
    <br />
    <a href="https://github.com/nexmoe/VidBee/releases/latest" target="_blank"><img src="screenshots/main-interface.png" alt="VidBee Desktop" width="46%"/></a>
    <a href="https://github.com/nexmoe/VidBee/releases/latest" target="_blank"><img src="screenshots/download-queue.png" alt="VidBee Download Queue" width="46%"/></a>
    <br />
    <br />
  </p>
</div>

VidBee is a modern, open-source video downloader that lets you download videos and audios from 1000+ websites worldwide. Built with Electron and powered by yt-dlp, VidBee offers a clean, intuitive interface with powerful features for all your downloading needs, including RSS auto-download automation that automatically subscribes to feeds and downloads new videos from your favorite creators in the background.

## 👋🏻 Getting Started

VidBee is currently under active development, and feedback is welcome for any [issue](https://github.com/nexmoe/VidBee/issues) encountered.

[📥 Download VidBee](https://vidbee.org/download/) | [📚 Documentation](https://docs.vidbee.org)

> [!IMPORTANT]
>
> **Star Us**, You will receive all release notifications from GitHub without any delay ~

<a href="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats?repo_id=1081230042" target="_blank" style="display: block" align="left">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=1081230042&image_size=auto&color_scheme=dark" width="655" height="auto">
    <img alt="Performance Stats of nexmoe/VidBee - Last 28 days" src="https://next.ossinsight.io/widgets/official/compose-last-28-days-stats/thumbnail.png?repo_id=1081230042&image_size=auto&color_scheme=light" width="655" height="auto">
  </picture>
</a>

<!-- Made with [OSS Insight](https://ossinsight.io/) -->

## ✨ Features

### 🌍 Global Video Download Support

Download videos from almost any website worldwide through the powerful yt-dlp engine. Support for 1000+ sites including YouTube, TikTok, Instagram, Twitter, and many more.

![VidBee Main Interface](screenshots/main-interface.png)

### 🎨 Best-in-class UI Experience

Modern, clean interface with intuitive operations. One-click pause/resume/retry, real-time progress tracking, and comprehensive download queue management.

![VidBee Download Queue](screenshots/download-queue.png)

### 📡 RSS Auto Download

Automatically subscribe to RSS feeds and auto-download new videos in the background from your favorite creators across YouTube, TikTok, and more. Set up RSS subscriptions once, and VidBee will automatically download new uploads without manual intervention, perfect for keeping up with your favorite channels and creators.

## 🌐 Supported Sites

VidBee supports 1000+ video and audio platforms through yt-dlp. For the complete list of supported sites, visit [https://vidbee.org/supported-sites/](https://vidbee.org/supported-sites/)

## 🧱 Web + API (Docker-ready)

This monorepo now includes:

- `packages/downloader-core`: Shared yt-dlp/ffmpeg download core
- `apps/api`: Fastify API server with oRPC and SSE events
- `apps/web`: TanStack Start web client using oRPC

Run locally:

```bash
pnpm run start:web
```

This command starts `apps/api` and `apps/web` together.

Run with Docker:

```bash
docker compose up -d --build
```

Run with GitHub Container Registry images:

```yaml
services:
  api:
    image: ghcr.io/nexmoe/vidbee-api:latest
    environment:
      VIDBEE_API_HOST: 0.0.0.0
      VIDBEE_API_PORT: 3100
      VIDBEE_DOWNLOAD_DIR: /data/downloads
      VIDBEE_HISTORY_STORE_PATH: /data/vidbee/vidbee.db
    ports:
      - "3100:3100"
    volumes:
      - vidbee-downloads:/data/downloads
      - vidbee-data:/data/vidbee
    restart: unless-stopped

  web:
    image: ghcr.io/nexmoe/vidbee-web:latest
    depends_on:
      - api
    ports:
      - "3000:3000"
    restart: unless-stopped

volumes:
  vidbee-downloads:
  vidbee-data:
```

Stop services:

```bash
docker compose down
```

Optional env vars (via `.env`):

```bash
VIDBEE_API_PORT=3100
VIDBEE_WEB_PORT=3000
VITE_API_URL=http://localhost:3100
```

## 🤝 Contributing

You are welcome to join the open source community to build together. For more details, check out:

- Monorepo apps:
  - `apps/desktop`: VidBee desktop app (Electron)
  - `apps/docs`: Documentation site (Next.js)
  - `apps/extension`: Browser extension (WXT)
- [Contributing Guide](./CONTRIBUTING.md)
- [DeepWiki Documentation](https://deepwiki.com/nexmoe/VidBee)

## 📄 License

This project is distributed under the MIT License. See [`LICENSE`](LICENSE) for details.

## 🙏 Thanks

- [yt-dlp](https://github.com/yt-dlp/yt-dlp) - The powerful video downloader engine
- [FFmpeg](https://ffmpeg.org/) - The multimedia framework for video and audio processing
- [Electron](https://www.electronjs.org/) - Build cross-platform desktop apps
- [React](https://react.dev/) - The UI library
- [Vite](https://vitejs.dev/) - Next generation frontend tooling
- [Tailwind CSS](https://tailwindcss.com/) - Utility-first CSS framework
- [shadcn/ui](https://ui.shadcn.com/) - Beautifully designed components


================================================
FILE: apps/api/Dockerfile
================================================
FROM node:22-alpine

ENV PNPM_HOME=/pnpm
ENV PATH=${PNPM_HOME}:${PATH}
ENV YTDLP_PATH=/usr/bin/yt-dlp
ENV FFMPEG_PATH=/usr/bin/ffmpeg

RUN apk add --no-cache yt-dlp ffmpeg python3 make g++

RUN corepack enable

WORKDIR /app

COPY pnpm-lock.yaml pnpm-workspace.yaml package.json ./
COPY apps/api/package.json apps/api/package.json
COPY packages/db/package.json packages/db/package.json
COPY packages/downloader-core/package.json packages/downloader-core/package.json

RUN pnpm install --filter "{./apps/api}..." --frozen-lockfile

COPY apps/api apps/api
COPY apps/desktop/resources/drizzle apps/desktop/resources/drizzle
COPY packages/db packages/db
COPY packages/downloader-core packages/downloader-core

EXPOSE 3100

ENV VIDBEE_API_HOST=0.0.0.0
ENV VIDBEE_API_PORT=3100
ENV VIDBEE_DOWNLOAD_DIR=/data/downloads

VOLUME ["/data/downloads"]

CMD ["pnpm", "--filter", "./apps/api", "run", "start"]


================================================
FILE: apps/api/package.json
================================================
{
  "name": "api",
  "private": true,
  "type": "module",
  "scripts": {
    "dev": "tsx watch src/index.ts",
    "start": "tsx src/index.ts",
    "typecheck": "tsc --noEmit",
    "check": "ultracite check && pnpm run typecheck"
  },
  "dependencies": {
    "@fastify/cors": "^11.2.0",
    "@orpc/openapi": "^1.13.5",
    "@orpc/server": "^1.13.5",
    "@orpc/zod": "^1.13.5",
    "@vidbee/db": "workspace:*",
    "@vidbee/downloader-core": "workspace:*",
    "better-sqlite3": "^12.4.1",
    "drizzle-orm": "^0.44.7",
    "fastify": "^5.7.4"
  },
  "devDependencies": {
    "@types/better-sqlite3": "^7.6.13",
    "@types/node": "^22.18.6",
    "drizzle-kit": "0.31.7",
    "tsx": "^4.21.0",
    "typescript": "^5.9.2",
    "ultracite": "7.1.5"
  }
}


================================================
FILE: apps/api/src/index.ts
================================================
import { createApiServer } from './server'

const host = process.env.VIDBEE_API_HOST?.trim() || '0.0.0.0'
const portValue = Number(process.env.VIDBEE_API_PORT ?? '')
const port = Number.isInteger(portValue) && portValue > 0 ? portValue : 3100

const server = await createApiServer()

try {
  await server.listen({ host, port })
  server.log.info(`VidBee API server listening on http://${host}:${port}`)
} catch (error) {
  server.log.error(error)
  process.exit(1)
}

const shutdown = async (signal: string) => {
  server.log.info(`Received ${signal}, shutting down API server`)
  await server.close()
  process.exit(0)
}

process.on('SIGINT', () => {
  void shutdown('SIGINT')
})
process.on('SIGTERM', () => {
  void shutdown('SIGTERM')
})


================================================
FILE: apps/api/src/lib/database-migrate.ts
================================================
import { existsSync } from 'node:fs'
import path from 'node:path'
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'
import { migrate } from 'drizzle-orm/better-sqlite3/migrator'

const MIGRATIONS_FOLDER = path.resolve(import.meta.dirname, '../../../desktop/resources/drizzle')

export const runDatabaseMigrations = (database: BetterSQLite3Database): void => {
  if (!existsSync(MIGRATIONS_FOLDER)) {
    throw new Error(`API migrations folder not found: ${MIGRATIONS_FOLDER}`)
  }

  migrate(database, { migrationsFolder: MIGRATIONS_FOLDER })
}


================================================
FILE: apps/api/src/lib/downloader.ts
================================================
import path from 'node:path'
import type { DownloadTask } from '@vidbee/downloader-core'
import { DownloaderCore } from '@vidbee/downloader-core'
import { HistoryStore } from './history-store'

const defaultDownloadDir =
  process.env.VIDBEE_DOWNLOAD_DIR?.trim() || process.env.DOWNLOAD_DIR?.trim() || undefined

const maxConcurrentValue = process.env.VIDBEE_MAX_CONCURRENT?.trim()
const parsedMaxConcurrent = maxConcurrentValue ? Number(maxConcurrentValue) : Number.NaN
const maxConcurrent =
  Number.isFinite(parsedMaxConcurrent) && parsedMaxConcurrent > 0 ? parsedMaxConcurrent : undefined

const configuredHistoryStorePath = process.env.VIDBEE_HISTORY_STORE_PATH?.trim()
const historyStorePath = configuredHistoryStorePath
  ? configuredHistoryStorePath
  : defaultDownloadDir
    ? path.join(defaultDownloadDir, '.vidbee', 'vidbee.db')
    : path.join(process.cwd(), '.vidbee', 'vidbee.db')

export const historyStore = new HistoryStore(historyStorePath)

export const downloaderCore = new DownloaderCore({
  downloadDir: defaultDownloadDir,
  maxConcurrent
})

const terminalStatuses = new Set<DownloadTask['status']>(['completed', 'error', 'cancelled'])

downloaderCore.on('task-updated', (task: DownloadTask) => {
  if (!terminalStatuses.has(task.status)) {
    return
  }
  historyStore.save(task)
})


================================================
FILE: apps/api/src/lib/history-record-mapper.ts
================================================
import type { DownloadHistoryInsert, DownloadHistoryRow } from '@vidbee/db/history'
import type { DownloadTask } from '@vidbee/downloader-core'

const TERMINAL_STATUSES = new Set<DownloadTask['status']>(['completed', 'error', 'cancelled'])
const TAG_SEPARATOR = '\n'

const parseJson = <T>(value: string | null | undefined): T | undefined => {
  if (!value) {
    return undefined
  }
  try {
    return JSON.parse(value) as T
  } catch {
    return undefined
  }
}

const sanitizeList = (values?: string[]): string[] => {
  if (!values || values.length === 0) {
    return []
  }
  return values
    .map((value) => value.trim())
    .filter((value, index, array) => value.length > 0 && array.indexOf(value) === index)
}

const serializeTags = (values?: string[]): string | null => {
  const sanitized = sanitizeList(values)
  return sanitized.length > 0 ? sanitized.join(TAG_SEPARATOR) : null
}

const parseTags = (value: string | null): string[] | undefined => {
  if (!value) {
    return undefined
  }
  const parsed = value
    .split(TAG_SEPARATOR)
    .map((tag) => tag.trim())
    .filter((tag, index, array) => tag.length > 0 && array.indexOf(tag) === index)
  return parsed.length > 0 ? parsed : undefined
}

export const isTerminalTask = (task: DownloadTask): boolean => TERMINAL_STATUSES.has(task.status)

export const serializeHistoryTask = (task: DownloadTask): DownloadHistoryInsert => {
  const downloadedAt = task.createdAt
  const completedAt = task.completedAt ?? null
  const sortKey = completedAt ?? downloadedAt

  return {
    id: task.id,
    url: task.url,
    title: task.title ?? task.url,
    thumbnail: task.thumbnail ?? null,
    type: task.type,
    status: task.status,
    downloadPath: task.downloadPath ?? null,
    savedFileName: task.savedFileName ?? null,
    fileSize: task.fileSize ?? null,
    duration: task.duration ?? null,
    downloadedAt,
    completedAt,
    sortKey,
    error: task.error ?? null,
    ytDlpCommand: task.ytDlpCommand ?? null,
    ytDlpLog: task.ytDlpLog ?? null,
    description: task.description ?? null,
    channel: task.channel ?? null,
    uploader: task.uploader ?? null,
    viewCount: task.viewCount ?? null,
    tags: serializeTags(task.tags),
    origin: null,
    subscriptionId: null,
    selectedFormat: task.selectedFormat ? JSON.stringify(task.selectedFormat) : null,
    playlistId: task.playlistId ?? null,
    playlistTitle: task.playlistTitle ?? null,
    playlistIndex: task.playlistIndex ?? null,
    playlistSize: task.playlistSize ?? null
  }
}

export const mapHistoryRowToTask = (row: DownloadHistoryRow): DownloadTask => {
  const parsedTags = parseTags(row.tags ?? null)
  const parsedSelectedFormat = parseJson<DownloadTask['selectedFormat']>(row.selectedFormat)

  return {
    id: row.id,
    url: row.url,
    title: row.title,
    thumbnail: row.thumbnail ?? undefined,
    type: row.type as DownloadTask['type'],
    status: row.status as DownloadTask['status'],
    createdAt: row.downloadedAt,
    completedAt: row.completedAt ?? undefined,
    downloadPath: row.downloadPath ?? undefined,
    savedFileName: row.savedFileName ?? undefined,
    fileSize: row.fileSize ?? undefined,
    duration: row.duration ?? undefined,
    error: row.error ?? undefined,
    ytDlpCommand: row.ytDlpCommand ?? undefined,
    ytDlpLog: row.ytDlpLog ?? undefined,
    description: row.description ?? undefined,
    channel: row.channel ?? undefined,
    uploader: row.uploader ?? undefined,
    viewCount: row.viewCount ?? undefined,
    tags: parsedTags,
    selectedFormat: parsedSelectedFormat,
    playlistId: row.playlistId ?? undefined,
    playlistTitle: row.playlistTitle ?? undefined,
    playlistIndex: row.playlistIndex ?? undefined,
    playlistSize: row.playlistSize ?? undefined
  }
}


================================================
FILE: apps/api/src/lib/history-store.ts
================================================
import fs from 'node:fs'
import path from 'node:path'
import { downloadHistoryTable } from '@vidbee/db/history'
import type { DownloadTask } from '@vidbee/downloader-core'
import DatabaseConstructor from 'better-sqlite3'
import { desc, eq, inArray } from 'drizzle-orm'
import { drizzle } from 'drizzle-orm/better-sqlite3'
import { runDatabaseMigrations } from './database-migrate'
import { isTerminalTask, mapHistoryRowToTask, serializeHistoryTask } from './history-record-mapper'

export class HistoryStore {
  private readonly db
  private readonly sqlite

  constructor(databasePath: string) {
    fs.mkdirSync(path.dirname(databasePath), { recursive: true })
    this.sqlite = new DatabaseConstructor(databasePath, { timeout: 5000 })
    this.sqlite.pragma('journal_mode = WAL')
    this.db = drizzle(this.sqlite)
    runDatabaseMigrations(this.db)
  }

  save(task: DownloadTask): void {
    if (!isTerminalTask(task)) {
      return
    }

    const row = serializeHistoryTask(task)

    this.db
      .insert(downloadHistoryTable)
      .values(row)
      .onConflictDoUpdate({
        target: downloadHistoryTable.id,
        set: { ...row }
      })
      .run()
  }

  list(): DownloadTask[] {
    const rows = this.db
      .select()
      .from(downloadHistoryTable)
      .orderBy(desc(downloadHistoryTable.sortKey))
      .all()

    const tasks: DownloadTask[] = []
    for (const row of rows) {
      const task = mapHistoryRowToTask(row)
      if (isTerminalTask(task)) {
        tasks.push(task)
      }
    }
    return tasks
  }

  removeItems(ids: string[]): number {
    const normalizedIds = ids.map((id) => id.trim()).filter((id) => id.length > 0)
    if (normalizedIds.length === 0) {
      return 0
    }
    const result = this.db
      .delete(downloadHistoryTable)
      .where(inArray(downloadHistoryTable.id, normalizedIds))
      .run()
    return result.changes
  }

  removeByPlaylist(playlistId: string): number {
    const normalizedPlaylistId = playlistId.trim()
    if (!normalizedPlaylistId) {
      return 0
    }
    const result = this.db
      .delete(downloadHistoryTable)
      .where(eq(downloadHistoryTable.playlistId, normalizedPlaylistId))
      .run()
    return result.changes
  }
}


================================================
FILE: apps/api/src/lib/rpc-router.ts
================================================
import { spawn } from 'node:child_process'
import { randomUUID } from 'node:crypto'
import { constants as fsConstants } from 'node:fs'
import { access, mkdir, readdir, rm, stat, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { implement, ORPCError } from '@orpc/server'
import { downloaderContract } from '@vidbee/downloader-core'
import { downloaderCore, historyStore } from './downloader'
import { webSettingsStore } from './web-settings-store'

const os = implement(downloaderContract)
const WEB_SETTINGS_FILES_DIR = path.resolve(process.cwd(), '.data', 'web-settings-files')
const MAX_WEB_SETTINGS_FILE_BYTES = 1_000_000
const MANAGED_SETTINGS_FILE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000
const SAFE_FILE_NAME_REGEX = /[^A-Za-z0-9._-]+/g
type ManagedSettingsFileKind = 'cookies' | 'config'

const toErrorMessage = (error: unknown, fallbackMessage: string): string => {
  if (error instanceof Error && error.message.trim().length > 0) {
    return error.message
  }

  return fallbackMessage
}

const runProcess = (command: string, args: string[]): Promise<boolean> =>
  new Promise((resolve) => {
    const child = spawn(command, args, {
      stdio: 'ignore',
      windowsHide: true
    })

    child.on('error', () => {
      resolve(false)
    })

    child.on('close', (code) => {
      resolve(code === 0)
    })
  })

const pathExists = async (targetPath: string): Promise<boolean> => {
  try {
    await access(targetPath, fsConstants.F_OK)
    return true
  } catch {
    return false
  }
}

const isPathWithinBase = (basePath: string, targetPath: string): boolean => {
  const normalizedBase = path.resolve(basePath)
  const normalizedTarget = path.resolve(targetPath)
  const relativePath = path.relative(normalizedBase, normalizedTarget)

  return relativePath !== '' && !relativePath.startsWith('..') && !path.isAbsolute(relativePath)
}

const openFileWithSystem = async (targetPath: string): Promise<boolean> => {
  if (process.platform === 'darwin') {
    return runProcess('open', [targetPath])
  }

  if (process.platform === 'win32') {
    return runProcess('cmd', ['/c', 'start', '', targetPath])
  }

  return runProcess('xdg-open', [targetPath])
}

const openFileLocationWithSystem = async (targetPath: string): Promise<boolean> => {
  if (process.platform === 'darwin') {
    return runProcess('open', ['-R', targetPath])
  }

  if (process.platform === 'win32') {
    return runProcess('explorer', [`/select,${targetPath}`])
  }

  return runProcess('xdg-open', [path.dirname(targetPath)])
}

const copyFileToClipboardWithSystem = async (targetPath: string): Promise<boolean> => {
  if (process.platform === 'darwin') {
    const escapedPath = targetPath.replace(/\\/g, '\\\\').replace(/"/g, '\\"')
    return runProcess('osascript', ['-e', `set the clipboard to (POSIX file "${escapedPath}")`])
  }

  if (process.platform === 'win32') {
    const escapedPath = targetPath.replace(/'/g, "''")
    return runProcess('powershell', [
      '-NoProfile',
      '-Command',
      `Set-Clipboard -Path '${escapedPath}'`
    ])
  }

  return false
}

const listServerDirectories = async (
  rawPath: string | undefined
): Promise<{
  currentPath: string
  parentPath: string | null
  directories: { name: string; path: string }[]
}> => {
  const requestedPath = rawPath?.trim()
  const candidatePath = requestedPath && requestedPath.length > 0 ? requestedPath : process.cwd()
  const currentPath = path.resolve(candidatePath)

  const pathInfo = await stat(currentPath)
  if (!pathInfo.isDirectory()) {
    throw new Error('Path is not a directory.')
  }

  const entries = await readdir(currentPath, { withFileTypes: true })
  const directories = entries
    .filter((entry) => entry.isDirectory())
    .map((entry) => ({
      name: entry.name,
      path: path.join(currentPath, entry.name)
    }))
    .sort((a, b) => a.name.localeCompare(b.name))

  const parsed = path.parse(currentPath)
  const parentPath = currentPath === parsed.root ? null : path.dirname(currentPath)

  return { currentPath, parentPath, directories }
}

const sanitizeUploadedFileName = (fileName: string, fallbackFileName: string): string => {
  const normalized = path
    .basename(fileName.trim())
    .replace(SAFE_FILE_NAME_REGEX, '-')
    .replace(/-+/g, '-')
    .replace(/^-|-$/g, '')

  if (!normalized) {
    return fallbackFileName
  }

  return normalized.slice(0, 120)
}

const storeWebSettingsFile = async (
  kind: 'cookies' | 'config',
  fileName: string,
  content: string
): Promise<string> => {
  const contentBuffer = Buffer.from(content, 'utf-8')
  if (contentBuffer.byteLength > MAX_WEB_SETTINGS_FILE_BYTES) {
    throw new Error('Uploaded file is too large.')
  }

  const destinationDir = path.join(WEB_SETTINGS_FILES_DIR, kind)
  await mkdir(destinationDir, { recursive: true })

  const fallbackFileName = kind === 'cookies' ? 'cookies.txt' : 'config.txt'
  const safeFileName = sanitizeUploadedFileName(fileName, fallbackFileName)
  const storedFileName = `${Date.now()}-${randomUUID()}-${safeFileName}`
  const destinationPath = path.join(destinationDir, storedFileName)

  await writeFile(destinationPath, contentBuffer)
  return destinationPath
}

const resolveManagedSettingsFilePath = (
  rawPath: string,
  kind: ManagedSettingsFileKind
): string | null => {
  const trimmedPath = rawPath.trim()
  if (!trimmedPath) {
    return null
  }

  const resolvedPath = path.resolve(trimmedPath)
  const managedDirectory = path.join(WEB_SETTINGS_FILES_DIR, kind)
  if (!isPathWithinBase(managedDirectory, resolvedPath)) {
    return null
  }

  return resolvedPath
}

const pruneManagedSettingsFiles = async (
  kind: ManagedSettingsFileKind,
  referencedPaths: string[]
): Promise<void> => {
  const managedDirectory = path.join(WEB_SETTINGS_FILES_DIR, kind)
  const keepPaths = new Set<string>()

  for (const rawPath of referencedPaths) {
    const managedPath = resolveManagedSettingsFilePath(rawPath, kind)
    if (managedPath) {
      keepPaths.add(managedPath)
    }
  }

  let entries: { isFile: () => boolean; name: string }[] = []
  try {
    entries = await readdir(managedDirectory, { withFileTypes: true })
  } catch {
    return
  }

  const now = Date.now()
  for (const entry of entries) {
    if (!entry.isFile()) {
      continue
    }

    const candidatePath = path.resolve(path.join(managedDirectory, entry.name))
    if (keepPaths.has(candidatePath)) {
      continue
    }

    try {
      const candidateInfo = await stat(candidatePath)
      if (now - candidateInfo.mtimeMs < MANAGED_SETTINGS_FILE_RETENTION_MS) {
        continue
      }

      await rm(candidatePath, { force: true })
    } catch {
      // Ignore cleanup errors to keep upload and settings updates resilient.
    }
  }
}

const triggerManagedSettingsFilePrune = (
  kind: ManagedSettingsFileKind,
  newlyUploadedPath: string
): void => {
  void (async () => {
    try {
      const settings = await webSettingsStore.get()
      const currentSettingsPath = kind === 'cookies' ? settings.cookiesPath : settings.configPath
      await pruneManagedSettingsFiles(kind, [newlyUploadedPath, currentSettingsPath])
    } catch {
      // Ignore cleanup errors to keep upload and settings updates resilient.
    }
  })()
}

export const rpcRouter = os.router({
  status: os.status.handler(() => {
    const status = downloaderCore.getStatus()
    return {
      ok: true,
      version: '1.0.0',
      active: status.active,
      pending: status.pending
    }
  }),
  videoInfo: os.videoInfo.handler(async ({ input }) => {
    try {
      const video = await downloaderCore.getVideoInfo(input.url, input.settings)
      return { video }
    } catch (error) {
      throw new ORPCError('INTERNAL_SERVER_ERROR', {
        message: toErrorMessage(error, 'Failed to fetch video info.')
      })
    }
  }),
  playlist: {
    info: os.playlist.info.handler(async ({ input }) => {
      try {
        const playlist = await downloaderCore.getPlaylistInfo(input.url, input.settings)
        return { playlist }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to fetch playlist info.')
        })
      }
    }),
    download: os.playlist.download.handler(async ({ input }) => {
      try {
        const result = await downloaderCore.startPlaylistDownload(input)
        return { result }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to start playlist download.')
        })
      }
    })
  },
  downloads: {
    create: os.downloads.create.handler(async ({ input }) => {
      try {
        const download = await downloaderCore.createDownload(input)
        return { download }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to create download.')
        })
      }
    }),
    list: os.downloads.list.handler(() => {
      return {
        downloads: downloaderCore.listDownloads()
      }
    }),
    cancel: os.downloads.cancel.handler(async ({ input }) => {
      try {
        const cancelled = await downloaderCore.cancelDownload(input.id)
        return { cancelled }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to cancel download.')
        })
      }
    })
  },
  history: {
    list: os.history.list.handler(() => {
      return {
        history: historyStore.list()
      }
    }),
    removeItems: os.history.removeItems.handler(({ input }) => {
      try {
        const removed = historyStore.removeItems(input.ids)
        downloaderCore.removeHistoryItems(input.ids)
        return { removed }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to remove history items.')
        })
      }
    }),
    removeByPlaylist: os.history.removeByPlaylist.handler(({ input }) => {
      try {
        const removed = historyStore.removeByPlaylist(input.playlistId)
        downloaderCore.removeHistoryByPlaylist(input.playlistId)
        return { removed }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to remove playlist history.')
        })
      }
    })
  },
  files: {
    exists: os.files.exists.handler(async ({ input }) => {
      try {
        const resolvedPath = path.resolve(input.path)
        return { exists: await pathExists(resolvedPath) }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to check file existence.')
        })
      }
    }),
    listDirectories: os.files.listDirectories.handler(async ({ input }) => {
      try {
        return await listServerDirectories(input.path)
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to list server directories.')
        })
      }
    }),
    openFile: os.files.openFile.handler(async ({ input }) => {
      try {
        const resolvedPath = path.resolve(input.path)
        const exists = await pathExists(resolvedPath)
        if (!exists) {
          return { success: false }
        }

        return { success: await openFileWithSystem(resolvedPath) }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to open file.')
        })
      }
    }),
    openFileLocation: os.files.openFileLocation.handler(async ({ input }) => {
      try {
        const resolvedPath = path.resolve(input.path)
        const exists = await pathExists(resolvedPath)
        if (!exists) {
          return { success: false }
        }

        return { success: await openFileLocationWithSystem(resolvedPath) }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to open file location.')
        })
      }
    }),
    copyFileToClipboard: os.files.copyFileToClipboard.handler(async ({ input }) => {
      try {
        const resolvedPath = path.resolve(input.path)
        const exists = await pathExists(resolvedPath)
        if (!exists) {
          return { success: false }
        }

        return { success: await copyFileToClipboardWithSystem(resolvedPath) }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to copy file to clipboard.')
        })
      }
    }),
    deleteFile: os.files.deleteFile.handler(async ({ input }) => {
      try {
        const settings = await webSettingsStore.get()
        const managedDownloadPath = settings.downloadPath.trim()
        if (!managedDownloadPath) {
          throw new ORPCError('FORBIDDEN', {
            message: 'Deleting files is disabled until a download path is configured.'
          })
        }

        const resolvedPath = path.resolve(input.path)
        if (!isPathWithinBase(managedDownloadPath, resolvedPath)) {
          throw new ORPCError('FORBIDDEN', {
            message: 'Refusing to delete files outside the managed download directory.'
          })
        }

        const exists = await pathExists(resolvedPath)
        if (!exists) {
          return { success: false }
        }

        await rm(resolvedPath)
        return { success: true }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to delete file.')
        })
      }
    }),
    uploadSettingsFile: os.files.uploadSettingsFile.handler(async ({ input }) => {
      try {
        const storedPath = await storeWebSettingsFile(input.kind, input.fileName, input.content)
        triggerManagedSettingsFilePrune(input.kind, storedPath)
        return { path: storedPath }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to upload settings file.')
        })
      }
    })
  },
  settings: {
    get: os.settings.get.handler(async () => {
      try {
        const settings = await webSettingsStore.get()
        return { settings }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to read settings.')
        })
      }
    }),
    set: os.settings.set.handler(async ({ input }) => {
      try {
        const settings = await webSettingsStore.set(input.settings)
        return { settings }
      } catch (error) {
        throw new ORPCError('INTERNAL_SERVER_ERROR', {
          message: toErrorMessage(error, 'Failed to save settings.')
        })
      }
    })
  }
})


================================================
FILE: apps/api/src/lib/sse.ts
================================================
import type { ServerResponse } from 'node:http'

const HEARTBEAT_INTERVAL_MS = 15_000

export class SseHub {
  private readonly clients = new Set<ServerResponse>()
  private heartbeatTimer: NodeJS.Timeout | null = null

  addClient(client: ServerResponse): void {
    this.clients.add(client)
    client.write('event: connected\ndata: {"ok":true}\n\n')
    this.ensureHeartbeatTimer()
  }

  removeClient(client: ServerResponse): void {
    this.clients.delete(client)
    if (this.clients.size === 0) {
      this.clearHeartbeatTimer()
    }
  }

  publish(event: string, payload: unknown): void {
    if (this.clients.size === 0) {
      return
    }

    const data = JSON.stringify(payload)
    const message = `event: ${event}\ndata: ${data}\n\n`

    for (const client of this.clients) {
      client.write(message)
    }
  }

  closeAll(): void {
    for (const client of this.clients) {
      client.end()
    }
    this.clients.clear()
    this.clearHeartbeatTimer()
  }

  private ensureHeartbeatTimer(): void {
    if (this.heartbeatTimer) {
      return
    }

    this.heartbeatTimer = setInterval(() => {
      for (const client of this.clients) {
        client.write(': heartbeat\n\n')
      }
    }, HEARTBEAT_INTERVAL_MS)
  }

  private clearHeartbeatTimer(): void {
    if (!this.heartbeatTimer) {
      return
    }
    clearInterval(this.heartbeatTimer)
    this.heartbeatTimer = null
  }
}


================================================
FILE: apps/api/src/lib/web-settings-store.ts
================================================
import { mkdir, readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
import { WebAppSettingsSchema } from '@vidbee/downloader-core'

const STORAGE_DIR = path.resolve(process.cwd(), '.data')
const STORAGE_FILE = path.join(STORAGE_DIR, 'web-settings.json')

const defaultWebSettings = WebAppSettingsSchema.parse({
  downloadPath: '',
  maxConcurrentDownloads: 5,
  browserForCookies: 'none',
  cookiesPath: '',
  proxy: '',
  configPath: '',
  betaProgram: false,
  language: 'en',
  theme: 'system',
  oneClickDownload: false,
  oneClickDownloadType: 'video',
  oneClickQuality: 'best',
  closeToTray: true,
  autoUpdate: true,
  subscriptionOnlyLatestDefault: true,
  enableAnalytics: true,
  embedSubs: true,
  embedThumbnail: false,
  embedMetadata: true,
  embedChapters: true,
  shareWatermark: false
})

type WebAppSettings = typeof defaultWebSettings

class WebSettingsStore {
  private settings = defaultWebSettings
  private initialized = false

  private async ensureInitialized(): Promise<void> {
    if (this.initialized) {
      return
    }

    this.initialized = true

    try {
      const raw = await readFile(STORAGE_FILE, 'utf-8')
      const parsed = JSON.parse(raw)
      const result = WebAppSettingsSchema.safeParse(parsed)
      if (result.success) {
        this.settings = result.data
      }
    } catch {
      this.settings = defaultWebSettings
    }
  }

  async get(): Promise<WebAppSettings> {
    await this.ensureInitialized()
    return this.settings
  }

  async set(nextSettings: WebAppSettings): Promise<WebAppSettings> {
    await this.ensureInitialized()
    const validated = WebAppSettingsSchema.parse(nextSettings)
    await mkdir(STORAGE_DIR, { recursive: true })
    await writeFile(STORAGE_FILE, JSON.stringify(validated), 'utf-8')
    this.settings = validated
    return this.settings
  }
}

export const webSettingsStore = new WebSettingsStore()


================================================
FILE: apps/api/src/server.ts
================================================
import { lookup } from 'node:dns/promises'
import type { ServerResponse } from 'node:http'
import net from 'node:net'
import cors from '@fastify/cors'
import { OpenAPIHandler } from '@orpc/openapi/fastify'
import { OpenAPIReferencePlugin } from '@orpc/openapi/plugins'
import { RPCHandler } from '@orpc/server/fastify'
import { ZodToJsonSchemaConverter } from '@orpc/zod/zod4'
import type { DownloadTask } from '@vidbee/downloader-core'
import Fastify from 'fastify'
import { downloaderCore } from './lib/downloader'
import { rpcRouter } from './lib/rpc-router'
import { SseHub } from './lib/sse'

const MAX_PROXY_IMAGE_BYTES = 10 * 1024 * 1024
const MAX_PROXY_REDIRECTS = 5

const isPrivateIpv4 = (ip: string): boolean => {
  const octets = ip.split('.').map((value) => Number.parseInt(value, 10))
  if (octets.length !== 4 || octets.some((value) => Number.isNaN(value))) {
    return false
  }

  const [a, b] = octets
  if (a === 10) {
    return true
  }
  if (a === 127) {
    return true
  }
  if (a === 169 && b === 254) {
    return true
  }
  if (a === 172 && b >= 16 && b <= 31) {
    return true
  }
  if (a === 192 && b === 168) {
    return true
  }
  return false
}

const isPrivateIpv6 = (ip: string): boolean => {
  const normalized = ip.toLowerCase()
  if (normalized === '::1') {
    return true
  }
  if (normalized.startsWith('fc') || normalized.startsWith('fd')) {
    return true
  }
  if (normalized.startsWith('fe80:')) {
    return true
  }
  return false
}

const isBlockedHost = async (url: URL): Promise<boolean> => {
  const hostname = url.hostname.trim().toLowerCase()
  if (!hostname) {
    return true
  }

  if (hostname === 'localhost' || hostname.endsWith('.localhost') || hostname === '0.0.0.0') {
    return true
  }

  if (net.isIP(hostname) === 4) {
    return isPrivateIpv4(hostname)
  }
  if (net.isIP(hostname) === 6) {
    return isPrivateIpv6(hostname)
  }

  try {
    const records = await lookup(hostname, { all: true, verbatim: true })
    if (records.length === 0) {
      return true
    }
    for (const record of records) {
      if (record.family === 4 && isPrivateIpv4(record.address)) {
        return true
      }
      if (record.family === 6 && isPrivateIpv6(record.address)) {
        return true
      }
    }
    return false
  } catch {
    return true
  }
}

const parseRemoteImageUrl = (value: string): URL | null => {
  try {
    const parsed = new URL(value)
    if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') {
      return null
    }

    return parsed
  } catch {
    return null
  }
}

export const createApiServer = async () => {
  await downloaderCore.initialize()
  const isDev = process.env.NODE_ENV !== 'production'

  const fastify = Fastify({
    logger: true,
    disableRequestLogging: isDev
  })

  await fastify.register(cors, {
    origin: true,
    methods: ['GET', 'POST', 'OPTIONS']
  })

  const rpcHandler = new RPCHandler(rpcRouter)
  const openApiHandler = new OpenAPIHandler(rpcRouter, {
    plugins: [
      new OpenAPIReferencePlugin({
        schemaConverters: [new ZodToJsonSchemaConverter()],
        docsProvider: 'swagger',
        docsPath: '/docs',
        specPath: '/openapi.json',
        docsTitle: 'VidBee API Reference',
        specGenerateOptions: {
          info: {
            title: 'VidBee API',
            version: '1.0.0'
          },
          servers: [{ url: '/openapi' }]
        }
      })
    ]
  })

  const sseHub = new SseHub()

  downloaderCore.on('task-updated', (task: DownloadTask) => {
    sseHub.publish('task-updated', { task })
  })
  downloaderCore.on('queue-updated', (downloads: DownloadTask[]) => {
    sseHub.publish('queue-updated', { downloads })
  })

  fastify.get('/health', async () => {
    return { ok: true }
  })

  fastify.get<{ Querystring: { url?: string } }>('/images/proxy', async (request, reply) => {
    const sourceUrl = request.query.url?.trim()
    if (!sourceUrl) {
      return reply.code(400).send({ message: 'Missing url query parameter.' })
    }

    const parsedUrl = parseRemoteImageUrl(sourceUrl)
    if (!parsedUrl) {
      return reply.code(400).send({ message: 'Invalid remote image URL.' })
    }

    let response: Response | null = null
    let currentUrl = parsedUrl

    for (let redirectCount = 0; redirectCount <= MAX_PROXY_REDIRECTS; redirectCount++) {
      if (await isBlockedHost(currentUrl)) {
        return reply.code(400).send({ message: 'Remote host is not allowed.' })
      }

      try {
        response = await fetch(currentUrl.toString(), {
          signal: AbortSignal.timeout(15_000),
          redirect: 'manual'
        })
      } catch {
        return reply.code(502).send({ message: 'Failed to fetch remote image.' })
      }

      const locationHeader = response.headers.get('location')
      const isRedirect =
        response.status >= 300 &&
        response.status < 400 &&
        typeof locationHeader === 'string' &&
        locationHeader.length > 0
      if (!isRedirect) {
        break
      }

      currentUrl = new URL(locationHeader, currentUrl)
      response.body?.cancel()
    }

    if (!response) {
      return reply.code(502).send({ message: 'Failed to fetch remote image.' })
    }

    if (!response.ok) {
      return reply.code(502).send({
        message: `Remote image request failed with status ${response.status}.`
      })
    }

    const contentType = response.headers.get('content-type')?.toLowerCase() ?? ''
    if (!contentType.startsWith('image/')) {
      return reply.code(415).send({ message: 'Remote resource is not an image.' })
    }

    const contentLengthHeader = response.headers.get('content-length')
    if (contentLengthHeader) {
      const declaredSize = Number.parseInt(contentLengthHeader, 10)
      if (Number.isFinite(declaredSize) && declaredSize > MAX_PROXY_IMAGE_BYTES) {
        return reply.code(413).send({ message: 'Remote image is too large.' })
      }
    }

    if (!response.body) {
      return reply.code(502).send({ message: 'Remote image response body is empty.' })
    }

    const reader = response.body.getReader()
    const chunks: Buffer[] = []
    let totalBytes = 0

    while (true) {
      const { done, value } = await reader.read()
      if (done) {
        break
      }

      if (!value) {
        continue
      }

      totalBytes += value.byteLength
      if (totalBytes > MAX_PROXY_IMAGE_BYTES) {
        await reader.cancel()
        return reply.code(413).send({ message: 'Remote image is too large.' })
      }

      chunks.push(Buffer.from(value))
    }

    const imageBuffer = Buffer.concat(chunks, totalBytes)
    const cacheControl = response.headers.get('cache-control')
    const etag = response.headers.get('etag')
    const lastModified = response.headers.get('last-modified')

    reply.header('Content-Type', contentType)
    reply.header('Content-Length', imageBuffer.length.toString())
    reply.header('Cache-Control', cacheControl ?? 'public, max-age=3600')
    if (etag) {
      reply.header('ETag', etag)
    }
    if (lastModified) {
      reply.header('Last-Modified', lastModified)
    }

    return reply.send(imageBuffer)
  })

  fastify.get('/events', async (request, reply) => {
    const requestOrigin = request.headers.origin?.trim()
    const responseHeaders: Record<string, string> = {
      'Content-Type': 'text/event-stream',
      'Cache-Control': 'no-cache',
      Connection: 'keep-alive',
      'X-Accel-Buffering': 'no',
      'Access-Control-Allow-Origin': requestOrigin || '*'
    }

    if (requestOrigin) {
      responseHeaders.Vary = 'Origin'
    }

    reply.hijack()
    reply.raw.writeHead(200, responseHeaders)

    const response = reply.raw as ServerResponse
    sseHub.addClient(response)

    request.raw.on('close', () => {
      sseHub.removeClient(response)
    })
  })

  fastify.all('/rpc/*', async (request, reply) => {
    await rpcHandler.handle(request, reply, {
      prefix: '/rpc'
    })
  })

  fastify.all('/docs', async (request, reply) => {
    await openApiHandler.handle(request, reply, {
      prefix: '/'
    })
  })

  fastify.all('/openapi.json', async (request, reply) => {
    await openApiHandler.handle(request, reply, {
      prefix: '/'
    })
  })

  fastify.addHook('onClose', async () => {
    sseHub.closeAll()
  })

  return fastify
}


================================================
FILE: apps/api/tsconfig.json
================================================
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "Bundler",
    "strict": true,
    "skipLibCheck": true,
    "noEmit": true,
    "types": ["node"]
  },
  "include": ["src/**/*"]
}


================================================
FILE: apps/desktop/build/after-pack.cjs
================================================
const { execFileSync } = require('node:child_process')
const fs = require('node:fs')
const path = require('node:path')

const BINARIES = [
  'yt-dlp_macos',
  path.join('ffmpeg', 'ffmpeg'),
  path.join('ffmpeg', 'ffprobe'),
  'deno'
]

const findAppBundle = (appOutDir) => {
  const entries = fs.readdirSync(appOutDir)
  const app = entries.find((entry) => entry.endsWith('.app'))
  return app ? path.join(appOutDir, app) : null
}

const resolveSigningIdentity = () =>
  process.env.CSC_NAME || process.env.APPLE_SIGNING_IDENTITY || '-'

const signBinary = (targetPath, entitlementsPath) => {
  const identity = resolveSigningIdentity()
  const args = ['--force', '--sign', identity, '--entitlements', entitlementsPath]

  if (identity !== '-') {
    args.push('--options', 'runtime', '--timestamp')
  }

  args.push(targetPath)
  execFileSync('codesign', args, { stdio: 'inherit' })
}

exports.default = async function afterPack(context) {
  if (context.electronPlatformName !== 'darwin') {
    return
  }

  const appBundle = findAppBundle(context.appOutDir)
  if (!appBundle) {
    console.warn('afterPack: No .app bundle found, skipping tool signing.')
    return
  }

  const resourcesPath = path.join(
    appBundle,
    'Contents',
    'Resources',
    'app.asar.unpacked',
    'resources'
  )

  const entitlementsPath = path.resolve(__dirname, 'entitlements.mac.plist')

  for (const binary of BINARIES) {
    const targetPath = path.join(resourcesPath, binary)
    if (!fs.existsSync(targetPath)) {
      console.warn(`afterPack: Missing ${binary}, skipping.`)
      continue
    }
    console.log(`afterPack: Signing ${binary} with entitlements.`)
    signBinary(targetPath, entitlementsPath)
  }
}


================================================
FILE: apps/desktop/build/entitlements.mac.plist
================================================
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
  <dict>
    <key>com.apple.security.cs.allow-jit</key>
    <true/>
    <key>com.apple.security.cs.allow-unsigned-executable-memory</key>
    <true/>
    <key>com.apple.security.cs.allow-dyld-environment-variables</key>
    <true/>
    <key>com.apple.security.cs.disable-library-validation</key>
    <true/>
  </dict>
</plist>


================================================
FILE: apps/desktop/changelogs/CHANGELOG.fr.md
================================================
# Journal des modifications de VidBee

Cette page ne présente que les évolutions visibles par les utilisateurs, sans détails techniques.
Pour les notes de version complètes, consultez [GitHub Releases](https://github.com/nexmoe/VidBee/releases).

## [v1.3.4](https://github.com/nexmoe/VidBee/releases/tag/v1.3.4) - 2026-03-14
### Corrections de bugs
- Utilisation de la source Electron par defaut pendant le packaging afin d'ameliorer la fiabilite des builds macOS de release.

## [v1.3.3](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3) - 2026-03-14
### Mises a jour de fonctionnalites
- Amelioration du flux de publication afin de diffuser les builds preview separement des notifications de mise a jour en production.

### Corrections de bugs
- Reactivation de npm rebuild pendant le packaging Electron afin de preparer plus fiablement les dependances natives dans les builds de release.
- Amelioration du bundling desktop pour inclure plus regulierement les dependances partagees du workspace dans les versions publiees.

## [v1.3.3-preview.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.1) - 2026-03-14
### Corrections de bugs
- Reactivation de npm rebuild pendant le packaging Electron pour preparer plus fiablement les dependances natives dans les builds de release.

## [v1.3.3-preview.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.0) - 2026-03-14
### Mises a jour de fonctionnalites
- Ajout d'un canal de publication preview pour diffuser les builds de test sans declencher les mises a jour du site de production.

### Corrections de bugs
- Amelioration du bundling desktop afin d'inclure plus regulierement les dependances partagees du workspace dans les builds publies.

## [v1.3.2](https://github.com/nexmoe/VidBee/releases/tag/v1.3.2) - 2026-03-14
### Corrections de bugs
- Amelioration de la fiabilite du packaging desktop afin d'inclure plus regulierement les composants de telechargement partages.

## [v1.3.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.1) - 2026-03-14
### Mises a jour de fonctionnalites
- Ajout des editions Web et API, avec des capacites de telechargement partagees et un comportement des reglages harmonise.
- Ajout de la prise en charge de l'envoi de fichiers Cookie et de configuration depuis les reglages.
- Migration de l'historique des telechargements vers SQLite pour une meilleure fiabilite et une meilleure coherence multi-plateforme.
- Ajout d'un flux partage pour l'ajout d'URL dans les boites de dialogue de telechargement et amelioration de la visibilite du curseur en theme sombre.

### Corrections de bugs
- Amelioration de la robustesse et des diagnostics du processus d'initialisation des binaires embarques sur desktop.
- Correction du saut de curseur dans le champ de profil des reglages.
- Correction de la validation du dossier de telechargement sous Linux lors de la selection d'un dossier existant non vide.
- Amelioration de la coherence des localisations, y compris des corrections de traduction chinoise.

## [v1.3.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.0) - 2026-02-15
### Mises a jour de fonctionnalites
- Ajout de nouvelles actions en un clic pour coller un lien et lancer le téléchargement plus vite.
- Ajout de la prise en charge des langues française, russe et turque dans l'application.
- Le format de conteneur sélectionné est désormais respecté de manière plus cohérente.

### Corrections de bugs
- Amélioration de la compatibilité des téléchargements pour YouTube et les scénarios de repli de format.
- Les réglages et la documentation ont été améliorés, avec des indications plus claires pour les rapports de bug et le RSS.

## [v1.2.4](https://github.com/nexmoe/VidBee/releases/tag/v1.2.4) - 2026-01-24
### Mises a jour de fonctionnalites
- Le flux de téléchargement en un clic est plus direct et demande moins d'étapes.
- Un onglet Cookie dédié a été ajouté dans les réglages pour simplifier les actions liées au compte.
- Les points d'entrée FAQ sont plus clairs et les messages d'erreur sont plus faciles à comprendre.

### Corrections de bugs
- Les indications RSS sont plus claires, surtout pour les nouveaux utilisateurs.

## [v1.2.3](https://github.com/nexmoe/VidBee/releases/tag/v1.2.3) - 2026-01-23
### Mises a jour de fonctionnalites
- Le chargement des playlists est plus stable et ne compresse plus l'interface.
- Le guide d'utilisation des cookies inclut désormais des exemples plus clairs.

## [v1.2.2](https://github.com/nexmoe/VidBee/releases/tag/v1.2.2) - 2026-01-21
### Mises a jour de fonctionnalites
- Les actions liées au téléchargement sont plus faciles à trouver.
- Ajout d'une option pour inclure ou retirer le filigrane lors du partage.
- Les interactions de téléchargement sont plus cohérentes dans l'ensemble.

## [v1.2.1](https://github.com/nexmoe/VidBee/releases/tag/v1.2.1) - 2026-01-20
### Mises a jour de fonctionnalites
- Les éléments avec le même titre dans les playlists sont plus faciles à distinguer.
- Il est plus simple de trouver les journaux et les fichiers liés lors du dépannage.

### Corrections de bugs
- Les notifications de téléchargement sont moins intrusives.
- Les liens et indications des abonnements sont plus fiables.

## [v1.2.0](https://github.com/nexmoe/VidBee/releases/tag/v1.2.0) - 2026-01-17
### Mises a jour de fonctionnalites
- Ajout d'actions rapides pour tout sélectionner et vider l'historique des téléchargements.
- Le comportement lors de la réduction et de la réouverture est plus fluide.
- Les doublons dans les abonnements sont réduits.
- Les pages Playlist et Réglages sont plus simples à utiliser.

### Corrections de bugs
- La reprise après une interruption de téléchargement est plus fiable.

## [v1.1.12](https://github.com/nexmoe/VidBee/releases/tag/v1.1.12) - 2026-01-15
### Mises a jour de fonctionnalites
- Le comportement du dossier de téléchargement dans les réglages est plus prévisible.

### Corrections de bugs
- Les rapports de retour contiennent désormais des informations d'appui plus claires.

## [v1.1.11](https://github.com/nexmoe/VidBee/releases/tag/v1.1.11) - 2026-01-14
### Mises a jour de fonctionnalites
- Les flux de téléchargement et la mise en page sont plus clairs.
- La navigation des abonnements est plus fluide.
- Les réglages par défaut sont plus adaptés à un usage quotidien.

### Corrections de bugs
- Les messages d'erreur proposent des étapes suivantes plus claires.

## [v1.1.10](https://github.com/nexmoe/VidBee/releases/tag/v1.1.10) - 2026-01-12
### Mises a jour de fonctionnalites
- L'installation et la mise à jour sur macOS sont plus stables.

## [v1.1.8](https://github.com/nexmoe/VidBee/releases/tag/v1.1.8) - 2026-01-12
### Mises a jour de fonctionnalites
- Les détails de progression des téléchargements sont plus lisibles.

### Corrections de bugs
- Les notifications de mise à jour localisées sont plus claires.

## [v1.1.7](https://github.com/nexmoe/VidBee/releases/tag/v1.1.7) - 2026-01-11
### Mises a jour de fonctionnalites
- Davantage d'options de préférences de sortie média ont été ajoutées.
- La configuration initiale et l'utilisation quotidienne sont plus fluides.

## [v1.1.6](https://github.com/nexmoe/VidBee/releases/tag/v1.1.6) - 2026-01-11
### Mises a jour de fonctionnalites
- Les flux liés aux informations vidéo locales sont plus faciles à utiliser.
- La gestion des profils de cookies est plus stable et plus prévisible.

## [v1.1.5](https://github.com/nexmoe/VidBee/releases/tag/v1.1.5) - 2026-01-10
### Mises a jour de fonctionnalites
- Correction de problèmes connus dans les réglages avancés.

### Corrections de bugs
- Amélioration de la stabilité du chargement des couvertures distantes.
- La sélection des couvertures d'abonnement est plus fiable.

## [v1.1.4](https://github.com/nexmoe/VidBee/releases/tag/v1.1.4) - 2026-01-09
### Mises a jour de fonctionnalites
- Le comportement de la fenêtre au démarrage est plus naturel.
- Le comportement global des réglages est plus cohérent.

## [v1.1.3](https://github.com/nexmoe/VidBee/releases/tag/v1.1.3) - 2026-01-02
### Mises a jour de fonctionnalites
- Le statut de mise à jour est plus visible sur la page About.

### Corrections de bugs
- La sélection de format est plus fiable selon les scénarios.

## [v1.1.2](https://github.com/nexmoe/VidBee/releases/tag/v1.1.2) - 2025-12-26
### Mises a jour de fonctionnalites
- La disponibilité du téléchargement a été rétablie pour davantage de sites.
- Le flux de signalement des problèmes est plus simple.

## [v1.1.1](https://github.com/nexmoe/VidBee/releases/tag/v1.1.1) - 2025-12-26
### Mises a jour de fonctionnalites
- Les notifications de mise à jour sont moins perturbantes.
- Les textes et liens de la page About sont plus clairs.
- Les interactions du panneau de téléchargement sont plus fluides.

## [v1.1.0](https://github.com/nexmoe/VidBee/releases/tag/v1.1.0) - 2025-12-20
### Mises a jour de fonctionnalites
- Ajout d'actions groupées pour nettoyer l'historique des téléchargements.
- L'ouverture des liens de tâche de téléchargement est plus prévisible.
- Ajout de la prise en charge des dossiers de téléchargement personnalisés.
- La boîte de dialogue de configuration RSS est plus simple à comprendre et à remplir.

## [v1.0.2](https://github.com/nexmoe/VidBee/releases/tag/v1.0.2) - 2025-12-06
### Mises a jour de fonctionnalites
- La saisie des chemins est plus tolérante au quotidien.

### Corrections de bugs
- Ajout de plus d'options de compatibilité pour davantage de scénarios d'usage.

## [v1.0.1](https://github.com/nexmoe/VidBee/releases/tag/v1.0.1) - 2025-11-16
### Mises a jour de fonctionnalites
- Ajout de la prise en charge du lancement automatique.
- La prise en charge des langues a été encore élargie.

## [v1.0.0](https://github.com/nexmoe/VidBee/releases/tag/v1.0.0) - 2025-11-15
### Mises a jour de fonctionnalites
- Première version majeure stable de VidBee.
- Ajout des téléchargements via abonnements RSS.
- La navigation et le flux général de l'interface sont plus clairs.
- L'historique et l'aperçu des médias ont été améliorés.

## [v0.3.5](https://github.com/nexmoe/VidBee/releases/tag/v0.3.5) - 2025-11-08
### Mises a jour de fonctionnalites
- Les textes et messages du téléchargement en un clic sont plus faciles à comprendre.
- Le style visuel est plus cohérent.

## [v0.3.4](https://github.com/nexmoe/VidBee/releases/tag/v0.3.4) - 2025-11-03
### Mises a jour de fonctionnalites
- Les messages de mise à jour et l'affichage des options de téléchargement sont plus clairs.

## [v0.3.3](https://github.com/nexmoe/VidBee/releases/tag/v0.3.3) - 2025-11-02
### Corrections de bugs
- La stabilité du traitement des téléchargements a été améliorée dans davantage de scénarios.

## [v0.3.2](https://github.com/nexmoe/VidBee/releases/tag/v0.3.2) - 2025-10-31
### Mises a jour de fonctionnalites
- L'expérience de distribution multi-appareils a été améliorée.

## [v0.3.1](https://github.com/nexmoe/VidBee/releases/tag/v0.3.1) - 2025-10-30
### Mises a jour de fonctionnalites
- L'expérience Linux est plus conviviale.
- Ajout de notifications de nouvelles versions pour des mises à niveau plus rapides.

## [v0.3.0](https://github.com/nexmoe/VidBee/releases/tag/v0.3.0) - 2025-10-29
### Mises a jour de fonctionnalites
- Ajout de la prise en charge du téléchargement de playlists.
- Ajout de contrôles pour réduire les perturbations sur le bureau.

## [v0.2.2](https://github.com/nexmoe/VidBee/releases/tag/v0.2.2) - 2025-10-27
### Mises a jour de fonctionnalites
- Poursuite du peaufinage UX pendant la phase de préversion.

## [v0.2.1](https://github.com/nexmoe/VidBee/releases/tag/v0.2.1) - 2025-10-26
### Mises a jour de fonctionnalites
- Poursuite du peaufinage UX pendant la phase de préversion.

## [v0.2.0](https://github.com/nexmoe/VidBee/releases/tag/v0.2.0) - 2025-10-25
### Mises a jour de fonctionnalites
- Poursuite du peaufinage UX pendant la phase de préversion.

## [v0.1.8](https://github.com/nexmoe/VidBee/releases/tag/v0.1.8) - 2025-10-24
### Mises a jour de fonctionnalites
- Le programme de préversion publique a démarré.

## [v0.1.7](https://github.com/nexmoe/VidBee/releases/tag/v0.1.7) - 2025-10-24
### Mises a jour de fonctionnalites
- Ajout de la prise en charge de la mise a jour automatique et amélioration des consignes de publication dans la documentation.
- Amélioration de la documentation du projet, y compris les captures d'écran et le guide de contribution.

### Corrections de bugs
- Simplification de la gestion des chemins de téléchargement et suppression de la logique de chemin de sortie inutilisée.

## [v0.1.6](https://github.com/nexmoe/VidBee/releases/tag/v0.1.6) - 2025-10-23
### Corrections de bugs
- Suppression d'une étape inutile de création de dossier dans le workflow de publication.

## [v0.1.5](https://github.com/nexmoe/VidBee/releases/tag/v0.1.5) - 2025-10-23
### Corrections de bugs
- Amélioration du workflow de publication pour télécharger les binaires yt-dlp lors du packaging multiplateforme.

## [v0.1.4](https://github.com/nexmoe/VidBee/releases/tag/v0.1.4) - 2025-10-23
### Corrections de bugs
- Mise à jour du workflow de publication pour cibler uniquement les builds Windows.

## [v0.1.3](https://github.com/nexmoe/VidBee/releases/tag/v0.1.3) - 2025-10-23
### Corrections de bugs
- Simplification des étapes de build de publication et de la gestion des artefacts dans CI.
- Ajustement des déclencheurs CI: l'automatisation des pull requests ne s'exécute que pour `main`.

## [v0.1.2](https://github.com/nexmoe/VidBee/releases/tag/v0.1.2) - 2025-10-23
### Corrections de bugs
- Définition explicite du shell pour l'étape de build dans le workflow de publication.

## [v0.1.1](https://github.com/nexmoe/VidBee/releases/tag/v0.1.1) - 2025-10-23
### Mises a jour de fonctionnalites
- Itération de release précoce sans changement utilisateur supplémentaire documenté.

## [v0.1.0](https://github.com/nexmoe/VidBee/releases/tag/v0.1.0) - 2025-10-23
### Mises a jour de fonctionnalites
- Point de départ de la première release publique.


================================================
FILE: apps/desktop/changelogs/CHANGELOG.md
================================================
# VidBee Changelog

This page only includes user-visible updates and avoids implementation details.
For full release notes, see [GitHub Releases](https://github.com/nexmoe/VidBee/releases).

## [v1.3.5](https://github.com/nexmoe/VidBee/releases/tag/v1.3.5) - 2026-03-18
### Requirement Updates
- Updated the bundled yt-dlp runtime from v2026.03.13 to v2026.03.17 so site compatibility stays current.
## [v1.3.4](https://github.com/nexmoe/VidBee/releases/tag/v1.3.4) - 2026-03-14
### Bug Fixes
- Improved macOS release build reliability by using the default Electron download source during packaging.

## [v1.3.3](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3) - 2026-03-14
### Requirement Updates
- Improved the release pipeline so preview builds can be published separately from production update notifications.

### Bug Fixes
- Restored npm rebuilds during Electron packaging so native dependencies are prepared more reliably in release builds.
- Bundled shared workspace packages more consistently in desktop builds.

## [v1.3.3-preview.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.1) - 2026-03-14
### Bug Fixes
- Restored npm rebuilds during Electron packaging so native dependencies are prepared more reliably in release builds.

## [v1.3.3-preview.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.0) - 2026-03-14
### Requirement Updates
- Added a preview release channel so test builds can be published without triggering production site updates.

### Bug Fixes
- Bundled shared workspace packages more consistently in desktop builds.

## [v1.3.2](https://github.com/nexmoe/VidBee/releases/tag/v1.3.2) - 2026-03-14
### Bug Fixes
- Improved desktop packaging reliability so shared downloader components are bundled more consistently.

## [v1.3.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.1) - 2026-03-14
### Requirement Updates
- Added web and API editions with shared downloader capabilities and aligned settings behavior.
- Added support for uploading Cookie and config files from Settings.
- Migrated download history storage to SQLite for better reliability and cross-platform consistency.
- Added a shared add-url popover flow in download dialogs and refined dark-theme thumb visibility.

### Bug Fixes
- Improved bundled binary setup resilience and diagnostics in desktop startup scripts.
- Fixed profile input cursor jumping in Settings.
- Fixed Linux download-directory validation when selecting existing non-empty folders.
- Polished localization consistency, including Chinese translation corrections.

## [v1.3.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.0) - 2026-02-15
### Requirement Updates
- Added new one-click actions so you can paste and start downloading faster.
- Added French, Russian, and Turkish app localization support.
- Completed tray localization for Turkish.
- Reorganized Cookie settings into three clearer groups.
- Added RSSHub portal links to the RSS documentation.

### Bug Fixes
- Improved download compatibility for YouTube and format fallback scenarios.
- Download output now follows the selected container format more consistently.
- Settings and docs were improved, including clearer bug report and RSS guidance.
- Added fallback handling when requested download formats are unavailable.
- Synced missing Turkish locale keys.
- Reduced release pipeline friction with check and workflow fixes.

## [v1.2.4](https://github.com/nexmoe/VidBee/releases/tag/v1.2.4) - 2026-01-24
### Requirement Updates
- The one-click download flow is now more direct with fewer steps.
- A dedicated Cookie tab was added in Settings for easier account-related actions.

### Bug Fixes
- FAQ entry points are clearer and error messages are easier to understand.
- RSS guidance is clearer, especially for new users.

## [v1.2.3](https://github.com/nexmoe/VidBee/releases/tag/v1.2.3) - 2026-01-23
### Bug Fixes
- Playlist loading is more stable and no longer causes layout compression issues.
- Cookie usage guidance now includes clearer examples.

## [v1.2.2](https://github.com/nexmoe/VidBee/releases/tag/v1.2.2) - 2026-01-21
### Requirement Updates
- Download-related actions are easier to access.
- Added an option to include or remove watermarks when sharing.

### Bug Fixes
- Download interactions are more consistent overall.

## [v1.2.1](https://github.com/nexmoe/VidBee/releases/tag/v1.2.1) - 2026-01-20
### Requirement Updates
- Items with the same title in playlists are easier to distinguish.
- It is easier to find logs and related files when troubleshooting.
- Added docs site improvements, including i18n support, sitemap generation, and protocol documentation.

### Bug Fixes
- Download notifications are less intrusive.
- Subscription links and guidance are more reliable.
- Resolved TypeScript build issues in the documentation project.

## [v1.2.0](https://github.com/nexmoe/VidBee/releases/tag/v1.2.0) - 2026-01-17
### Requirement Updates
- Added faster ways to select all and clear download history.
- Playlist and Settings pages are easier to use.
- Minimize-to-tray behavior is now the default.
- Feedback reporting now warns when GitHub issue links are too long.

### Bug Fixes
- Minimize and reopen behavior feels smoother.
- Duplicate items in subscriptions are reduced.
- Resume behavior after interrupted downloads is more reliable.
- Playlist list height is constrained more reliably.
- Feedback issue links are cleaner and more stable.
- Added clearer Windows-only Cookie guidance.
- Strengthened ffmpeg/ffprobe bundle checks.

## [v1.1.12](https://github.com/nexmoe/VidBee/releases/tag/v1.1.12) - 2026-01-15
### Bug Fixes
- Download folder behavior in Settings is more predictable.
- Feedback reports now have clearer supporting information.

## [v1.1.11](https://github.com/nexmoe/VidBee/releases/tag/v1.1.11) - 2026-01-14
### Requirement Updates
- Default settings are better for everyday use.
- Extension error pages and branding were refined.
- Download error panels now include richer troubleshooting links.
- Subscription tabs support horizontal scrolling.

### Bug Fixes
- Download flows and page layouts are clearer.
- Subscription browsing feels smoother.
- Error messages now provide clearer next-step suggestions.
- Added safeguards for Bilibili subtitle embedding failures.
- Missing locale translation entries were filled.
- Embedded thumbnail behavior is safer by default.

## [v1.1.10](https://github.com/nexmoe/VidBee/releases/tag/v1.1.10) - 2026-01-12
### Bug Fixes
- Installation and update experience on macOS is more stable.
- Bundled tools are now signed during macOS builds.
- DMG notarization was tightened in CI for better release reliability.

## [v1.1.8](https://github.com/nexmoe/VidBee/releases/tag/v1.1.8) - 2026-01-12
### Requirement Updates
- Download progress details are easier to read.

### Bug Fixes
- Localized update notifications are clearer.

## [v1.1.7](https://github.com/nexmoe/VidBee/releases/tag/v1.1.7) - 2026-01-11
### Requirement Updates
- Added more media output preference options.

### Bug Fixes
- First-time setup and daily usage flow are smoother.

## [v1.1.6](https://github.com/nexmoe/VidBee/releases/tag/v1.1.6) - 2026-01-11
### Requirement Updates
- Local video information workflows are easier to use.

### Bug Fixes
- Cookie profile management is more stable and predictable.

## [v1.1.5](https://github.com/nexmoe/VidBee/releases/tag/v1.1.5) - 2026-01-10
### Bug Fixes
- Fixed known issues in Advanced Settings.
- Improved remote cover loading stability.
- Subscription cover selection is more reliable.

## [v1.1.4](https://github.com/nexmoe/VidBee/releases/tag/v1.1.4) - 2026-01-09
### Requirement Updates
- Startup window behavior feels more natural.

### Bug Fixes
- Settings behavior is more consistent overall.

## [v1.1.3](https://github.com/nexmoe/VidBee/releases/tag/v1.1.3) - 2026-01-02
### Requirement Updates
- Update status is easier to spot on the About page.

### Bug Fixes
- Format selection is more reliable across scenarios.

## [v1.1.2](https://github.com/nexmoe/VidBee/releases/tag/v1.1.2) - 2025-12-26
### Requirement Updates
- Issue reporting flow is simpler.
- Added issue templates and streamlined bug report forms.

### Bug Fixes
- Restored download availability for more sites.
- CI download steps were hardened for better reliability.

## [v1.1.1](https://github.com/nexmoe/VidBee/releases/tag/v1.1.1) - 2025-12-26
### Requirement Updates
- Update notifications are less disruptive.
- Added JavaScript runtime support for yt-dlp integration.
- Advanced options panels now use smoother animations.

### Bug Fixes
- About page text and links are clearer.
- Download panel interactions feel smoother.
- Electron language resources were limited to English for consistency.

## [v1.1.0](https://github.com/nexmoe/VidBee/releases/tag/v1.1.0) - 2025-12-20
### Requirement Updates
- Added bulk actions for download history cleanup.
- Opening download task links now behaves more predictably.
- Added support for custom download folders.

### Bug Fixes
- RSS setup dialog is easier to understand and fill.

## [v1.0.2](https://github.com/nexmoe/VidBee/releases/tag/v1.0.2) - 2025-12-06
### Bug Fixes
- Added more compatibility options for wider usage scenarios.
- Path input is more forgiving in daily usage.

## [v1.0.1](https://github.com/nexmoe/VidBee/releases/tag/v1.0.1) - 2025-11-16
### Requirement Updates
- Added auto-launch support.
- Language support was further expanded.

## [v1.0.0](https://github.com/nexmoe/VidBee/releases/tag/v1.0.0) - 2025-11-15
### Requirement Updates
- Added RSS subscription downloads.
- Added site icons in supported source lists.
- Introduced remote image loading with caching for media previews.
- Sidebar interactions were refined with draggable title-bar behavior.

### Bug Fixes
- First major stable release of VidBee.
- Navigation and overall interface flow became clearer.
- History and media preview experience were improved.
- Migrated history storage to SQLite (Drizzle) for more reliable data handling.

## [v0.3.5](https://github.com/nexmoe/VidBee/releases/tag/v0.3.5) - 2025-11-08
### Bug Fixes
- One-click download copy and feedback prompts are easier to understand.
- Visual style is more consistent.

## [v0.3.4](https://github.com/nexmoe/VidBee/releases/tag/v0.3.4) - 2025-11-03
### Bug Fixes
- Update prompts and download option display are clearer.

## [v0.3.3](https://github.com/nexmoe/VidBee/releases/tag/v0.3.3) - 2025-11-02
### Bug Fixes
- Download processing stability improved in more scenarios.

## [v0.3.2](https://github.com/nexmoe/VidBee/releases/tag/v0.3.2) - 2025-10-31
### Bug Fixes
- Multi-device distribution experience was improved.

## [v0.3.1](https://github.com/nexmoe/VidBee/releases/tag/v0.3.1) - 2025-10-30
### Requirement Updates
- Linux experience is more user-friendly.
- Added version update notifications for faster upgrades.

## [v0.3.0](https://github.com/nexmoe/VidBee/releases/tag/v0.3.0) - 2025-10-29
### Requirement Updates
- Added playlist download support.
- Added controls to reduce desktop disruption.

## [v0.2.2](https://github.com/nexmoe/VidBee/releases/tag/v0.2.2) - 2025-10-27
### Bug Fixes
- Continued UX polishing during the preview stage.

## [v0.2.1](https://github.com/nexmoe/VidBee/releases/tag/v0.2.1) - 2025-10-26
### Bug Fixes
- Continued UX polishing during the preview stage.

## [v0.2.0](https://github.com/nexmoe/VidBee/releases/tag/v0.2.0) - 2025-10-25
### Bug Fixes
- Continued UX polishing during the preview stage.

## [v0.1.8](https://github.com/nexmoe/VidBee/releases/tag/v0.1.8) - 2025-10-24
### Requirement Updates
- Public preview period started.

## [v0.1.7](https://github.com/nexmoe/VidBee/releases/tag/v0.1.7) - 2025-10-24
### Requirement Updates
- Added auto-updater support and improved release guidance in documentation.
- Improved project documentation, including screenshots and contribution guidelines.

### Bug Fixes
- Simplified download path handling and removed unused output path logic.

## [v0.1.6](https://github.com/nexmoe/VidBee/releases/tag/v0.1.6) - 2025-10-23
### Bug Fixes
- Removed an unnecessary directory creation step in the release workflow.

## [v0.1.5](https://github.com/nexmoe/VidBee/releases/tag/v0.1.5) - 2025-10-23
### Bug Fixes
- Improved the release workflow to download yt-dlp binaries for cross-platform packaging.

## [v0.1.4](https://github.com/nexmoe/VidBee/releases/tag/v0.1.4) - 2025-10-23
### Bug Fixes
- Updated the release workflow to target Windows builds only.

## [v0.1.3](https://github.com/nexmoe/VidBee/releases/tag/v0.1.3) - 2025-10-23
### Bug Fixes
- Simplified release build steps and artifact handling in CI.
- Adjusted CI triggers so pull-request automation runs only for `main`.

## [v0.1.2](https://github.com/nexmoe/VidBee/releases/tag/v0.1.2) - 2025-10-23
### Bug Fixes
- Set an explicit shell for the build step in the release workflow.

## [v0.1.1](https://github.com/nexmoe/VidBee/releases/tag/v0.1.1) - 2025-10-23
### Requirement Updates
- Early release iteration with no additional user-visible changes recorded.

## [v0.1.0](https://github.com/nexmoe/VidBee/releases/tag/v0.1.0) - 2025-10-23
### Requirement Updates
- Initial public release baseline.


================================================
FILE: apps/desktop/changelogs/CHANGELOG.ru.md
================================================
# Журнал изменений VidBee

На этой странице указаны только заметные для пользователей изменения, без технических деталей.
Полные заметки к релизам доступны в [GitHub Releases](https://github.com/nexmoe/VidBee/releases).

## [v1.3.4](https://github.com/nexmoe/VidBee/releases/tag/v1.3.4) - 2026-03-14
### Исправления ошибок
- Для упаковки снова используется стандартный источник Electron, что повышает надежность macOS release-сборок.

## [v1.3.3](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3) - 2026-03-14
### Обновления функций
- Улучшен процесс публикации релизов: preview-сборки теперь публикуются отдельно от уведомлений об обновлениях для production.

### Исправления ошибок
- Возвращена сборка npm rebuild при упаковке Electron, чтобы нативные зависимости надежнее подготавливались в релизных сборках.
- Улучшено включение общих workspace-пакетов в desktop-сборки для более стабильных релизных артефактов.

## [v1.3.3-preview.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.1) - 2026-03-14
### Исправления ошибок
- Возвращена сборка npm rebuild при упаковке Electron, чтобы нативные зависимости надежнее подготавливались в релизных сборках.

## [v1.3.3-preview.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.0) - 2026-03-14
### Обновления функций
- Добавлен preview-канал релизов, чтобы тестовые сборки публиковались отдельно и не запускали обновление production-сайта.

### Исправления ошибок
- Улучшено включение общих workspace-пакетов в desktop-сборки для более стабильных релизных артефактов.

## [v1.3.2](https://github.com/nexmoe/VidBee/releases/tag/v1.3.2) - 2026-03-14
### Исправления ошибок
- Повышена надежность desktop-пакетирования, чтобы общие компоненты загрузки стабильнее попадали в релизные сборки.

## [v1.3.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.1) - 2026-03-14
### Обновления функций
- Добавлены веб- и API-редакции с общими возможностями загрузки и согласованным поведением настроек.
- Добавлена поддержка загрузки файлов Cookie и конфигурации из страницы настроек.
- История загрузок перенесена в SQLite для более высокой надежности и лучшей кроссплатформенной согласованности.
- Добавлен общий сценарий добавления URL в диалогах загрузки и улучшена видимость ползунка в темной теме.

### Исправления ошибок
- Повышена устойчивость и информативность диагностики при инициализации встроенных бинарных файлов в desktop.
- Исправлено прыгание курсора в поле профиля на странице настроек.
- Исправлена проверка каталога загрузки в Linux при выборе существующей непустой папки.
- Улучшена согласованность локализации, включая исправления китайских переводов.

## [v1.3.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.0) - 2026-02-15
### Обновления функций
- Добавлены новые действия в один клик: вставить ссылку и быстрее начать загрузку.
- Добавлена локализация приложения на французский, русский и турецкий языки.
- Выбранный формат контейнера теперь соблюдается более последовательно.

### Исправления ошибок
- Улучшена совместимость загрузок для YouTube и сценариев с резервным выбором формата.
- Улучшены настройки и документация, включая более понятные подсказки для отчётов об ошибках и RSS.

## [v1.2.4](https://github.com/nexmoe/VidBee/releases/tag/v1.2.4) - 2026-01-24
### Обновления функций
- Сценарий загрузки в один клик стал прямее и требует меньше шагов.
- В настройках появилась отдельная вкладка Cookie для более удобных действий с аккаунтами.
- Подсказки по RSS стали яснее, особенно для новых пользователей.

### Исправления ошибок
- Раздел FAQ стал заметнее, а сообщения об ошибках понятнее.

## [v1.2.3](https://github.com/nexmoe/VidBee/releases/tag/v1.2.3) - 2026-01-23
### Исправления ошибок
- Загрузка плейлистов стала стабильнее и больше не сжимает интерфейс.
- Инструкции по Cookie теперь содержат более понятные примеры.

## [v1.2.2](https://github.com/nexmoe/VidBee/releases/tag/v1.2.2) - 2026-01-21
### Обновления функций
- Действия, связанные со скачиванием, стало проще найти.
- Добавлена возможность включать или отключать водяной знак при публикации.
- Взаимодействия при скачивании стали более единообразными.

## [v1.2.1](https://github.com/nexmoe/VidBee/releases/tag/v1.2.1) - 2026-01-20
### Обновления функций
- Уведомления во время загрузок стали менее навязчивыми.
- Элементы с одинаковыми названиями в плейлистах теперь проще различать.

### Исправления ошибок
- При диагностике проблем стало легче находить логи и связанные файлы.
- Ссылки и подсказки для подписок стали надежнее.

## [v1.2.0](https://github.com/nexmoe/VidBee/releases/tag/v1.2.0) - 2026-01-17
### Обновления функций
- Добавлены быстрые действия для выбора всего и очистки истории загрузок.
- Поведение при сворачивании и повторном открытии стало плавнее.
- Количество дубликатов в подписках уменьшено.
- Страницы плейлистов и настроек стали удобнее.

### Исправления ошибок
- Возобновление после прерванной загрузки работает стабильнее.

## [v1.1.12](https://github.com/nexmoe/VidBee/releases/tag/v1.1.12) - 2026-01-15
### Исправления ошибок
- Поведение папки загрузок в настройках стало более предсказуемым.
- В отчетах об ошибках появилась более полезная сопроводительная информация.

## [v1.1.11](https://github.com/nexmoe/VidBee/releases/tag/v1.1.11) - 2026-01-14
### Обновления функций
- Просмотр подписок ощущается более плавным.
- Настройки по умолчанию лучше подходят для повседневного использования.

### Исправления ошибок
- Сценарии загрузки и структура страниц стали понятнее.
- Ошибки теперь сопровождаются более понятными подсказками о следующих шагах.

## [v1.1.10](https://github.com/nexmoe/VidBee/releases/tag/v1.1.10) - 2026-01-12
### Исправления ошибок
- Установка и обновление на macOS стали стабильнее.

## [v1.1.8](https://github.com/nexmoe/VidBee/releases/tag/v1.1.8) - 2026-01-12
### Обновления функций
- Детали прогресса загрузки стало проще читать.

### Исправления ошибок
- Локализованные уведомления об обновлениях стали понятнее.

## [v1.1.7](https://github.com/nexmoe/VidBee/releases/tag/v1.1.7) - 2026-01-11
### Обновления функций
- Добавлено больше настроек предпочтений для вывода медиа.
- Первичная настройка и ежедневные сценарии использования стали плавнее.

## [v1.1.6](https://github.com/nexmoe/VidBee/releases/tag/v1.1.6) - 2026-01-11
### Обновления функций
- Сценарии с локальной информацией о видео стали удобнее.

### Исправления ошибок
- Управление профилями Cookie стало стабильнее и предсказуемее.

## [v1.1.5](https://github.com/nexmoe/VidBee/releases/tag/v1.1.5) - 2026-01-10
### Исправления ошибок
- Исправлены известные проблемы в расширенных настройках.
- Улучшена стабильность загрузки удаленных обложек.
- Выбор обложек для подписок стал надежнее.

## [v1.1.4](https://github.com/nexmoe/VidBee/releases/tag/v1.1.4) - 2026-01-09
### Обновления функций
- Поведение окна при запуске стало более естественным.
- Поведение настроек в целом стало более последовательным.

## [v1.1.3](https://github.com/nexmoe/VidBee/releases/tag/v1.1.3) - 2026-01-02
### Обновления функций
- Статус обновлений на странице About теперь заметнее.

### Исправления ошибок
- Выбор форматов работает надежнее в разных сценариях.

## [v1.1.2](https://github.com/nexmoe/VidBee/releases/tag/v1.1.2) - 2025-12-26
### Исправления ошибок
- Восстановлена возможность загрузки для большего числа сайтов.
- Процесс отправки отчета об ошибке стал проще.

## [v1.1.1](https://github.com/nexmoe/VidBee/releases/tag/v1.1.1) - 2025-12-26
### Обновления функций
- Уведомления об обновлениях стали менее отвлекающими.
- Взаимодействия в панели загрузок стали более плавными.

### Исправления ошибок
- Тексты и ссылки на странице About стали понятнее.

## [v1.1.0](https://github.com/nexmoe/VidBee/releases/tag/v1.1.0) - 2025-12-20
### Обновления функций
- Добавлены массовые действия для очистки истории загрузок.
- Добавлена поддержка пользовательских папок загрузки.

### Исправления ошибок
- Открытие ссылок задач загрузки теперь ведет себя предсказуемее.
- Диалог настройки RSS стал понятнее и проще для заполнения.

## [v1.0.2](https://github.com/nexmoe/VidBee/releases/tag/v1.0.2) - 2025-12-06
### Обновления функций
- Ввод путей стал более гибким в ежедневном использовании.

### Исправления ошибок
- Добавлены дополнительные параметры совместимости для более широких сценариев использования.

## [v1.0.1](https://github.com/nexmoe/VidBee/releases/tag/v1.0.1) - 2025-11-16
### Обновления функций
- Добавлена поддержка автозапуска.
- Языковая поддержка была дополнительно расширена.

## [v1.0.0](https://github.com/nexmoe/VidBee/releases/tag/v1.0.0) - 2025-11-15
### Обновления функций
- Добавлены загрузки по RSS-подпискам.

### Исправления ошибок
- Первый крупный стабильный релиз VidBee.
- Навигация и общий поток интерфейса стали понятнее.
- Улучшен опыт работы с историей и предпросмотром медиа.

## [v0.3.5](https://github.com/nexmoe/VidBee/releases/tag/v0.3.5) - 2025-11-08
### Обновления функций
- Визуальный стиль стал более цельным.

### Исправления ошибок
- Тексты и подсказки в сценарии загрузки в один клик стали понятнее.

## [v0.3.4](https://github.com/nexmoe/VidBee/releases/tag/v0.3.4) - 2025-11-03
### Обновления функций
- Подсказки об обновлениях и отображение вариантов загрузки стали яснее.

## [v0.3.3](https://github.com/nexmoe/VidBee/releases/tag/v0.3.3) - 2025-11-02
### Исправления ошибок
- Стабильность обработки загрузок улучшена в большем числе сценариев.

## [v0.3.2](https://github.com/nexmoe/VidBee/releases/tag/v0.3.2) - 2025-10-31
### Исправления ошибок
- Улучшен опыт установки и использования на разных устройствах.

## [v0.3.1](https://github.com/nexmoe/VidBee/releases/tag/v0.3.1) - 2025-10-30
### Обновления функций
- Использование на Linux стало удобнее.
- Добавлены уведомления о новых версиях для более быстрых обновлений.

## [v0.3.0](https://github.com/nexmoe/VidBee/releases/tag/v0.3.0) - 2025-10-29
### Обновления функций
- Добавлена поддержка загрузки плейлистов.
- Добавлены настройки для снижения отвлекающих факторов на рабочем столе.

## [v0.2.2](https://github.com/nexmoe/VidBee/releases/tag/v0.2.2) - 2025-10-27
### Обновления функций
- Продолжена полировка пользовательского опыта на этапе предварительной версии.

## [v0.2.1](https://github.com/nexmoe/VidBee/releases/tag/v0.2.1) - 2025-10-26
### Обновления функций
- Продолжена полировка пользовательского опыта на этапе предварительной версии.

## [v0.2.0](https://github.com/nexmoe/VidBee/releases/tag/v0.2.0) - 2025-10-25
### Обновления функций
- Продолжена полировка пользовательского опыта на этапе предварительной версии.

## [v0.1.8](https://github.com/nexmoe/VidBee/releases/tag/v0.1.8) - 2025-10-24
### Обновления функций
- Начался период публичного предварительного просмотра.

## [v0.1.7](https://github.com/nexmoe/VidBee/releases/tag/v0.1.7) - 2025-10-24
### Обновления функций
- Добавлена поддержка автообновления и улучшены инструкции по релизам в документации.
- Улучшена проектная документация, включая скриншоты и руководство для контрибьюторов.

### Исправления ошибок
- Упрощена обработка путей загрузки и удалена неиспользуемая логика выходного пути.

## [v0.1.6](https://github.com/nexmoe/VidBee/releases/tag/v0.1.6) - 2025-10-23
### Исправления ошибок
- Удален лишний шаг создания директории в workflow публикации релиза.

## [v0.1.5](https://github.com/nexmoe/VidBee/releases/tag/v0.1.5) - 2025-10-23
### Исправления ошибок
- Улучшен workflow релиза: добавена загрузка бинарников yt-dlp для кроссплатформенной сборки.

## [v0.1.4](https://github.com/nexmoe/VidBee/releases/tag/v0.1.4) - 2025-10-23
### Исправления ошибок
- Обновлен workflow релиза: теперь собираются только Windows-версии.

## [v0.1.3](https://github.com/nexmoe/VidBee/releases/tag/v0.1.3) - 2025-10-23
### Исправления ошибок
- Упрощены шаги сборки релиза и обработка артефактов в CI.
- Скорректированы триггеры CI: автоматизация pull request запускается только для `main`.

## [v0.1.2](https://github.com/nexmoe/VidBee/releases/tag/v0.1.2) - 2025-10-23
### Исправления ошибок
- Для шага сборки в workflow релиза явно указан shell для более стабильного выполнения.

## [v0.1.1](https://github.com/nexmoe/VidBee/releases/tag/v0.1.1) - 2025-10-23
### Обновления функций
- Ранний итерационный релиз без дополнительных пользовательских изменений в заметках.

## [v0.1.0](https://github.com/nexmoe/VidBee/releases/tag/v0.1.0) - 2025-10-23
### Обновления функций
- Начальная базовая версия публичного релиза.


================================================
FILE: apps/desktop/changelogs/CHANGELOG.zh.md
================================================
# VidBee 更新日志

本页只记录你能直接感知到的更新,不展开技术实现细节。
完整发布记录请查看 [GitHub Releases](https://github.com/nexmoe/VidBee/releases)。

## [v1.3.4](https://github.com/nexmoe/VidBee/releases/tag/v1.3.4) - 2026-03-14
### Bug 修复
- 改用 Electron 默认下载源进行打包,提升 macOS 发布构建的稳定性。

## [v1.3.3](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3) - 2026-03-14
### 需求更新
- 优化了发布流程,preview 测试版现在可以独立于正式更新通知发布。

### Bug 修复
- 恢复 Electron 打包时的 npm rebuild,原生依赖在发布构建中的准备过程会更可靠。
- 进一步改善桌面端构建打包,让共享工作区依赖在发布版本中更稳定地被正确包含。

## [v1.3.3-preview.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.1) - 2026-03-14
### Bug 修复
- 恢复 Electron 打包时的 npm rebuild,原生依赖在发布构建中的准备过程会更可靠。

## [v1.3.3-preview.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.3-preview.0) - 2026-03-14
### 需求更新
- 新增 preview 发布通道,测试版可独立发布且不会触发正式站点更新通知。

### Bug 修复
- 进一步改善桌面端构建打包,让共享工作区依赖在发布版本中更稳定地被正确包含。

## [v1.3.2](https://github.com/nexmoe/VidBee/releases/tag/v1.3.2) - 2026-03-14
### Bug 修复
- 提升了桌面端打包稳定性,让共享下载组件在发布版本中更稳定地被正确包含。

## [v1.3.1](https://github.com/nexmoe/VidBee/releases/tag/v1.3.1) - 2026-03-14
### 需求更新
- 新增 Web 与 API 版本,并与桌面端共享下载核心能力和主要设置行为。
- 设置页新增上传 Cookie 与配置文件的能力。
- 下载历史迁移到 SQLite 存储,可靠性和跨端一致性更好。
- 下载弹窗新增统一的添加链接交互,并优化深色主题下滑块可见性。

### Bug 修复
- 提升了桌面端内置二进制初始化流程的稳定性与诊断信息质量。
- 修复了设置页资料输入框光标跳动问题。
- 修复了 Linux 下选择非空目录作为下载目录时的校验问题。
- 优化了本地化一致性,修正了部分中文翻译问题。

## [v1.3.0](https://github.com/nexmoe/VidBee/releases/tag/v1.3.0) - 2026-02-15
### 需求更新
- 新增更快捷的一键操作,粘贴链接后可更快开始下载。
- 新增法语、俄语与土耳其语界面本地化支持。
- 完善了土耳其语托盘菜单的本地化。
- 将 Cookie 设置重构为三个更清晰的分组。
- 在 RSS 文档中新增 RSSHub 门户链接。

### Bug 修复
- 提升了 YouTube 与格式回退场景下的下载兼容性。
- 下载输出会更稳定地遵循你选择的容器格式。
- 设置页与文档体验持续优化,问题反馈与 RSS 指引更清晰。
- 新增当请求格式不可用时的下载回退处理。
- 补齐了土耳其语缺失的本地化键。
- 通过检查与工作流修复提升了发布流程稳定性。

## [v1.2.4](https://github.com/nexmoe/VidBee/releases/tag/v1.2.4) - 2026-01-24
### 需求更新
- 一键下载流程更直接,操作步骤更短。
- 设置页新增 Cookie 管理标签,账号相关操作更集中。
- 常见问题入口更明显,报错提示更容易理解。

### Bug 修复
- RSS 使用说明更清晰,新用户上手更快。

## [v1.2.3](https://github.com/nexmoe/VidBee/releases/tag/v1.2.3) - 2026-01-23
### 需求更新
- Cookie 使用指引补充了更直观的示例。

### Bug 修复
- 播放列表加载更稳定,不再出现界面挤压问题。

## [v1.2.2](https://github.com/nexmoe/VidBee/releases/tag/v1.2.2) - 2026-01-21
### 需求更新
- 下载相关操作入口更顺手。
- 新增分享时的水印开关选择。
- 整体下载操作的一致性更好。

## [v1.2.1](https://github.com/nexmoe/VidBee/releases/tag/v1.2.1) - 2026-01-20
### 需求更新
- 播放列表内重名内容更容易区分。
- 排查问题时更容易找到日志和相关文件。
- 订阅相关链接和指引更加可靠。
- 文档站点能力进一步完善,新增 i18n 支持、站点地图与协议文档。

### Bug 修复
- 下载过程中的提示更克制,减少打扰。
- 修复了文档项目中的 TypeScript 构建问题。

## [v1.2.0](https://github.com/nexmoe/VidBee/releases/tag/v1.2.0) - 2026-01-17
### 需求更新
- 支持更快捷地全选和清空下载历史。
- 订阅使用中重复内容更少。
- 播放列表与设置页面更易用。
- 默认改为最小化到系统托盘。
- 反馈流程会在 GitHub Issue 链接过长时给出提醒。

### Bug 修复
- 最小化和重新打开应用时体验更顺滑。
- 下载中断后的继续体验更稳定。
- 更可靠地限制播放列表区域高度。
- 问题反馈链接结构更简洁稳定。
- 增加了更清晰的 Windows 专用 Cookie 说明。
- 强化了 ffmpeg/ffprobe 资源目录检查。

## [v1.1.12](https://github.com/nexmoe/VidBee/releases/tag/v1.1.12) - 2026-01-15
### 需求更新
- 提交反馈时可提供的信息更清楚。

### Bug 修复
- 设置项对下载目录选择的行为更符合预期。

## [v1.1.11](https://github.com/nexmoe/VidBee/releases/tag/v1.1.11) - 2026-01-14
### 需求更新
- 订阅页面浏览体验更流畅。
- 错误提示提供了更明确的下一步建议。
- 默认设置更适合日常使用。
- 扩展错误页与品牌展示进一步优化。
- 下载错误面板提供了更丰富的排查链接。
- 订阅标签页支持横向滚动。

### Bug 修复
- 下载流程与页面布局更清晰。
- 增加了 Bilibili 字幕嵌入失败保护。
- 补齐了缺失的本地化翻译条目。
- 默认关闭了更容易引发问题的嵌入缩略图行为。

## [v1.1.10](https://github.com/nexmoe/VidBee/releases/tag/v1.1.10) - 2026-01-12
### Bug 修复
- macOS 的安装和更新体验更稳定。
- 在 macOS 构建中增加了捆绑工具签名流程。
- 在 CI 中完善了 DMG 公证流程,发布可靠性更高。

## [v1.1.8](https://github.com/nexmoe/VidBee/releases/tag/v1.1.8) - 2026-01-12
### 需求更新
- 下载进度信息更直观。

### Bug 修复
- 更新提示的本地化显示更清晰。

## [v1.1.7](https://github.com/nexmoe/VidBee/releases/tag/v1.1.7) - 2026-01-11
### 需求更新
- 新增更多媒体输出偏好设置。
- 首次配置和日常使用流程更顺畅。

## [v1.1.6](https://github.com/nexmoe/VidBee/releases/tag/v1.1.6) - 2026-01-11
### 需求更新
- 本地视频信息相关流程更顺手。

### Bug 修复
- Cookie 配置管理更稳定、可预期。

## [v1.1.5](https://github.com/nexmoe/VidBee/releases/tag/v1.1.5) - 2026-01-10
### Bug 修复
- 高级设置页的已知问题得到修复。
- 远程封面加载稳定性更好。
- 订阅封面选择更可靠。

## [v1.1.4](https://github.com/nexmoe/VidBee/releases/tag/v1.1.4) - 2026-01-09
### 需求更新
- 开机自启后的窗口行为更自然。

### Bug 修复
- 设置页整体行为更一致。

## [v1.1.3](https://github.com/nexmoe/VidBee/releases/tag/v1.1.3) - 2026-01-02
### 需求更新
- 关于页面更容易看到更新状态。

### Bug 修复
- 下载格式选择在更多场景下更稳定。

## [v1.1.2](https://github.com/nexmoe/VidBee/releases/tag/v1.1.2) - 2025-12-26
### 需求更新
- 恢复了更多站点的下载可用性。
- 问题反馈流程更简洁。
- 新增 Issue 模板并简化了 Bug 报告表单。

### Bug 修复
- 强化了 CI 下载步骤的稳定性。

## [v1.1.1](https://github.com/nexmoe/VidBee/releases/tag/v1.1.1) - 2025-12-26
### 需求更新
- 更新提示更克制,不打断当前操作。
- 关于页面文案与链接更清楚。
- 增加了 yt-dlp 集成所需的 JavaScript 运行时支持。
- 高级选项面板的动效过渡更顺滑。

### Bug 修复
- 下载相关面板交互更顺滑。
- 为了统一体验与体积控制,Electron 语言资源默认限制为英文。

## [v1.1.0](https://github.com/nexmoe/VidBee/releases/tag/v1.1.0) - 2025-12-20
### 需求更新
- 支持批量管理下载历史,清理更高效。
- 支持自定义下载目录。
- RSS 设置弹窗更容易理解和填写。

### Bug 修复
- 打开下载任务链接时行为更符合预期。

## [v1.0.2](https://github.com/nexmoe/VidBee/releases/tag/v1.0.2) - 2025-12-06
### Bug 修复
- 增加更多兼容选项,适配场景更广。
- 路径填写容错更好,日常使用更省心。

## [v1.0.1](https://github.com/nexmoe/VidBee/releases/tag/v1.0.1) - 2025-11-16
### 需求更新
- 新增开机自启动支持。
- 语言支持进一步完善。

## [v1.0.0](https://github.com/nexmoe/VidBee/releases/tag/v1.0.0) - 2025-11-15
### 需求更新
- VidBee 首个主版本发布。
- 新增 RSS 订阅下载能力。
- 历史记录与媒体预览体验得到加强。
- 支持站点列表新增站点图标展示。
- 增强了媒体预览远程图片加载与缓存能力。
- 侧边栏拖拽与标题栏交互行为更合理。

### Bug 修复
- 导航结构和整体界面体验更清晰。
- 历史记录存储迁移到 SQLite(Drizzle),数据处理更可靠。

## [v0.3.5](https://github.com/nexmoe/VidBee/releases/tag/v0.3.5) - 2025-11-08
### 需求更新
- 一键下载文案和反馈提示更易懂。
- 视觉风格更统一。

## [v0.3.4](https://github.com/nexmoe/VidBee/releases/tag/v0.3.4) - 2025-11-03
### Bug 修复
- 更新提示与下载选项展示更清晰。

## [v0.3.3](https://github.com/nexmoe/VidBee/releases/tag/v0.3.3) - 2025-11-02
### Bug 修复
- 更多场景下的下载处理稳定性得到提升。

## [v0.3.2](https://github.com/nexmoe/VidBee/releases/tag/v0.3.2) - 2025-10-31
### Bug 修复
- 多设备分发体验进一步优化。

## [v0.3.1](https://github.com/nexmoe/VidBee/releases/tag/v0.3.1) - 2025-10-30
### 需求更新
- Linux 使用体验更友好。
- 新增版本更新提醒,方便及时升级。

## [v0.3.0](https://github.com/nexmoe/VidBee/releases/tag/v0.3.0) - 2025-10-29
### 需求更新
- 新增播放列表下载支持。

### Bug 修复
- 增加减少桌面打扰的相关控制项。

## [v0.2.2](https://github.com/nexmoe/VidBee/releases/tag/v0.2.2) - 2025-10-27
### 需求更新
- 预览阶段的持续体验打磨。

## [v0.2.1](https://github.com/nexmoe/VidBee/releases/tag/v0.2.1) - 2025-10-26
### 需求更新
- 预览阶段的持续体验打磨。

## [v0.2.0](https://github.com/nexmoe/VidBee/releases/tag/v0.2.0) - 2025-10-25
### 需求更新
- 预览阶段的持续体验打磨。

## [v0.1.8](https://github.com/nexmoe/VidBee/releases/tag/v0.1.8) - 2025-10-24
### 需求更新
- 公开预览期开始。

## [v0.1.7](https://github.com/nexmoe/VidBee/releases/tag/v0.1.7) - 2025-10-24
### 需求更新
- 新增自动更新支持,并完善了文档中的发布说明。
- 完善项目文档,包括截图与贡献指南。

### Bug 修复
- 简化下载路径处理逻辑,移除了未使用的输出路径逻辑。

## [v0.1.6](https://github.com/nexmoe/VidBee/releases/tag/v0.1.6) - 2025-10-23
### Bug 修复
- 移除了发布流程中不必要的目录创建步骤。

## [v0.1.5](https://github.com/nexmoe/VidBee/releases/tag/v0.1.5) - 2025-10-23
### Bug 修复
- 优化发布流程:支持在跨平台打包时下载 yt-dlp 二进制文件。

## [v0.1.4](https://github.com/nexmoe/VidBee/releases/tag/v0.1.4) - 2025-10-23
### Bug 修复
- 调整发布流程为仅构建 Windows 目标。

## [v0.1.3](https://github.com/nexmoe/VidBee/releases/tag/v0.1.3) - 2025-10-23
### Bug 修复
- 简化 CI 中的发布构建步骤与产物处理流程。
- 调整 CI 触发条件:仅在 `main` 分支运行 Pull Request 自动化流程。

## [v0.1.2](https://github.com/nexmoe/VidBee/releases/tag/v0.1.2) - 2025-10-23
### Bug 修复
- 为发布流程中的构建步骤显式指定 shell,提升执行稳定性。

## [v0.1.1](https://github.com/nexmoe/VidBee/releases/tag/v0.1.1) - 2025-10-23
### 需求更新
- 早期迭代版本,未记录新增用户可见改动。

## [v0.1.0](https://github.com/nexmoe/VidBee/releases/tag/v0.1.0) - 2025-10-23
### 需求更新
- 初始公开发布基线版本。


================================================
FILE: apps/desktop/components.json
================================================
{
  "$schema": "https://ui.shadcn.com/schema.json",
  "style": "new-york",
  "rsc": false,
  "tsx": true,
  "tailwind": {
    "config": "",
    "css": "src/renderer/src/assets/global.css",
    "baseColor": "neutral",
    "cssVariables": true,
    "prefix": ""
  },
  "aliases": {
    "components": "@renderer/components",
    "utils": "@renderer/lib/utils",
    "ui": "@renderer/components/ui",
    "lib": "@renderer/lib",
    "hooks": "@renderer/hooks"
  },
  "iconLibrary": "lucide"
}


================================================
FILE: apps/desktop/dev-app-update.yml
================================================
provider: generic
url: https://github.com/nexmoe/vidbee/releases/latest/download
updaterCacheDirName: vidbee-updater


================================================
FILE: apps/desktop/drizzle.config.ts
================================================
import { defineConfig } from 'drizzle-kit'

export default defineConfig({
  schema: './src/main/lib/database/schema.ts',
  out: './resources/drizzle',
  dialect: 'sqlite'
})


================================================
FILE: apps/desktop/electron-builder.yml
================================================
appId: com.vidbee
productName: VidBee
directories:
  buildResources: build
afterPack: build/after-pack.cjs
protocols:
  - name: VidBee
    schemes:
      - vidbee
files:
  - '!**/.vscode/*'
  - '!**/.context/**'
  - '!**/.github/**'
  - '!node_modules/@vidbee/downloader-core{,/**}'
  - '!node_modules/.pnpm/@vidbee+downloader-core@*/**'
  - '!src/*'
  - '!electron.vite.config.{js,ts,mjs,cjs}'
  - '!{.eslintcache,eslint.config.mjs,dev-app-update.yml}'
  - '!changelogs/**'
  - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}'
  - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}'
extraResources:
  - from: resources
    to: resources
    filter:
      - '**/*'
win:
  executableName: vidbee
nsis:
  artifactName: ${name}-${version}-setup.${ext}
  shortcutName: ${productName}
  uninstallDisplayName: ${productName}
  createDesktopShortcut: always
mac:
  hardenedRuntime: true
  entitlements: build/entitlements.mac.plist
  entitlementsInherit: build/entitlements.mac.plist
  notarize: true
  artifactName: ${name}-${version}-${arch}.${ext}
  target:
    - target: zip
      arch:
        - arm64
        - x64
    - target: dmg
      arch:
        - arm64
        - x64
linux:
  target:
    - AppImage
    - deb
  maintainer: nexmoex@gmail.com
  category: Utility
appImage:
  artifactName: ${name}-${version}.${ext}
npmRebuild: true
publish:
  provider: generic
  url: https://github.com/nexmoe/vidbee/releases/latest/download
electronLanguages:
  - en


================================================
FILE: apps/desktop/electron.vite.config.ts
================================================
import { resolve } from 'node:path'
import tailwindcss from '@tailwindcss/vite'
import react from '@vitejs/plugin-react'
import { defineConfig, externalizeDepsPlugin } from 'electron-vite'
import Icons from 'unplugin-icons/vite'

const bundledWorkspacePackages = ['@vidbee/db', '@vidbee/downloader-core', '@vidbee/i18n']

export default defineConfig({
  main: {
    plugins: [
      externalizeDepsPlugin({
        exclude: bundledWorkspacePackages
      })
    ],
    resolve: {
      alias: {
        '@main': resolve('src/main'),
        '@shared': resolve('src/shared')
      }
    },
    assetsInclude: ['**/*.png', '**/*.ico', '**/*.icns'],
    publicDir: 'build'
  },
  preload: {
    plugins: [
      externalizeDepsPlugin({
        exclude: bundledWorkspacePackages
      })
    ]
  },
  renderer: {
    base: './',
    resolve: {
      alias: {
        '@main': resolve('src/main'),
        '@renderer': resolve('src/renderer/src'),
        '@shared': resolve('src/shared')
      }
    },
    plugins: [
      react(),
      Icons({
        compiler: 'jsx',
        jsx: 'react'
      }),
      tailwindcss()
    ]
  }
})


================================================
FILE: apps/desktop/package.json
================================================
{
  "name": "vidbee",
  "version": "1.3.5",
  "description": "A modern Electron application for downloading videos and audios",
  "main": "./out/main/index.js",
  "author": "VidBee",
  "homepage": "https://github.com/nexmoe/vidbee",
  "scripts": {
    "check": "ultracite check && pnpm run check:i18n && pnpm run typecheck",
    "check:i18n": "node scripts/check-locales.js",
    "typecheck:node": "tsc --noEmit -p tsconfig.node.json --composite false",
    "typecheck:web": "tsc --noEmit -p tsconfig.web.json --composite false",
    "typecheck": "pnpm run typecheck:node && pnpm run typecheck:web",
    "prepare:native-deps": "node scripts/ensure-native-deps.mjs",
    "start": "pnpm run prepare:native-deps && electron-vite preview",
    "dev": "pnpm run setup && pnpm run prepare:native-deps && node scripts/set-console-encoding.js && electron-vite dev",
    "build": "electron-vite build",
    "setup": "node scripts/setup-dev-binaries.js",
    "postinstall": "node scripts/postinstall.mjs",
    "build:unpack": "pnpm run setup && pnpm run build && electron-builder --dir",
    "build:win": "pnpm run setup && node scripts/check-ytdlp.js win && pnpm run build && electron-builder --win",
    "build:mac": "pnpm run setup && node scripts/check-ytdlp.js mac && pnpm run build && electron-builder --mac --x64 --arm64",
    "build:linux": "pnpm run setup && node scripts/check-ytdlp.js linux && pnpm run build && electron-builder --linux",
    "release": "git checkout main && git pull && pnpm run check && bumpp",
    "db:generate": "drizzle-kit generate",
    "db:migrate": "drizzle-kit migrate",
    "fix": "ultracite fix"
  },
  "dependencies": {
    "@electron-toolkit/preload": "^3.0.2",
    "@electron-toolkit/utils": "^4.0.0",
    "@hookform/resolvers": "^5.2.2",
    "@vidbee/db": "workspace:*",
    "@vidbee/downloader-core": "workspace:*",
    "@vidbee/i18n": "workspace:*",
    "@radix-ui/react-accordion": "^1.2.12",
    "@radix-ui/react-checkbox": "^1.3.3",
    "@radix-ui/react-context-menu": "^2.2.16",
    "@radix-ui/react-dialog": "^1.1.15",
    "@radix-ui/react-dropdown-menu": "^2.1.16",
    "@radix-ui/react-hover-card": "^1.1.15",
    "@radix-ui/react-label": "^2.1.7",
    "@radix-ui/react-popover": "^1.1.15",
    "@radix-ui/react-progress": "^1.1.7",
    "@radix-ui/react-radio-group": "^1.3.8",
    "@radix-ui/react-scroll-area": "^1.2.10",
    "@radix-ui/react-select": "^2.2.6",
    "@radix-ui/react-separator": "^1.1.7",
    "@radix-ui/react-slot": "^1.2.3",
    "@radix-ui/react-switch": "^1.2.6",
    "@radix-ui/react-tabs": "^1.1.13",
    "@radix-ui/react-tooltip": "^1.2.8",
    "@svgr/core": "^8.1.0",
    "@svgr/plugin-jsx": "^8.1.0",
    "@tailwindcss/vite": "^4.1.13",
    "better-sqlite3": "^12.4.1",
    "class-variance-authority": "^0.7.1",
    "clsx": "^2.1.1",
    "cmdk": "^1.1.1",
    "dayjs": "^1.11.18",
    "drizzle-orm": "^0.44.7",
    "electron-ipc-decorator": "^0.2.0",
    "electron-log": "^5.4.3",
    "electron-store": "^11.0.2",
    "electron-updater": "^6.3.9",
    "flag-icons": "^7.5.0",
    "i18next": "^25.5.3",
    "jotai": "^2.15.0",
    "lucide-react": "^0.544.0",
    "next-themes": "^0.4.6",
    "react-hook-form": "^7.63.0",
    "react-i18next": "^16.0.0",
    "react-router": "^7.9.4",
    "rss-parser": "^3.13.0",
    "sonner": "^2.0.7",
    "tailwind-merge": "^3.3.1",
    "tailwindcss": "^4.1.13",
    "tw-animate-css": "^1.4.0",
    "yt-dlp-wrap-plus": "^2.3.20",
    "zod": "^4.1.11"
  },
  "dependenciesMeta": {
    "@vidbee/downloader-core": {
      "injected": true
    }
  },
  "devDependencies": {
    "@biomejs/biome": "2.3.13",
    "@electron-toolkit/tsconfig": "^2.0.0",
    "@iconify/json": "^2.2.394",
    "@types/node": "^22.18.6",
    "@types/react": "^19.1.13",
    "@types/react-dom": "^19.1.9",
    "@vidbee/ui": "workspace:*",
    "@vitejs/plugin-react": "^5.0.3",
    "bumpp": "^10.3.1",
    "drizzle-kit": "^0.31.7",
    "electron": "^38.1.2",
    "electron-builder": "^25.1.8",
    "electron-vite": "^4.0.1",
    "react": "^19.1.1",
    "react-dom": "^19.1.1",
    "typescript": "^5.9.2",
    "ultracite": "7.1.5",
    "unplugin-icons": "^22.4.2",
    "vite": "^7.1.6"
  }
}


================================================
FILE: apps/desktop/release-metadata.json
================================================
{
  "ytDlpVersion": "2026.03.17"
}


================================================
FILE: apps/desktop/resources/.gitignore
================================================
# Ignore yt-dlp binaries (too large for git)
yt-dlp.exe
yt-dlp_macos
yt-dlp_linux
ffmpeg.exe
ffmpeg_macos
ffmpeg_linux
ffmpeg
ffmpeg/
ffprobe
ffprobe.exe
deno.exe
deno

# But keep the README
!README.md
!.gitignore


================================================
FILE: apps/desktop/resources/README.md
================================================
# Resources Directory

This directory contains bundled resources for the application.

## yt-dlp Binaries

To bundle yt-dlp with the application, place the appropriate binaries in this directory:

### Required Files

1. **Windows**: `yt-dlp.exe`
2. **macOS**: `yt-dlp_macos`
3. **Linux**: `yt-dlp_linux`

### How to Download

You can download the latest yt-dlp binaries from the official GitHub releases:

**Option 1: Manual Download**

- Visit: <https://github.com/yt-dlp/yt-dlp/releases/latest>
- Download the appropriate version for each platform:
  - Windows: `yt-dlp.exe`
  - macOS: `yt-dlp_macos`
  - Linux: `yt-dlp` (rename to `yt-dlp_linux`)

**Option 2: Using curl/wget (Linux/macOS)**

```bash
# For Windows binary
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe -o resources/yt-dlp.exe

# For macOS binary
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos -o resources/yt-dlp_macos
chmod +x resources/yt-dlp_macos

# For Linux binary
curl -L https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp -o resources/yt-dlp_linux
chmod +x resources/yt-dlp_linux
```

**Option 3: Using PowerShell (Windows)**

```powershell
# Download all three binaries
Invoke-WebRequest -Uri "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp.exe" -OutFile "resources/yt-dlp.exe"
Invoke-WebRequest -Uri "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp_macos" -OutFile "resources/yt-dlp_macos"
Invoke-WebRequest -Uri "https://github.com/yt-dlp/yt-dlp/releases/latest/download/yt-dlp" -OutFile "resources/yt-dlp_linux"
```

## ffmpeg/ffprobe Binaries

ffmpeg is required for merging audio/video streams and audio extraction. ffprobe is required for post-processing metadata. Bundle both binaries under `resources/ffmpeg/`.

### Required Files

1. **Windows**: `resources/ffmpeg/ffmpeg.exe` and `resources/ffmpeg/ffprobe.exe`
2. **macOS**: `resources/ffmpeg/ffmpeg` and `resources/ffmpeg/ffprobe`
3. **Linux**: `resources/ffmpeg/ffmpeg` and `resources/ffmpeg/ffprobe`

### How to Download

- **Windows / Linux**: Grab static builds from <https://ffmpeg.org/download.html> (or <https://github.com/yt-dlp/FFmpeg-Builds/releases>) and copy `ffmpeg` and `ffprobe` into `resources/ffmpeg/`.
- **macOS**: Download the `ffmpeg-*.zip` asset from <https://github.com/eko5624/mpv-mac/releases/latest>, then copy `ffmpeg` and `ffprobe` from the archive into `resources/ffmpeg/`.
- On macOS/Linux ensure both binaries are executable: `chmod +x resources/ffmpeg/ffmpeg resources/ffmpeg/ffprobe`.

### Note

- Bundled binaries are required for Windows builds. On macOS/Linux the app can also use ffmpeg/ffprobe from the system PATH.
- You can override the lookup path via `FFMPEG_PATH`. It must point to a directory containing both `ffmpeg` and `ffprobe`.
- File sizes: ~40-80 MB per ffmpeg build (ffmpeg + ffprobe)

## JS Runtime (Deno)

yt-dlp uses an external JS runtime (Deno by default) for some extractors. Bundle a Deno binary so the app can run without system dependencies.

### Required Files

1. **Windows**: `deno.exe`
2. **macOS**: `deno`
3. **Linux**: `deno`

### How to Download

- Visit: <https://github.com/denoland/deno/releases/latest>
- Download the matching platform archive and extract the `deno` (or `deno.exe`) binary into `resources/`.
- On macOS/Linux ensure the file is executable: `chmod +x resources/deno`

### Note

- You can override the runtime path via `YTDLP_JS_RUNTIME_PATH` if needed.


================================================
FILE: apps/desktop/resources/drizzle/0000_swift_aaron_stack.sql
================================================
CREATE TABLE `download_history` (
	`id` text PRIMARY KEY NOT NULL,
	`url` text NOT NULL,
	`title` text NOT NULL,
	`thumbnail` text,
	`type` text NOT NULL,
	`status` text NOT NULL,
	`download_path` text,
	`saved_file_name` text,
	`file_size` integer,
	`duration` integer,
	`downloaded_at` integer NOT NULL,
	`completed_at` integer,
	`sort_key` integer NOT NULL,
	`error` text,
	`description` text,
	`channel` text,
	`uploader` text,
	`view_count` integer,
	`tags` text,
	`origin` text,
	`subscription_id` text,
	`selected_format` text,
	`playlist_id` text,
	`playlist_title` text,
	`playlist_index` integer,
	`playlist_size` integer
);
--> statement-breakpoint
CREATE TABLE `subscription_items` (
	`subscription_id` text NOT NULL,
	`item_id` text NOT NULL,
	`title` text NOT NULL,
	`url` text NOT NULL,
	`published_at` integer NOT NULL,
	`thumbnail` text,
	`added` integer NOT NULL,
	`download_id` text,
	`created_at` integer NOT NULL,
	`updated_at` integer NOT NULL,
	PRIMARY KEY(`subscription_id`, `item_id`)
);
--> statement-breakpoint
CREATE INDEX `subscription_items_subscription_idx` ON `subscription_items` (`subscription_id`);--> statement-breakpoint
CREATE TABLE `subscriptions` (
	`id` text PRIMARY KEY NOT NULL,
	`title` text NOT NULL,
	`source_url` text NOT NULL,
	`feed_url` text NOT NULL,
	`platform` text NOT NULL,
	`keywords` text NOT NULL,
	`tags` text NOT NULL,
	`only_latest` integer NOT NULL,
	`enabled` integer NOT NULL,
	`cover_url` text,
	`latest_video_title` text,
	`latest_video_published_at` integer,
	`last_checked_at` integer,
	`last_success_at` integer,
	`status` text NOT NULL,
	`last_error` text,
	`created_at` integer NOT NULL,
	`updated_at` integer NOT NULL,
	`download_directory` text,
	`naming_template` text
);


================================================
FILE: apps/desktop/resources/drizzle/0001_smiling_agent_zero.sql
================================================
ALTER TABLE `download_history` ADD `yt_dlp_command` text;

================================================
FILE: apps/desktop/resources/drizzle/0002_smooth_impossible_man.sql
================================================
ALTER TABLE `download_history` ADD `yt_dlp_log` text;

================================================
FILE: apps/desktop/resources/drizzle/meta/0000_snapshot.json
================================================
{
  "version": "6",
  "dialect": "sqlite",
  "id": "91501763-7554-435d-91d8-382c52ee65e4",
  "prevId": "00000000-0000-0000-0000-000000000000",
  "tables": {
    "download_history": {
      "name": "download_history",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "url": {
          "name": "url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "thumbnail": {
          "name": "thumbnail",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "type": {
          "name": "type",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "status": {
          "name": "status",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_path": {
          "name": "download_path",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "saved_file_name": {
          "name": "saved_file_name",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "file_size": {
          "name": "file_size",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "duration": {
          "name": "duration",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "downloaded_at": {
          "name": "downloaded_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "completed_at": {
          "name": "completed_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "sort_key": {
          "name": "sort_key",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "error": {
          "name": "error",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "description": {
          "name": "description",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "channel": {
          "name": "channel",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "uploader": {
          "name": "uploader",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "view_count": {
          "name": "view_count",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "tags": {
          "name": "tags",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "origin": {
          "name": "origin",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "subscription_id": {
          "name": "subscription_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "selected_format": {
          "name": "selected_format",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_id": {
          "name": "playlist_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_title": {
          "name": "playlist_title",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_index": {
          "name": "playlist_index",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_size": {
          "name": "playlist_size",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {},
      "checkConstraints": {}
    },
    "subscription_items": {
      "name": "subscription_items",
      "columns": {
        "subscription_id": {
          "name": "subscription_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "item_id": {
          "name": "item_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "url": {
          "name": "url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "published_at": {
          "name": "published_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "thumbnail": {
          "name": "thumbnail",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "added": {
          "name": "added",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_id": {
          "name": "download_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "created_at": {
          "name": "created_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "updated_at": {
          "name": "updated_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        }
      },
      "indexes": {
        "subscription_items_subscription_idx": {
          "name": "subscription_items_subscription_idx",
          "columns": ["subscription_id"],
          "isUnique": false
        }
      },
      "foreignKeys": {},
      "compositePrimaryKeys": {
        "subscription_items_pk": {
          "columns": ["subscription_id", "item_id"],
          "name": "subscription_items_pk"
        }
      },
      "uniqueConstraints": {},
      "checkConstraints": {}
    },
    "subscriptions": {
      "name": "subscriptions",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "source_url": {
          "name": "source_url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "feed_url": {
          "name": "feed_url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "platform": {
          "name": "platform",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "keywords": {
          "name": "keywords",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "tags": {
          "name": "tags",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "only_latest": {
          "name": "only_latest",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "enabled": {
          "name": "enabled",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "cover_url": {
          "name": "cover_url",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "latest_video_title": {
          "name": "latest_video_title",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "latest_video_published_at": {
          "name": "latest_video_published_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "last_checked_at": {
          "name": "last_checked_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "last_success_at": {
          "name": "last_success_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "status": {
          "name": "status",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "last_error": {
          "name": "last_error",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "created_at": {
          "name": "created_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "updated_at": {
          "name": "updated_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_directory": {
          "name": "download_directory",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "naming_template": {
          "name": "naming_template",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {},
      "checkConstraints": {}
    }
  },
  "views": {},
  "enums": {},
  "_meta": {
    "schemas": {},
    "tables": {},
    "columns": {}
  },
  "internal": {
    "indexes": {}
  }
}


================================================
FILE: apps/desktop/resources/drizzle/meta/0001_snapshot.json
================================================
{
  "version": "6",
  "dialect": "sqlite",
  "id": "a2ffc60d-d3a6-4bbb-96d3-fbc79bca6d8b",
  "prevId": "91501763-7554-435d-91d8-382c52ee65e4",
  "tables": {
    "download_history": {
      "name": "download_history",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "url": {
          "name": "url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "thumbnail": {
          "name": "thumbnail",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "type": {
          "name": "type",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "status": {
          "name": "status",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_path": {
          "name": "download_path",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "saved_file_name": {
          "name": "saved_file_name",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "file_size": {
          "name": "file_size",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "duration": {
          "name": "duration",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "downloaded_at": {
          "name": "downloaded_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "completed_at": {
          "name": "completed_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "sort_key": {
          "name": "sort_key",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "error": {
          "name": "error",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "yt_dlp_command": {
          "name": "yt_dlp_command",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "description": {
          "name": "description",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "channel": {
          "name": "channel",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "uploader": {
          "name": "uploader",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "view_count": {
          "name": "view_count",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "tags": {
          "name": "tags",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "origin": {
          "name": "origin",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "subscription_id": {
          "name": "subscription_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "selected_format": {
          "name": "selected_format",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_id": {
          "name": "playlist_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_title": {
          "name": "playlist_title",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_index": {
          "name": "playlist_index",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_size": {
          "name": "playlist_size",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {},
      "checkConstraints": {}
    },
    "subscription_items": {
      "name": "subscription_items",
      "columns": {
        "subscription_id": {
          "name": "subscription_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "item_id": {
          "name": "item_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "url": {
          "name": "url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "published_at": {
          "name": "published_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "thumbnail": {
          "name": "thumbnail",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "added": {
          "name": "added",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_id": {
          "name": "download_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "created_at": {
          "name": "created_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "updated_at": {
          "name": "updated_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        }
      },
      "indexes": {
        "subscription_items_subscription_idx": {
          "name": "subscription_items_subscription_idx",
          "columns": ["subscription_id"],
          "isUnique": false
        }
      },
      "foreignKeys": {},
      "compositePrimaryKeys": {
        "subscription_items_pk": {
          "columns": ["subscription_id", "item_id"],
          "name": "subscription_items_pk"
        }
      },
      "uniqueConstraints": {},
      "checkConstraints": {}
    },
    "subscriptions": {
      "name": "subscriptions",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "source_url": {
          "name": "source_url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "feed_url": {
          "name": "feed_url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "platform": {
          "name": "platform",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "keywords": {
          "name": "keywords",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "tags": {
          "name": "tags",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "only_latest": {
          "name": "only_latest",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "enabled": {
          "name": "enabled",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "cover_url": {
          "name": "cover_url",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "latest_video_title": {
          "name": "latest_video_title",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "latest_video_published_at": {
          "name": "latest_video_published_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "last_checked_at": {
          "name": "last_checked_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "last_success_at": {
          "name": "last_success_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "status": {
          "name": "status",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "last_error": {
          "name": "last_error",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "created_at": {
          "name": "created_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "updated_at": {
          "name": "updated_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_directory": {
          "name": "download_directory",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "naming_template": {
          "name": "naming_template",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {},
      "checkConstraints": {}
    }
  },
  "views": {},
  "enums": {},
  "_meta": {
    "schemas": {},
    "tables": {},
    "columns": {}
  },
  "internal": {
    "indexes": {}
  }
}


================================================
FILE: apps/desktop/resources/drizzle/meta/0002_snapshot.json
================================================
{
  "version": "6",
  "dialect": "sqlite",
  "id": "3a269324-eb4d-44c4-860c-80d90f4f3df1",
  "prevId": "a2ffc60d-d3a6-4bbb-96d3-fbc79bca6d8b",
  "tables": {
    "download_history": {
      "name": "download_history",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "url": {
          "name": "url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "thumbnail": {
          "name": "thumbnail",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "type": {
          "name": "type",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "status": {
          "name": "status",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_path": {
          "name": "download_path",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "saved_file_name": {
          "name": "saved_file_name",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "file_size": {
          "name": "file_size",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "duration": {
          "name": "duration",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "downloaded_at": {
          "name": "downloaded_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "completed_at": {
          "name": "completed_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "sort_key": {
          "name": "sort_key",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "error": {
          "name": "error",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "yt_dlp_command": {
          "name": "yt_dlp_command",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "yt_dlp_log": {
          "name": "yt_dlp_log",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "description": {
          "name": "description",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "channel": {
          "name": "channel",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "uploader": {
          "name": "uploader",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "view_count": {
          "name": "view_count",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "tags": {
          "name": "tags",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "origin": {
          "name": "origin",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "subscription_id": {
          "name": "subscription_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "selected_format": {
          "name": "selected_format",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_id": {
          "name": "playlist_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_title": {
          "name": "playlist_title",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_index": {
          "name": "playlist_index",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "playlist_size": {
          "name": "playlist_size",
          "type": "integer",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        }
      },
      "indexes": {},
      "foreignKeys": {},
      "compositePrimaryKeys": {},
      "uniqueConstraints": {},
      "checkConstraints": {}
    },
    "subscription_items": {
      "name": "subscription_items",
      "columns": {
        "subscription_id": {
          "name": "subscription_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "item_id": {
          "name": "item_id",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "url": {
          "name": "url",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "published_at": {
          "name": "published_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "thumbnail": {
          "name": "thumbnail",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "added": {
          "name": "added",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "download_id": {
          "name": "download_id",
          "type": "text",
          "primaryKey": false,
          "notNull": false,
          "autoincrement": false
        },
        "created_at": {
          "name": "created_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "updated_at": {
          "name": "updated_at",
          "type": "integer",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        }
      },
      "indexes": {
        "subscription_items_subscription_idx": {
          "name": "subscription_items_subscription_idx",
          "columns": ["subscription_id"],
          "isUnique": false
        }
      },
      "foreignKeys": {},
      "compositePrimaryKeys": {
        "subscription_items_pk": {
          "columns": ["subscription_id", "item_id"],
          "name": "subscription_items_pk"
        }
      },
      "uniqueConstraints": {},
      "checkConstraints": {}
    },
    "subscriptions": {
      "name": "subscriptions",
      "columns": {
        "id": {
          "name": "id",
          "type": "text",
          "primaryKey": true,
          "notNull": true,
          "autoincrement": false
        },
        "title": {
          "name": "title",
          "type": "text",
          "primaryKey": false,
          "notNull": true,
          "autoincrement": false
        },
        "source_url": {
          "name": "source_url",
          "type": "text",
    
Download .txt
gitextract_plngy_re/

├── .agents/
│   └── skills/
│       ├── orpc-contract-first/
│       │   └── SKILL.md
│       └── release-skills/
│           └── SKILL.md
├── .claude/
│   └── CLAUDE.md
├── .cursor/
│   └── hooks.json
├── .dockerignore
├── .editorconfig
├── .gitattributes
├── .github/
│   ├── ISSUE_TEMPLATE/
│   │   ├── bug_report.yml
│   │   └── feature_request.yml
│   └── workflows/
│       ├── build.yml
│       ├── ci.yml
│       ├── docker-publish.yml
│       ├── extension-build.yml
│       ├── extension-publish.yml
│       ├── release.yml
│       ├── translator.yaml
│       └── ytdlp-auto-release.yml
├── .gitignore
├── .husky/
│   └── pre-commit
├── .npmrc
├── .vscode/
│   ├── extensions.json
│   └── settings.json
├── AGENTS.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── apps/
│   ├── api/
│   │   ├── Dockerfile
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   ├── lib/
│   │   │   │   ├── database-migrate.ts
│   │   │   │   ├── downloader.ts
│   │   │   │   ├── history-record-mapper.ts
│   │   │   │   ├── history-store.ts
│   │   │   │   ├── rpc-router.ts
│   │   │   │   ├── sse.ts
│   │   │   │   └── web-settings-store.ts
│   │   │   └── server.ts
│   │   └── tsconfig.json
│   ├── desktop/
│   │   ├── build/
│   │   │   ├── after-pack.cjs
│   │   │   ├── entitlements.mac.plist
│   │   │   └── icon.icns
│   │   ├── changelogs/
│   │   │   ├── CHANGELOG.fr.md
│   │   │   ├── CHANGELOG.md
│   │   │   ├── CHANGELOG.ru.md
│   │   │   └── CHANGELOG.zh.md
│   │   ├── components.json
│   │   ├── dev-app-update.yml
│   │   ├── drizzle.config.ts
│   │   ├── electron-builder.yml
│   │   ├── electron.vite.config.ts
│   │   ├── package.json
│   │   ├── release-metadata.json
│   │   ├── resources/
│   │   │   ├── .gitignore
│   │   │   ├── README.md
│   │   │   └── drizzle/
│   │   │       ├── 0000_swift_aaron_stack.sql
│   │   │       ├── 0001_smiling_agent_zero.sql
│   │   │       ├── 0002_smooth_impossible_man.sql
│   │   │       └── meta/
│   │   │           ├── 0000_snapshot.json
│   │   │           ├── 0001_snapshot.json
│   │   │           ├── 0002_snapshot.json
│   │   │           └── _journal.json
│   │   ├── scripts/
│   │   │   ├── check-locales.js
│   │   │   ├── check-ytdlp.js
│   │   │   ├── ensure-native-deps.mjs
│   │   │   ├── postinstall.mjs
│   │   │   ├── set-console-encoding.js
│   │   │   ├── setup-dev-binaries.js
│   │   │   └── ytdlp-auto-release.mjs
│   │   ├── src/
│   │   │   ├── main/
│   │   │   │   ├── assets.d.ts
│   │   │   │   ├── config/
│   │   │   │   │   └── logger-config.ts
│   │   │   │   ├── download-engine/
│   │   │   │   │   ├── args-builder.ts
│   │   │   │   │   └── format-utils.ts
│   │   │   │   ├── index.ts
│   │   │   │   ├── ipc/
│   │   │   │   │   ├── index.ts
│   │   │   │   │   └── services/
│   │   │   │   │       ├── app-service.ts
│   │   │   │   │       ├── browser-cookies-service.ts
│   │   │   │   │       ├── download-service.ts
│   │   │   │   │       ├── file-system-service.ts
│   │   │   │   │       ├── history-service.ts
│   │   │   │   │       ├── settings-service.ts
│   │   │   │   │       ├── subscription-service.ts
│   │   │   │   │       ├── thumbnail-service.ts
│   │   │   │   │       ├── update-service.ts
│   │   │   │   │       └── window-service.ts
│   │   │   │   ├── lib/
│   │   │   │   │   ├── command-utils.ts
│   │   │   │   │   ├── database/
│   │   │   │   │   │   ├── migrate.ts
│   │   │   │   │   │   └── schema.ts
│   │   │   │   │   ├── database-path.ts
│   │   │   │   │   ├── database.ts
│   │   │   │   │   ├── download-engine.ts
│   │   │   │   │   ├── download-queue.ts
│   │   │   │   │   ├── download-session-store.ts
│   │   │   │   │   ├── ffmpeg-manager.ts
│   │   │   │   │   ├── history-manager.ts
│   │   │   │   │   ├── path-resolver.ts
│   │   │   │   │   ├── progress-utils.ts
│   │   │   │   │   ├── subscription-manager.ts
│   │   │   │   │   ├── subscription-scheduler.ts
│   │   │   │   │   ├── thumbnail-cache.ts
│   │   │   │   │   ├── watermark-utils.ts
│   │   │   │   │   ├── youtube-extractor-args.ts
│   │   │   │   │   └── ytdlp-manager.ts
│   │   │   │   ├── local-api.ts
│   │   │   │   ├── settings.ts
│   │   │   │   ├── tray.ts
│   │   │   │   └── utils/
│   │   │   │       ├── auto-launch.ts
│   │   │   │       ├── dock.ts
│   │   │   │       ├── logger.ts
│   │   │   │       └── path-helpers.ts
│   │   │   ├── preload/
│   │   │   │   ├── index.d.ts
│   │   │   │   └── index.ts
│   │   │   ├── renderer/
│   │   │   │   ├── index.html
│   │   │   │   └── src/
│   │   │   │       ├── App.tsx
│   │   │   │       ├── assets/
│   │   │   │       │   ├── global.css
│   │   │   │       │   ├── main.css
│   │   │   │       │   ├── theme.css
│   │   │   │       │   └── title-bar.css
│   │   │   │       ├── components/
│   │   │   │       │   ├── download/
│   │   │   │       │   │   ├── DownloadDialog.tsx
│   │   │   │       │   │   ├── DownloadItem.tsx
│   │   │   │       │   │   ├── PlaylistDownload.tsx
│   │   │   │       │   │   ├── PlaylistDownloadGroup.tsx
│   │   │   │       │   │   ├── SingleVideoDownload.tsx
│   │   │   │       │   │   └── UnifiedDownloadHistory.tsx
│   │   │   │       │   ├── error/
│   │   │   │       │   │   ├── ErrorBoundary.tsx
│   │   │   │       │   │   └── ErrorPage.tsx
│   │   │   │       │   ├── feedback/
│   │   │   │       │   │   └── FeedbackLinks.tsx
│   │   │   │       │   ├── playlist/
│   │   │   │       │   │   └── PlaylistPreviewCard.tsx
│   │   │   │       │   ├── subscription/
│   │   │   │       │   │   └── SubscriptionFormDialog.tsx
│   │   │   │       │   ├── ui/
│   │   │   │       │   │   ├── accordion.tsx
│   │   │   │       │   │   ├── add-url-popover.tsx
│   │   │   │       │   │   ├── badge.tsx
│   │   │   │       │   │   ├── button.tsx
│   │   │   │       │   │   ├── card.tsx
│   │   │   │       │   │   ├── checkbox.tsx
│   │   │   │       │   │   ├── context-menu.tsx
│   │   │   │       │   │   ├── dialog.tsx
│   │   │   │       │   │   ├── download-dialog-layout.tsx
│   │   │   │       │   │   ├── dropdown-menu.tsx
│   │   │   │       │   │   ├── hover-card.tsx
│   │   │   │       │   │   ├── image-with-placeholder.tsx
│   │   │   │       │   │   ├── input.tsx
│   │   │   │       │   │   ├── item.tsx
│   │   │   │       │   │   ├── label.tsx
│   │   │   │       │   │   ├── popover.tsx
│   │   │   │       │   │   ├── progress.tsx
│   │   │   │       │   │   ├── radio-group.tsx
│   │   │   │       │   │   ├── remote-image.tsx
│   │   │   │       │   │   ├── scroll-area.tsx
│   │   │   │       │   │   ├── select.tsx
│   │   │   │       │   │   ├── separator.tsx
│   │   │   │       │   │   ├── sheet.tsx
│   │   │   │       │   │   ├── sidebar.tsx
│   │   │   │       │   │   ├── sonner.tsx
│   │   │   │       │   │   ├── switch.tsx
│   │   │   │       │   │   ├── table.tsx
│   │   │   │       │   │   ├── tabs.tsx
│   │   │   │       │   │   ├── textarea.tsx
│   │   │   │       │   │   ├── title-bar.tsx
│   │   │   │       │   │   └── tooltip.tsx
│   │   │   │       │   └── video/
│   │   │   │       │       └── AdvancedOptions.tsx
│   │   │   │       ├── data/
│   │   │   │       │   └── popularSites.ts
│   │   │   │       ├── env.d.ts
│   │   │   │       ├── hooks/
│   │   │   │       │   ├── use-cached-thumbnail.ts
│   │   │   │       │   ├── use-download-events.ts
│   │   │   │       │   ├── use-history-sync.ts
│   │   │   │       │   └── use-ipc-example.ts
│   │   │   │       ├── i18n.ts
│   │   │   │       ├── lib/
│   │   │   │       │   ├── ipc.ts
│   │   │   │       │   ├── logger.ts
│   │   │   │       │   └── utils.ts
│   │   │   │       ├── main.tsx
│   │   │   │       ├── pages/
│   │   │   │       │   ├── About.tsx
│   │   │   │       │   ├── Home.tsx
│   │   │   │       │   ├── Settings.tsx
│   │   │   │       │   └── Subscriptions.tsx
│   │   │   │       └── store/
│   │   │   │           ├── downloads.ts
│   │   │   │           ├── settings.ts
│   │   │   │           ├── subscriptions.ts
│   │   │   │           ├── update.ts
│   │   │   │           └── video.ts
│   │   │   └── shared/
│   │   │       ├── constants.ts
│   │   │       ├── types/
│   │   │       │   ├── index.ts
│   │   │       │   └── ipc.ts
│   │   │       └── utils/
│   │   │           ├── download-file.ts
│   │   │           └── format-preferences.ts
│   │   ├── tsconfig.json
│   │   ├── tsconfig.node.json
│   │   └── tsconfig.web.json
│   ├── docs/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── biome.config.json
│   │   ├── content/
│   │   │   ├── cookies.mdx
│   │   │   ├── faq.mdx
│   │   │   ├── fr/
│   │   │   │   ├── cookies.mdx
│   │   │   │   ├── faq.mdx
│   │   │   │   ├── index.mdx
│   │   │   │   ├── meta.json
│   │   │   │   ├── protocol.mdx
│   │   │   │   └── rss.mdx
│   │   │   ├── index.mdx
│   │   │   ├── meta.json
│   │   │   ├── protocol.mdx
│   │   │   ├── rss.mdx
│   │   │   ├── ru/
│   │   │   │   ├── cookies.mdx
│   │   │   │   ├── faq.mdx
│   │   │   │   ├── index.mdx
│   │   │   │   ├── meta.json
│   │   │   │   ├── protocol.mdx
│   │   │   │   └── rss.mdx
│   │   │   └── zh/
│   │   │       ├── cookies.mdx
│   │   │       ├── faq.mdx
│   │   │       ├── index.mdx
│   │   │       ├── meta.json
│   │   │       ├── protocol.mdx
│   │   │       └── rss.mdx
│   │   ├── next.config.mjs
│   │   ├── package.json
│   │   ├── postcss.config.mjs
│   │   ├── public/
│   │   │   └── ICONS.md
│   │   ├── scripts/
│   │   │   └── post-export.js
│   │   ├── source.config.ts
│   │   ├── src/
│   │   │   ├── app/
│   │   │   │   ├── (docs)/
│   │   │   │   │   ├── [[...slug]]/
│   │   │   │   │   │   └── page.tsx
│   │   │   │   │   └── layout.tsx
│   │   │   │   ├── [lang]/
│   │   │   │   │   ├── (docs)/
│   │   │   │   │   │   ├── [[...slug]]/
│   │   │   │   │   │   │   └── page.tsx
│   │   │   │   │   │   └── layout.tsx
│   │   │   │   │   └── layout.tsx
│   │   │   │   ├── api/
│   │   │   │   │   └── search/
│   │   │   │   │       └── route.ts
│   │   │   │   ├── global.css
│   │   │   │   ├── layout.tsx
│   │   │   │   └── sitemap.ts
│   │   │   ├── components/
│   │   │   │   └── ai/
│   │   │   │       └── page-actions.tsx
│   │   │   ├── lib/
│   │   │   │   ├── cn.ts
│   │   │   │   ├── i18n.ts
│   │   │   │   ├── layout.shared.tsx
│   │   │   │   └── source.ts
│   │   │   ├── mdx-components.tsx
│   │   │   └── middleware.ts
│   │   └── tsconfig.json
│   ├── extension/
│   │   ├── .gitignore
│   │   ├── README.md
│   │   ├── assets/
│   │   │   └── content.css
│   │   ├── entrypoints/
│   │   │   ├── background.ts
│   │   │   └── popup/
│   │   │       ├── App.css
│   │   │       ├── App.tsx
│   │   │       ├── index.html
│   │   │       └── main.tsx
│   │   ├── package.json
│   │   ├── public/
│   │   │   └── _locales/
│   │   │       └── en/
│   │   │           └── messages.json
│   │   ├── tsconfig.json
│   │   └── wxt.config.ts
│   └── web/
│       ├── Dockerfile
│       ├── README.md
│       ├── biome.json
│       ├── package.json
│       ├── public/
│       │   ├── manifest.json
│       │   └── robots.txt
│       ├── src/
│       │   ├── components/
│       │   │   ├── download/
│       │   │   │   ├── download-dialog.tsx
│       │   │   │   ├── download-item.tsx
│       │   │   │   ├── playlist-download-group.tsx
│       │   │   │   ├── playlist-download.tsx
│       │   │   │   ├── single-video-download.tsx
│       │   │   │   └── types.ts
│       │   │   ├── layout/
│       │   │   │   └── app-shell.tsx
│       │   │   └── pages/
│       │   │       ├── about-page.tsx
│       │   │       ├── download-page.tsx
│       │   │       └── settings-page.tsx
│       │   ├── env.d.ts
│       │   ├── hooks/
│       │   │   ├── use-web-download-settings.ts
│       │   │   └── use-web-settings.ts
│       │   ├── lib/
│       │   │   ├── download-format-preferences.ts
│       │   │   ├── i18n.ts
│       │   │   ├── orpc-client.ts
│       │   │   ├── orpc-download-settings.ts
│       │   │   ├── remote-image-proxy.ts
│       │   │   └── web-settings.ts
│       │   ├── routeTree.gen.ts
│       │   ├── router.tsx
│       │   ├── routes/
│       │   │   ├── __root.tsx
│       │   │   ├── about.tsx
│       │   │   ├── index.tsx
│       │   │   └── settings.tsx
│       │   └── styles.css
│       ├── tsconfig.json
│       └── vite.config.ts
├── biome.json
├── conductor.json
├── docker-compose.yml
├── package.json
├── packages/
│   ├── db/
│   │   ├── package.json
│   │   ├── src/
│   │   │   └── history.ts
│   │   └── tsconfig.json
│   ├── downloader-core/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── browser-cookies-setting.ts
│   │   │   ├── contract.ts
│   │   │   ├── download-file.ts
│   │   │   ├── downloader-core.ts
│   │   │   ├── format-preferences.ts
│   │   │   ├── index.ts
│   │   │   ├── schemas.ts
│   │   │   ├── types.ts
│   │   │   └── yt-dlp-args.ts
│   │   └── tsconfig.json
│   ├── i18n/
│   │   ├── package.json
│   │   ├── src/
│   │   │   ├── index.ts
│   │   │   ├── init.ts
│   │   │   ├── languages.ts
│   │   │   ├── locales/
│   │   │   │   ├── ar.json
│   │   │   │   ├── de.json
│   │   │   │   ├── en.json
│   │   │   │   ├── es.json
│   │   │   │   ├── fr.json
│   │   │   │   ├── id.json
│   │   │   │   ├── it.json
│   │   │   │   ├── ja.json
│   │   │   │   ├── ko.json
│   │   │   │   ├── pt.json
│   │   │   │   ├── ru.json
│   │   │   │   ├── tr.json
│   │   │   │   ├── zh-TW.json
│   │   │   │   └── zh.json
│   │   │   └── resources.ts
│   │   └── tsconfig.json
│   └── ui/
│       ├── package.json
│       ├── src/
│       │   ├── base.css
│       │   ├── components/
│       │   │   └── ui/
│       │   │       ├── accordion.tsx
│       │   │       ├── add-url-popover.tsx
│       │   │       ├── app-sidebar-icons.tsx
│       │   │       ├── app-sidebar.tsx
│       │   │       ├── badge.tsx
│       │   │       ├── button.tsx
│       │   │       ├── card.tsx
│       │   │       ├── checkbox.tsx
│       │   │       ├── context-menu.tsx
│       │   │       ├── dialog.tsx
│       │   │       ├── download-dialog-layout.tsx
│       │   │       ├── download-empty-state.tsx
│       │   │       ├── download-filter-bar.tsx
│       │   │       ├── dropdown-menu.tsx
│       │   │       ├── feedback-link-buttons.tsx
│       │   │       ├── hover-card.tsx
│       │   │       ├── image-with-placeholder.tsx
│       │   │       ├── input.tsx
│       │   │       ├── item.tsx
│       │   │       ├── label.tsx
│       │   │       ├── popover.tsx
│       │   │       ├── progress.tsx
│       │   │       ├── radio-group.tsx
│       │   │       ├── remote-image.tsx
│       │   │       ├── scroll-area.tsx
│       │   │       ├── select.tsx
│       │   │       ├── separator.tsx
│       │   │       ├── sheet.tsx
│       │   │       ├── sidebar.tsx
│       │   │       ├── sonner.tsx
│       │   │       ├── switch.tsx
│       │   │       ├── table.tsx
│       │   │       ├── tabs.tsx
│       │   │       ├── textarea.tsx
│       │   │       ├── title-bar.tsx
│       │   │       └── tooltip.tsx
│       │   ├── lib/
│       │   │   ├── cn.ts
│       │   │   └── use-add-url-interaction.ts
│       │   └── theme.css
│       └── tsconfig.json
├── pnpm-workspace.yaml
└── skills-lock.json
Download .txt
SYMBOL INDEX (730 symbols across 145 files)

FILE: apps/api/src/lib/database-migrate.ts
  constant MIGRATIONS_FOLDER (line 6) | const MIGRATIONS_FOLDER = path.resolve(import.meta.dirname, '../../../de...

FILE: apps/api/src/lib/history-record-mapper.ts
  constant TERMINAL_STATUSES (line 4) | const TERMINAL_STATUSES = new Set<DownloadTask['status']>(['completed', ...
  constant TAG_SEPARATOR (line 5) | const TAG_SEPARATOR = '\n'

FILE: apps/api/src/lib/history-store.ts
  class HistoryStore (line 11) | class HistoryStore {
    method constructor (line 15) | constructor(databasePath: string) {
    method save (line 23) | save(task: DownloadTask): void {
    method list (line 40) | list(): DownloadTask[] {
    method removeItems (line 57) | removeItems(ids: string[]): number {
    method removeByPlaylist (line 69) | removeByPlaylist(playlistId: string): number {

FILE: apps/api/src/lib/rpc-router.ts
  constant WEB_SETTINGS_FILES_DIR (line 12) | const WEB_SETTINGS_FILES_DIR = path.resolve(process.cwd(), '.data', 'web...
  constant MAX_WEB_SETTINGS_FILE_BYTES (line 13) | const MAX_WEB_SETTINGS_FILE_BYTES = 1_000_000
  constant MANAGED_SETTINGS_FILE_RETENTION_MS (line 14) | const MANAGED_SETTINGS_FILE_RETENTION_MS = 30 * 24 * 60 * 60 * 1000
  constant SAFE_FILE_NAME_REGEX (line 15) | const SAFE_FILE_NAME_REGEX = /[^A-Za-z0-9._-]+/g
  type ManagedSettingsFileKind (line 16) | type ManagedSettingsFileKind = 'cookies' | 'config'

FILE: apps/api/src/lib/sse.ts
  constant HEARTBEAT_INTERVAL_MS (line 3) | const HEARTBEAT_INTERVAL_MS = 15_000
  class SseHub (line 5) | class SseHub {
    method addClient (line 9) | addClient(client: ServerResponse): void {
    method removeClient (line 15) | removeClient(client: ServerResponse): void {
    method publish (line 22) | publish(event: string, payload: unknown): void {
    method closeAll (line 35) | closeAll(): void {
    method ensureHeartbeatTimer (line 43) | private ensureHeartbeatTimer(): void {
    method clearHeartbeatTimer (line 55) | private clearHeartbeatTimer(): void {

FILE: apps/api/src/lib/web-settings-store.ts
  constant STORAGE_DIR (line 5) | const STORAGE_DIR = path.resolve(process.cwd(), '.data')
  constant STORAGE_FILE (line 6) | const STORAGE_FILE = path.join(STORAGE_DIR, 'web-settings.json')
  type WebAppSettings (line 32) | type WebAppSettings = typeof defaultWebSettings
  class WebSettingsStore (line 34) | class WebSettingsStore {
    method ensureInitialized (line 38) | private async ensureInitialized(): Promise<void> {
    method get (line 57) | async get(): Promise<WebAppSettings> {
    method set (line 62) | async set(nextSettings: WebAppSettings): Promise<WebAppSettings> {

FILE: apps/api/src/server.ts
  constant MAX_PROXY_IMAGE_BYTES (line 15) | const MAX_PROXY_IMAGE_BYTES = 10 * 1024 * 1024
  constant MAX_PROXY_REDIRECTS (line 16) | const MAX_PROXY_REDIRECTS = 5

FILE: apps/desktop/build/after-pack.cjs
  constant BINARIES (line 5) | const BINARIES = [

FILE: apps/desktop/resources/drizzle/0000_swift_aaron_stack.sql
  type `download_history` (line 1) | CREATE TABLE `download_history` (
  type `subscription_items` (line 30) | CREATE TABLE `subscription_items` (
  type `subscription_items_subscription_idx` (line 44) | CREATE INDEX `subscription_items_subscription_idx` ON `subscription_item...
  type `subscriptions` (line 45) | CREATE TABLE `subscriptions` (

FILE: apps/desktop/scripts/ensure-native-deps.mjs
  function canLoadBetterSqlite3WithElectron (line 10) | function canLoadBetterSqlite3WithElectron() {

FILE: apps/desktop/scripts/setup-dev-binaries.js
  constant RESOURCES_DIR (line 19) | const RESOURCES_DIR = path.join(currentDirPath, '..', 'resources')
  constant FFMPEG_DIR (line 20) | const FFMPEG_DIR = path.join(RESOURCES_DIR, 'ffmpeg')
  constant YTDLP_BASE_URL (line 21) | const YTDLP_BASE_URL = 'https://github.com/yt-dlp/yt-dlp/releases/latest...
  constant DENO_BASE_URL (line 22) | const DENO_BASE_URL = 'https://github.com/denoland/deno/releases/latest/...
  constant MAC_FFMPEG_MODE (line 23) | const MAC_FFMPEG_MODE = (process.env.VIDBEE_MAC_FFMPEG_MODE || 'native')...
  constant GITHUB_TOKEN (line 24) | const GITHUB_TOKEN =
  constant PLATFORM_CONFIG (line 28) | const PLATFORM_CONFIG = {
  function log (line 103) | function log(message, type = 'info') {
  function ensureDir (line 114) | function ensureDir(dir) {
  function safeUnlink (line 120) | function safeUnlink(filePath) {
  function getDownloadHeaders (line 126) | function getDownloadHeaders(url) {
  function downloadFile (line 137) | function downloadFile(url, dest) {
  function downloadFileWithRetry (line 224) | async function downloadFileWithRetry(url, dest, retries = 3, delayMs = 2...
  function fetchJson (line 244) | function fetchJson(url) {
  function inferFfmpegInnerPath (line 282) | function inferFfmpegInnerPath(assetName, binaryName) {
  function resolveReleaseAsset (line 293) | async function resolveReleaseAsset(release) {
  function extractZip (line 323) | function extractZip(zipPath, extractDir) {
  function extractTarXz (line 349) | function extractTarXz(tarPath, extractDir) {
  function setExecutable (line 354) | function setExecutable(filePath) {
  function fileExists (line 360) | function fileExists(filePath) {
  function findFirstFileByName (line 364) | function findFirstFileByName(dirPath, fileName) {
  function formatBytes (line 385) | function formatBytes(bytes) {
  function checkBinary (line 395) | function checkBinary(filePath, args, label, options = {}) {
  function logBinaryVersion (line 422) | function logBinaryVersion(label, validation) {
  function getDenoAssetName (line 429) | function getDenoAssetName(platform, arch) {
  function getDenoOutputName (line 451) | function getDenoOutputName(platform) {
  function getMacFfmpegMode (line 455) | function getMacFfmpegMode() {
  function hasRequiredMacArchitectures (line 465) | function hasRequiredMacArchitectures(filePath, expectedArchitectures) {
  function runCommandOrThrow (line 491) | function runCommandOrThrow(command, args, label) {
  function resolveMacExtractedBinary (line 506) | function resolveMacExtractedBinary(extractDir, expectedInnerPath, binary...
  function resolveMacFfmpegDownloadUrl (line 520) | async function resolveMacFfmpegDownloadUrl(ffmpegConfig) {
  function downloadYtDlp (line 538) | async function downloadYtDlp(config) {
  function downloadFfmpegWindows (line 576) | async function downloadFfmpegWindows(config) {
  function downloadFfmpegMac (line 686) | async function downloadFfmpegMac(config) {
  function downloadFfmpegLinux (line 855) | async function downloadFfmpegLinux(config) {
  function downloadDenoRuntime (line 962) | async function downloadDenoRuntime() {
  function setup (line 1025) | async function setup() {

FILE: apps/desktop/src/main/config/logger-config.ts
  function configureLogger (line 7) | function configureLogger() {

FILE: apps/desktop/src/main/index.ts
  constant RENDERER_DIST_PATH (line 45) | const RENDERER_DIST_PATH = join(import.meta.dirname, '../renderer')
  type DeepLinkData (line 61) | interface DeepLinkData {
  function createWindow (line 176) | function createWindow(): void {
  function setupRendererErrorHandling (line 256) | function setupRendererErrorHandling(): void {
  function setupDownloadEvents (line 293) | function setupDownloadEvents(): void {
  function sanitizeRequestPath (line 386) | function sanitizeRequestPath(requestUrl: URL): string {
  function isWithinBase (line 393) | function isWithinBase(targetPath: string, basePath: string): boolean {
  function resolveVidbeeFilePath (line 398) | function resolveVidbeeFilePath(requestUrl: URL, userDataPath: string): s...
  function registerVidbeeProtocol (line 426) | function registerVidbeeProtocol(): void {
  function initAutoUpdater (line 446) | function initAutoUpdater(): void {

FILE: apps/desktop/src/main/ipc/index.ts
  type IpcServices (line 28) | type IpcServices = MergeIpcService<typeof services>

FILE: apps/desktop/src/main/ipc/services/app-service.ts
  class AppService (line 6) | class AppService extends IpcService {
    method getVersion (line 10) | getVersion(_context: IpcContext): string {
    method getPlatform (line 15) | getPlatform(_context: IpcContext): string {
    method getOsVersion (line 20) | getOsVersion(_context: IpcContext): string {
    method quit (line 51) | quit(_context: IpcContext): void {
    method showMessageBox (line 56) | async showMessageBox(
    method getSiteIcon (line 69) | async getSiteIcon(_context: IpcContext, domain: string): Promise<strin...

FILE: apps/desktop/src/main/ipc/services/browser-cookies-service.ts
  class BrowserCookiesService (line 7) | class BrowserCookiesService extends IpcService {
    method buildValidationResult (line 10) | private buildValidationResult(valid: boolean, reason?: string) {
    method isDirectory (line 17) | private isDirectory(target: string): boolean {
    method pickFirstDirectory (line 25) | private pickFirstDirectory(paths: string[]): string {
    method normalizeProfileInput (line 34) | private normalizeProfileInput(value: string): string {
    method getBrowserProfileBaseDirs (line 38) | private getBrowserProfileBaseDirs(platform: string, homeDir: string, b...
    method getDefaultProfilePath (line 140) | private getDefaultProfilePath(baseDirs: string[], browser: string): st...
    method findFirefoxProfilePath (line 153) | private findFirefoxProfilePath(profilesDir: string): string {
    method getBrowserProfilePath (line 173) | getBrowserProfilePath(_context: IpcContext, browser: string): string {
    method validateBrowserProfilePath (line 220) | validateBrowserProfilePath(

FILE: apps/desktop/src/main/ipc/services/download-service.ts
  class DownloadService (line 13) | class DownloadService extends IpcService {
    method getVideoInfo (line 17) | async getVideoInfo(_context: IpcContext, url: string): Promise<VideoIn...
    method getVideoInfoWithCommand (line 22) | async getVideoInfoWithCommand(
    method getPlaylistInfo (line 30) | async getPlaylistInfo(_context: IpcContext, url: string): Promise<Play...
    method startDownload (line 35) | startDownload(_context: IpcContext, id: string, options: DownloadOptio...
    method cancelDownload (line 40) | cancelDownload(_context: IpcContext, id: string): boolean {
    method getQueueStatus (line 45) | getQueueStatus(_context: IpcContext) {
    method getActiveDownloads (line 50) | getActiveDownloads(_context: IpcContext): DownloadItem[] {
    method updateDownloadInfo (line 55) | updateDownloadInfo(_context: IpcContext, id: string, updates: Partial<...
    method startPlaylistDownload (line 60) | async startPlaylistDownload(

FILE: apps/desktop/src/main/ipc/services/file-system-service.ts
  class FileSystemService (line 13) | class FileSystemService extends IpcService {
    method selectDirectory (line 17) | async selectDirectory(_context: IpcContext): Promise<string | null> {
    method selectFile (line 30) | async selectFile(_context: IpcContext): Promise<string | null> {
    method getDefaultDownloadPath (line 43) | getDefaultDownloadPath(_context: IpcContext): string {
    method openFileLocation (line 64) | async openFileLocation(_context: IpcContext, filePath: string): Promis...
    method openFile (line 110) | async openFile(_context: IpcContext, filePath: string): Promise<boolea...
    method copyFileToClipboard (line 139) | async copyFileToClipboard(_context: IpcContext, filePath: string): Pro...
    method sanitizePath (line 163) | private sanitizePath(target: string): string {
    method copyFileToClipboardByPlatform (line 167) | private async copyFileToClipboardByPlatform(resolvedPath: string): Pro...
    method openExternal (line 181) | async openExternal(_context: IpcContext, url: string): Promise<boolean> {
    method copyFileToClipboardWindows (line 191) | private async copyFileToClipboardWindows(resolvedPath: string): Promis...
    method copyFileToClipboardMac (line 227) | private async copyFileToClipboardMac(resolvedPath: string): Promise<vo...
    method copyFileToClipboardLinux (line 255) | private async copyFileToClipboardLinux(resolvedPath: string): Promise<...
    method escapeForPlist (line 262) | private escapeForPlist(value: string): string {
    method fileExists (line 272) | async fileExists(_context: IpcContext, filePath: string): Promise<bool...
    method deleteFile (line 291) | async deleteFile(_context: IpcContext, filePath: string): Promise<bool...

FILE: apps/desktop/src/main/ipc/services/history-service.ts
  class HistoryService (line 5) | class HistoryService extends IpcService {
    method getHistory (line 9) | getHistory(_context: IpcContext): DownloadHistoryItem[] {
    method getHistoryById (line 14) | getHistoryById(_context: IpcContext, id: string): DownloadHistoryItem ...
    method addHistoryItem (line 19) | addHistoryItem(_context: IpcContext, item: DownloadHistoryItem): void {
    method removeHistoryItem (line 24) | removeHistoryItem(_context: IpcContext, id: string): boolean {
    method removeHistoryItems (line 29) | removeHistoryItems(_context: IpcContext, ids: string[]): number {
    method removeHistoryByPlaylistId (line 34) | removeHistoryByPlaylistId(_context: IpcContext, playlistId: string): n...
    method clearHistory (line 39) | clearHistory(_context: IpcContext): void {
    method getHistoryCount (line 44) | getHistoryCount(_context: IpcContext): {

FILE: apps/desktop/src/main/ipc/services/settings-service.ts
  class SettingsService (line 8) | class SettingsService extends IpcService {
    method get (line 12) | get<K extends keyof AppSettings>(_context: IpcContext, key: K): AppSet...
    method set (line 17) | set<K extends keyof AppSettings>(_context: IpcContext, key: K, value: ...
    method getAll (line 34) | getAll(_context: IpcContext): AppSettings {
    method setAll (line 39) | setAll(_context: IpcContext, settings: Partial<AppSettings>): void {
    method reset (line 56) | reset(_context: IpcContext): void {

FILE: apps/desktop/src/main/ipc/services/subscription-service.ts
  type CreateSubscriptionOptions (line 18) | interface CreateSubscriptionOptions {
  class SubscriptionService (line 100) | class SubscriptionService extends IpcService {
    method list (line 104) | list(_context: IpcContext): SubscriptionRule[] {
    method resolve (line 109) | resolve(_context: IpcContext, url: string): SubscriptionResolvedFeed {
    method create (line 114) | async create(
    method update (line 146) | update(
    method remove (line 165) | remove(_context: IpcContext, id: string): boolean {
    method refresh (line 170) | async refresh(_context: IpcContext, id?: string): Promise<void> {
    method queueItem (line 175) | async queueItem(_context: IpcContext, id: string, itemId: string): Pro...

FILE: apps/desktop/src/main/ipc/services/thumbnail-service.ts
  class ThumbnailService (line 4) | class ThumbnailService extends IpcService {
    method getThumbnailPath (line 8) | async getThumbnailPath(

FILE: apps/desktop/src/main/ipc/services/update-service.ts
  class UpdateService (line 33) | class UpdateService extends IpcService {
    method checkForUpdates (line 37) | async checkForUpdates(
    method downloadUpdate (line 65) | async downloadUpdate(_context: IpcContext): Promise<{ success: boolean...
    method quitAndInstall (line 78) | quitAndInstall(_context: IpcContext): void {
    method getCurrentVersion (line 83) | getCurrentVersion(_context: IpcContext): string {
    method isAutoUpdateEnabled (line 88) | isAutoUpdateEnabled(_context: IpcContext): boolean {

FILE: apps/desktop/src/main/ipc/services/window-service.ts
  class WindowService (line 4) | class WindowService extends IpcService {
    method minimize (line 8) | minimize(_context: IpcContext): void {
    method maximize (line 16) | maximize(_context: IpcContext): void {
    method close (line 28) | close(_context: IpcContext): void {
    method isMaximized (line 36) | isMaximized(_context: IpcContext): boolean {

FILE: apps/desktop/src/main/lib/database.ts
  type DatabaseConnection (line 11) | interface DatabaseConnection {

FILE: apps/desktop/src/main/lib/database/migrate.ts
  constant MIGRATIONS_RELATIVE_PATH (line 7) | const MIGRATIONS_RELATIVE_PATH = 'resources/drizzle'
  constant MIGRATIONS_TABLE (line 8) | const MIGRATIONS_TABLE = '__drizzle_migrations'

FILE: apps/desktop/src/main/lib/database/schema.ts
  type SubscriptionRow (line 54) | type SubscriptionRow = typeof subscriptionsTable.$inferSelect
  type SubscriptionInsert (line 55) | type SubscriptionInsert = typeof subscriptionsTable.$inferInsert
  type SubscriptionItemRow (line 56) | type SubscriptionItemRow = typeof subscriptionItemsTable.$inferSelect
  type SubscriptionItemInsert (line 57) | type SubscriptionItemInsert = typeof subscriptionItemsTable.$inferInsert

FILE: apps/desktop/src/main/lib/download-engine.ts
  type DownloadProcess (line 53) | interface DownloadProcess {
  class DownloadEngine (line 58) | class DownloadEngine extends EventEmitter {
    method constructor (line 67) | constructor() {
    method getVideoInfo (line 81) | async getVideoInfo(url: string): Promise<VideoInfo> {
    method getVideoInfoWithCommand (line 150) | async getVideoInfoWithCommand(url: string): Promise<VideoInfoCommandRe...
    method getPlaylistInfo (line 233) | async getPlaylistInfo(url: string): Promise<PlaylistInfo> {
    method startPlaylistDownload (line 377) | async startPlaylistDownload(options: PlaylistDownloadOptions): Promise...
    method normalizeDownloadValue (line 511) | private normalizeDownloadValue(value?: string): string {
    method buildDownloadSignature (line 515) | private buildDownloadSignature(options: DownloadOptions): string {
    method hasDuplicateDownload (line 538) | private hasDuplicateDownload(options: DownloadOptions): boolean {
    method startDownload (line 544) | startDownload(id: string, options: DownloadOptions): boolean {
    method prefetchVideoInfo (line 600) | private async prefetchVideoInfo(id: string, options: DownloadOptions):...
    method executeDownload (line 636) | private async executeDownload(id: string, options: DownloadOptions): P...
    method cancelDownload (line 1241) | cancelDownload(id: string): boolean {
    method updateMaxConcurrent (line 1267) | updateMaxConcurrent(max: number): void {
    method getQueueStatus (line 1271) | getQueueStatus() {
    method getActiveDownloads (line 1275) | getActiveDownloads(): DownloadItem[] {
    method restoreActiveDownloads (line 1286) | restoreActiveDownloads(): void {
    method flushDownloadSession (line 1333) | flushDownloadSession(): void {
    method updateDownloadInfo (line 1341) | updateDownloadInfo(id: string, updates: Partial<DownloadItem>): void {
    method scheduleSessionPersist (line 1423) | private scheduleSessionPersist(): void {
    method persistSession (line 1433) | private persistSession(): void {
    method addToHistory (line 1452) | private addToHistory(
    method upsertHistoryEntry (line 1485) | private upsertHistoryEntry(

FILE: apps/desktop/src/main/lib/download-queue.ts
  type QueueItem (line 4) | interface QueueItem {
  class DownloadQueue (line 10) | class DownloadQueue extends EventEmitter {
    method constructor (line 16) | constructor(maxConcurrent = 5) {
    method setMaxConcurrent (line 21) | setMaxConcurrent(max: number): void {
    method add (line 26) | add(id: string, options: DownloadOptions, item: DownloadItem): void {
    method remove (line 32) | remove(id: string): boolean {
    method downloadCompleted (line 52) | downloadCompleted(id: string): void {
    method processQueue (line 62) | private processQueue(): void {
    method getQueueStatus (line 73) | getQueueStatus(): {
    method getActiveItems (line 85) | getActiveItems(): DownloadItem[] {
    method getQueuedItems (line 89) | getQueuedItems(): DownloadItem[] {
    method getActiveEntries (line 93) | getActiveEntries(): Array<{ options: DownloadOptions; item: DownloadIt...
    method getQueuedEntries (line 100) | getQueuedEntries(): Array<{ options: DownloadOptions; item: DownloadIt...
    method isDownloading (line 107) | isDownloading(id: string): boolean {
    method getCompletedDownload (line 111) | getCompletedDownload(id: string): QueueItem | undefined {
    method getItemDetails (line 115) | getItemDetails(id: string): { options: DownloadOptions; item: Download...
    method updateItemInfo (line 143) | updateItemInfo(id: string, updates: Partial<DownloadItem>): void {
    method clear (line 163) | clear(): void {

FILE: apps/desktop/src/main/lib/download-session-store.ts
  type DownloadSessionItem (line 7) | interface DownloadSessionItem {
  type DownloadSessionPayload (line 13) | interface DownloadSessionPayload {
  constant SESSION_FILE_NAME (line 19) | const SESSION_FILE_NAME = 'download-session.json'

FILE: apps/desktop/src/main/lib/ffmpeg-manager.ts
  class FfmpegManager (line 7) | class FfmpegManager {
    method initialize (line 10) | async initialize(): Promise<void> {
    method getPath (line 15) | getPath(): string {
    method getResourcesPath (line 22) | private getResourcesPath(): string {
    method findFfmpegBinary (line 33) | private async findFfmpegBinary(): Promise<string> {

FILE: apps/desktop/src/main/lib/history-manager.ts
  constant TAG_SEPARATOR (line 14) | const TAG_SEPARATOR = '\n'
  class HistoryManager (line 41) | class HistoryManager {
    method constructor (line 45) | constructor() {
    method initialize (line 49) | private initialize(): void {
    method getDatabase (line 58) | private getDatabase(): BetterSQLite3Database {
    method loadHistoryFromDatabase (line 67) | private loadHistoryFromDatabase(): void {
    method normalizeItem (line 78) | private normalizeItem(item: DownloadHistoryItem): DownloadHistoryItem {
    method mapItemToInsert (line 90) | private mapItemToInsert(item: DownloadHistoryItem): DownloadHistoryIns...
    method mapItemToUpdate (line 123) | private mapItemToUpdate(payload: DownloadHistoryInsert): Omit<Download...
    method mapRowToItem (line 128) | private mapRowToItem(row: DownloadHistoryRow): DownloadHistoryItem {
    method addHistoryItem (line 171) | addHistoryItem(item: DownloadHistoryItem): void {
    method getHistory (line 190) | getHistory(): DownloadHistoryItem[] {
    method getHistoryById (line 198) | getHistoryById(id: string): DownloadHistoryItem | undefined {
    method removeHistoryItem (line 202) | removeHistoryItem(id: string): boolean {
    method removeHistoryItems (line 217) | removeHistoryItems(ids: string[]): number {
    method removeHistoryByPlaylistId (line 244) | removeHistoryByPlaylistId(playlistId: string): number {
    method clearHistory (line 272) | clearHistory(): void {
    method clearHistoryByStatus (line 282) | clearHistoryByStatus(status: DownloadHistoryItem['status']): number {
    method getHistoryCount (line 306) | getHistoryCount(): {
    method hasHistoryForUrl (line 336) | hasHistoryForUrl(url: string): boolean {

FILE: apps/desktop/src/main/lib/subscription-manager.ts
  class SubscriptionManager (line 61) | class SubscriptionManager extends EventEmitter {
    method constructor (line 64) | constructor() {
    method getAll (line 73) | getAll(): SubscriptionRule[] {
    method getById (line 83) | getById(id: string): SubscriptionRule | undefined {
    method findDuplicateFeed (line 96) | findDuplicateFeed(
    method add (line 112) | add(payload: SubscriptionCreatePayload): SubscriptionRule {
    method update (line 148) | update(
    method remove (line 182) | remove(id: string): boolean {
    method replaceFeedItems (line 196) | replaceFeedItems(subscriptionId: string, items: SubscriptionFeedItem[]...
    method updateFeedItemQueueState (line 226) | updateFeedItemQueueState(
    method attachFeedItems (line 263) | private attachFeedItems(records: SubscriptionRule[]): SubscriptionRule...
    method getDatabase (line 293) | private getDatabase(): BetterSQLite3Database {
    method buildFeedKey (line 303) | private buildFeedKey(feedUrl: string): string {
    method insertRecord (line 322) | private insertRecord(record: SubscriptionRule): void {
    method updateRecord (line 332) | private updateRecord(record: SubscriptionRule): void {
    method mapRecordToInsert (line 342) | private mapRecordToInsert(record: SubscriptionRule): SubscriptionInsert {
    method mapRowToRecord (line 369) | private mapRowToRecord(row: SubscriptionRow): SubscriptionRule {
    method mapItemRowToFeedItem (line 395) | private mapItemRowToFeedItem(row: SubscriptionItemRow): SubscriptionFe...
    method emitUpdates (line 407) | private emitUpdates(): void {

FILE: apps/desktop/src/main/lib/subscription-scheduler.ts
  type ParserItem (line 18) | interface ParserItem {
  type TrackedDownload (line 37) | interface TrackedDownload {
  type FeedItem (line 45) | interface FeedItem {
  class SubscriptionScheduler (line 82) | class SubscriptionScheduler extends EventEmitter {
    method constructor (line 88) | constructor() {
    method start (line 135) | start(): void {
    method refreshInterval (line 139) | refreshInterval(): void {
    method runNow (line 146) | async runNow(subscriptionId?: string): Promise<void> {
    method queueItem (line 157) | async queueItem(subscriptionId: string, itemId: string): Promise<boole...
    method scheduleNextRun (line 170) | private scheduleNextRun(initialDelay?: number): void {
    method checkAll (line 181) | private async checkAll(): Promise<void> {
    method checkSubscription (line 205) | private async checkSubscription(subscription: SubscriptionRule): Promi...
    method normalizeFeedItems (line 279) | private normalizeFeedItems(items: ParserItem[]): FeedItem[] {
    method buildFeedItems (line 298) | private buildFeedItems(
    method resolveItemId (line 319) | private resolveItemId(item: ParserItem): string | null {
    method resolvePublishedAt (line 328) | private resolvePublishedAt(item: ParserItem): number {
    method resolveThumbnail (line 342) | private resolveThumbnail(item: ParserItem): string | undefined {
    method resolveSubscriptionCover (line 402) | private resolveSubscriptionCover(
    method extractImageFromHtml (line 432) | private extractImageFromHtml(html?: string): string | undefined {
    method filterRecentItems (line 455) | private filterRecentItems(subscription: SubscriptionRule, items: FeedI...
    method getLastKnownPublishedAt (line 463) | private getLastKnownPublishedAt(subscription: SubscriptionRule): number {
    method filterNewItems (line 468) | private filterNewItems(subscription: SubscriptionRule, items: FeedItem...
    method queueDownload (line 473) | private async queueDownload(
    method getTrackedDownloadByUrl (line 542) | private getTrackedDownloadByUrl(url: string): TrackedDownload | undefi...

FILE: apps/desktop/src/main/lib/thumbnail-cache.ts
  constant SUPPORTED_EXTENSIONS (line 9) | const SUPPORTED_EXTENSIONS = new Set(['.jpg', '.jpeg', '.png', '.webp', ...
  class ThumbnailCache (line 30) | class ThumbnailCache {
    method ensureCacheDir (line 34) | private ensureCacheDir(): string {
    method getThumbnailUrl (line 46) | async getThumbnailUrl(originalUrl: string): Promise<string | null> {
    method fetchAndCache (line 70) | private async fetchAndCache(originalUrl: string): Promise<string | nul...
    method findExistingPath (line 100) | private async findExistingPath(
    method exists (line 119) | private async exists(filePath: string): Promise<boolean> {
    method getBasePath (line 128) | private getBasePath(
    method toAppProtocolUrl (line 151) | private toAppProtocolUrl(filePath: string): string {

FILE: apps/desktop/src/main/lib/watermark-utils.ts
  constant WATERMARK_TITLE_MAX (line 7) | const WATERMARK_TITLE_MAX = 28
  constant WATERMARK_AUTHOR_MAX (line 8) | const WATERMARK_AUTHOR_MAX = 60

FILE: apps/desktop/src/main/lib/youtube-extractor-args.ts
  constant YOUTUBE_HOST_SUFFIXES (line 1) | const YOUTUBE_HOST_SUFFIXES = ['youtube.com', 'youtu.be', 'youtube-nocoo...
  constant YOUTUBE_SAFE_PLAYER_CLIENTS (line 2) | const YOUTUBE_SAFE_PLAYER_CLIENTS = 'default,-web,-web_safari'

FILE: apps/desktop/src/main/lib/ytdlp-manager.ts
  type YTDlpWrapInstance (line 11) | type YTDlpWrapInstance = InstanceType<typeof YTDlpWrapCtor>
  class YtDlpManager (line 13) | class YtDlpManager {
    method initialize (line 18) | async initialize(): Promise<void> {
    method getInstance (line 25) | getInstance(): YTDlpWrapInstance {
    method getPath (line 32) | getPath(): string {
    method getJsRuntimeArgs (line 39) | getJsRuntimeArgs(): string[] {
    method getResourcesPath (line 43) | private getResourcesPath(): string {
    method resolveBundledYtDlp (line 56) | private resolveBundledYtDlp(): string {
    method resolveJsRuntimeArgs (line 90) | private resolveJsRuntimeArgs(): string[] {
    method resolveJsRuntimePath (line 114) | private resolveJsRuntimePath(runtime: string): string | null {

FILE: apps/desktop/src/main/local-api.ts
  constant PORT_RANGE_START (line 7) | const PORT_RANGE_START = 27_100
  constant PORT_RANGE_END (line 8) | const PORT_RANGE_END = 27_120
  constant TOKEN_TTL_MS (line 9) | const TOKEN_TTL_MS = 60_000
  type TokenRecord (line 11) | interface TokenRecord {
  function startExtensionApiServer (line 161) | async function startExtensionApiServer(): Promise<number | null> {
  function stopExtensionApiServer (line 185) | async function stopExtensionApiServer(): Promise<void> {

FILE: apps/desktop/src/main/settings.ts
  constant OLD_DEFAULT_DOWNLOAD_PATH (line 13) | const OLD_DEFAULT_DOWNLOAD_PATH = path.join(os.homedir(), 'Downloads')
  constant DEFAULT_DOWNLOAD_PATH (line 26) | const DEFAULT_DOWNLOAD_PATH = resolveDefaultDownloadPath()
  class SettingsManager (line 28) | class SettingsManager {
    method constructor (line 32) | constructor() {
    method get (line 42) | get<K extends keyof AppSettings>(key: K): AppSettings[K] {
    method set (line 46) | set<K extends keyof AppSettings>(key: K, value: AppSettings[K]): void {
    method getAll (line 53) | getAll(): AppSettings {
    method setAll (line 61) | setAll(settings: Partial<AppSettings>): void {
    method reset (line 70) | reset(): void {
    method ensureDownloadDirectory (line 78) | private ensureDownloadDirectory(): void {

FILE: apps/desktop/src/main/tray.ts
  function t (line 12) | function t(key: 'showHome' | 'quit'): string {
  function findMainWindow (line 80) | function findMainWindow(): BrowserWindow | null {
  function createContextMenu (line 88) | function createContextMenu(): Menu {
  function createTray (line 132) | function createTray(): void {
  function updateTrayMenu (line 181) | function updateTrayMenu(): void {
  function destroyTray (line 190) | function destroyTray(): void {

FILE: apps/desktop/src/main/utils/auto-launch.ts
  constant SUPPORTED_PLATFORMS (line 4) | const SUPPORTED_PLATFORMS = new Set(['darwin', 'win32'])
  function isAutoLaunchSupported (line 6) | function isAutoLaunchSupported(): boolean {
  function applyAutoLaunchSetting (line 10) | function applyAutoLaunchSetting(enabled: boolean): void {

FILE: apps/desktop/src/main/utils/dock.ts
  function applyDockVisibility (line 6) | function applyDockVisibility(hideDockIcon: boolean): void {

FILE: apps/desktop/src/preload/index.d.ts
  type Window (line 5) | interface Window {

FILE: apps/desktop/src/renderer/src/App.tsx
  type Page (line 22) | type Page = 'home' | 'subscriptions' | 'settings' | 'about'
  function AppContent (line 50) | function AppContent() {
  function App (line 290) | function App() {

FILE: apps/desktop/src/renderer/src/components/download/DownloadDialog.tsx
  type DownloadDialogProps (line 66) | interface DownloadDialogProps {
  function DownloadDialog (line 71) | function DownloadDialog({

FILE: apps/desktop/src/renderer/src/components/download/DownloadItem.tsx
  type DownloadItemProps (line 137) | interface DownloadItemProps {
  type MetadataDetail (line 143) | interface MetadataDetail {
  function DownloadItem (line 190) | function DownloadItem({ download, isSelected = false, onToggleSelect }: ...

FILE: apps/desktop/src/renderer/src/components/download/PlaylistDownload.tsx
  type PlaylistDownloadProps (line 18) | interface PlaylistDownloadProps {
  function PlaylistDownload (line 36) | function PlaylistDownload({

FILE: apps/desktop/src/renderer/src/components/download/PlaylistDownloadGroup.tsx
  type PlaylistDownloadGroupProps (line 9) | interface PlaylistDownloadGroupProps {
  constant STORAGE_KEY_PREFIX (line 19) | const STORAGE_KEY_PREFIX = 'playlist_expanded_'
  function PlaylistDownloadGroup (line 43) | function PlaylistDownloadGroup({

FILE: apps/desktop/src/renderer/src/components/download/SingleVideoDownload.tsx
  type SingleVideoState (line 28) | interface SingleVideoState {
  type SingleVideoDownloadProps (line 39) | interface SingleVideoDownloadProps {
  type FormatListProps (line 109) | interface FormatListProps {
  function SingleVideoDownload (line 440) | function SingleVideoDownload({

FILE: apps/desktop/src/renderer/src/components/download/UnifiedDownloadHistory.tsx
  type StatusFilter (line 41) | type StatusFilter = 'all' | 'active' | 'completed' | 'error'
  type ConfirmAction (line 42) | type ConfirmAction =
  type UnifiedDownloadHistoryProps (line 94) | interface UnifiedDownloadHistoryProps {
  function UnifiedDownloadHistory (line 100) | function UnifiedDownloadHistory({

FILE: apps/desktop/src/renderer/src/components/error/ErrorBoundary.tsx
  type Props (line 6) | interface Props {
  type State (line 12) | interface State {
  class ErrorBoundary (line 17) | class ErrorBoundary extends Component<Props, State> {
    method constructor (line 18) | constructor(props: Props) {
    method getDerivedStateFromError (line 26) | static getDerivedStateFromError(error: Error): Partial<State> {
    method componentDidCatch (line 52) | async componentDidCatch(error: Error, errorInfo: ErrorInfo): Promise<v...
    method render (line 136) | render(): ReactNode {

FILE: apps/desktop/src/renderer/src/components/error/ErrorPage.tsx
  type ErrorInfo (line 17) | interface ErrorInfo {
  type ErrorPageProps (line 31) | interface ErrorPageProps {
  function ErrorPage (line 37) | function ErrorPage({ errorInfo, onReload, onGoHome }: ErrorPageProps) {
  function generateErrorReport (line 156) | function generateErrorReport(errorInfo: ErrorInfo): string {

FILE: apps/desktop/src/renderer/src/components/feedback/FeedbackLinks.tsx
  type AppInfo (line 4) | interface AppInfo {
  constant DEFAULT_APP_INFO (line 9) | const DEFAULT_APP_INFO: AppInfo = { appVersion: '', osVersion: '' }

FILE: apps/desktop/src/renderer/src/components/playlist/PlaylistPreviewCard.tsx
  type PlaylistPreviewCardProps (line 13) | interface PlaylistPreviewCardProps {
  function PlaylistPreviewCard (line 19) | function PlaylistPreviewCard({ playlist, entries, onClear }: PlaylistPre...

FILE: apps/desktop/src/renderer/src/components/subscription/SubscriptionFormDialog.tsx
  type SubscriptionFormData (line 41) | interface SubscriptionFormData {
  type SubscriptionFormDialogProps (line 51) | interface SubscriptionFormDialogProps {
  function SubscriptionFormDialog (line 59) | function SubscriptionFormDialog({

FILE: apps/desktop/src/renderer/src/components/ui/remote-image.tsx
  type RemoteImageProps (line 9) | type RemoteImageProps = Omit<SharedRemoteImageProps, 'cacheResolver' | '...
  function RemoteImage (line 13) | function RemoteImage(props: RemoteImageProps) {

FILE: apps/desktop/src/renderer/src/components/ui/sidebar.tsx
  type Page (line 8) | type Page = 'home' | 'subscriptions' | 'settings' | 'about'
  type SidebarProps (line 10) | interface SidebarProps {
  function Sidebar (line 16) | function Sidebar({ currentPage, onPageChange, onOpenSupportedSites }: Si...

FILE: apps/desktop/src/renderer/src/components/ui/title-bar.tsx
  type TitleBarProps (line 10) | interface TitleBarProps {
  function TitleBar (line 14) | function TitleBar({ platform }: TitleBarProps) {

FILE: apps/desktop/src/renderer/src/components/video/AdvancedOptions.tsx
  type AdvancedOptionsProps (line 12) | interface AdvancedOptionsProps {
  function AdvancedOptions (line 22) | function AdvancedOptions({

FILE: apps/desktop/src/renderer/src/data/popularSites.ts
  type PopularSite (line 1) | interface PopularSite {

FILE: apps/desktop/src/renderer/src/hooks/use-download-events.ts
  function useDownloadEvents (line 19) | function useDownloadEvents() {

FILE: apps/desktop/src/renderer/src/hooks/use-history-sync.ts
  function useHistorySync (line 7) | function useHistorySync() {

FILE: apps/desktop/src/renderer/src/hooks/use-ipc-example.ts
  function useIpcExample (line 13) | function useIpcExample() {

FILE: apps/desktop/src/renderer/src/lib/utils.ts
  function cn (line 4) | function cn(...inputs: ClassValue[]) {

FILE: apps/desktop/src/renderer/src/main.tsx
  function setupGlobalErrorHandlers (line 27) | function setupGlobalErrorHandlers(): void {

FILE: apps/desktop/src/renderer/src/pages/About.tsx
  type AboutResource (line 27) | interface AboutResource {
  type LatestVersionState (line 36) | type LatestVersionState =
  function About (line 42) | function About() {

FILE: apps/desktop/src/renderer/src/pages/Home.tsx
  type HomeProps (line 3) | interface HomeProps {
  function Home (line 9) | function Home({ onOpenSupportedSites, onOpenSettings, onOpenCookiesSetti...

FILE: apps/desktop/src/renderer/src/pages/Settings.tsx
  function Settings (line 39) | function Settings() {

FILE: apps/desktop/src/renderer/src/pages/Subscriptions.tsx
  type SubscriptionItemStatus (line 78) | type SubscriptionItemStatus = DownloadStatus | 'queued' | 'notQueued'
  function SubscriptionTab (line 126) | function SubscriptionTab({
  function Subscriptions (line 247) | function Subscriptions() {
  type SubscriptionTabProps (line 464) | interface SubscriptionTabProps {
  type SubscriptionRuleUpdateForm (line 472) | type SubscriptionRuleUpdateForm = SubscriptionFormData
  function SubscriptionCard (line 474) | function SubscriptionCard({ subscription }: { subscription: Subscription...

FILE: apps/desktop/src/renderer/src/store/downloads.ts
  type DownloadRecord (line 4) | type DownloadRecord = DownloadItem & {

FILE: apps/desktop/src/renderer/src/store/subscriptions.ts
  type CreateSubscriptionForm (line 34) | interface CreateSubscriptionForm {

FILE: apps/desktop/src/renderer/src/store/update.ts
  type UpdateReadyState (line 3) | interface UpdateReadyState {
  type UpdateAvailableState (line 8) | interface UpdateAvailableState {

FILE: apps/desktop/src/shared/constants.ts
  constant APP_PROTOCOL (line 1) | const APP_PROTOCOL = 'vidbee'
  constant APP_PROTOCOL_SCHEME (line 2) | const APP_PROTOCOL_SCHEME = `${APP_PROTOCOL}://`

FILE: apps/desktop/src/shared/types/index.ts
  type VideoFormat (line 4) | interface VideoFormat {
  type VideoInfo (line 23) | interface VideoInfo {
  type VideoInfoCommandResult (line 36) | interface VideoInfoCommandResult {
  type DownloadProgress (line 42) | interface DownloadProgress {
  type DownloadStatus (line 50) | type DownloadStatus =
  type DownloadItem (line 58) | interface DownloadItem {
  type SubscriptionFeedItem (line 95) | interface SubscriptionFeedItem {
  type DownloadHistoryItem (line 105) | interface DownloadHistoryItem {
  type DownloadOptions (line 138) | interface DownloadOptions {
  type PlaylistEntry (line 154) | interface PlaylistEntry {
  type PlaylistInfo (line 162) | interface PlaylistInfo {
  type PlaylistDownloadOptions (line 169) | interface PlaylistDownloadOptions {
  type PlaylistDownloadEntry (line 181) | interface PlaylistDownloadEntry {
  type PlaylistDownloadResult (line 189) | interface PlaylistDownloadResult {
  type SubscriptionPlatform (line 201) | type SubscriptionPlatform = 'youtube' | 'bilibili' | 'custom'
  type SubscriptionStatus (line 203) | type SubscriptionStatus = 'idle' | 'checking' | 'up-to-date' | 'failed'
  constant SUBSCRIPTION_DUPLICATE_FEED_ERROR (line 205) | const SUBSCRIPTION_DUPLICATE_FEED_ERROR = 'SUBSCRIPTION_DUPLICATE_FEED_URL'
  type SubscriptionRule (line 207) | interface SubscriptionRule {
  type SubscriptionResolvedFeed (line 231) | interface SubscriptionResolvedFeed {
  type SubscriptionCreatePayload (line 237) | interface SubscriptionCreatePayload {
  type SubscriptionUpdatePayload (line 249) | interface SubscriptionUpdatePayload {
  type OneClickQualityPreset (line 264) | type OneClickQualityPreset = 'best' | 'good' | 'normal' | 'bad' | 'worst'
  type AppSettings (line 266) | interface AppSettings {
  constant DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE (line 292) | const DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE = '%(uploader)s/%(title)s.%...

FILE: apps/docs/src/app/(docs)/[[...slug]]/page.tsx
  function Page (line 10) | async function Page(props: PageProps<'/[[...slug]]'>) {
  function generateStaticParams (line 50) | async function generateStaticParams() {
  function generateMetadata (line 60) | async function generateMetadata(

FILE: apps/docs/src/app/(docs)/layout.tsx
  function Layout (line 20) | async function Layout({

FILE: apps/docs/src/app/[lang]/(docs)/[[...slug]]/page.tsx
  function Page (line 9) | async function Page(props: PageProps<'/[lang]/[[...slug]]'>) {
  function generateStaticParams (line 49) | async function generateStaticParams() {
  function generateMetadata (line 53) | async function generateMetadata(

FILE: apps/docs/src/app/[lang]/(docs)/layout.tsx
  function Layout (line 6) | async function Layout({

FILE: apps/docs/src/app/[lang]/layout.tsx
  function Layout (line 17) | async function Layout({

FILE: apps/docs/src/app/layout.tsx
  function Layout (line 24) | async function Layout({

FILE: apps/docs/src/app/sitemap.ts
  function buildPath (line 9) | function buildPath(segments: string[]): string {
  function sitemap (line 16) | async function sitemap(): Promise<MetadataRoute.Sitemap> {

FILE: apps/docs/src/components/ai/page-actions.tsx
  function LLMCopyButton (line 11) | function LLMCopyButton({
  function ViewOptions (line 60) | function ViewOptions({
  function GitHubEditButton (line 232) | function GitHubEditButton({ href }: { href: string }) {

FILE: apps/docs/src/lib/i18n.ts
  type Locale (line 11) | type Locale = (typeof i18n.languages)[number];
  function isLocale (line 15) | function isLocale(value?: string): value is Locale {
  function resolveLocaleFromSlug (line 19) | function resolveLocaleFromSlug(slug?: string[]): Locale {
  function stripLocaleFromSlug (line 26) | function stripLocaleFromSlug(slug?: string[]): string[] {

FILE: apps/docs/src/lib/layout.shared.tsx
  function baseOptions (line 4) | function baseOptions(_locale: string): BaseLayoutProps {

FILE: apps/docs/src/lib/source.ts
  function getPageImage (line 14) | function getPageImage(page: InferPageType<typeof source>) {
  function getLLMText (line 25) | async function getLLMText(page: InferPageType<typeof source>) {

FILE: apps/docs/src/mdx-components.tsx
  function getMDXComponents (line 35) | function getMDXComponents(components?: MDXComponents): MDXComponents {

FILE: apps/extension/entrypoints/background.ts
  type VideoFormat (line 1) | interface VideoFormat {
  type VideoInfo (line 16) | interface VideoInfo {
  type VideoInfoCacheEntry (line 23) | interface VideoInfoCacheEntry {
  constant PORT_RANGE_START (line 31) | const PORT_RANGE_START = 27_100
  constant PORT_RANGE_END (line 32) | const PORT_RANGE_END = 27_120
  constant STATUS_TIMEOUT_MS (line 33) | const STATUS_TIMEOUT_MS = 800
  constant INFO_TIMEOUT_MS (line 34) | const INFO_TIMEOUT_MS = 60_000
  constant CACHE_TTL_MS (line 35) | const CACHE_TTL_MS = 5 * 60 * 1000

FILE: apps/extension/entrypoints/popup/App.tsx
  type VideoFormat (line 4) | interface VideoFormat {
  type VideoInfo (line 19) | interface VideoInfo {
  constant CACHE_TTL_MS (line 26) | const CACHE_TTL_MS = 60 * 60 * 1000
  type VideoInfoCacheEntry (line 73) | interface VideoInfoCacheEntry {
  type VideoGroup (line 81) | interface VideoGroup {
  function App (line 117) | function App() {

FILE: apps/web/src/components/download/download-dialog.tsx
  type DownloadDialogProps (line 72) | interface DownloadDialogProps {
  function DownloadDialog (line 76) | function DownloadDialog({ onDownloadsChanged }: DownloadDialogProps) {

FILE: apps/web/src/components/download/download-item.tsx
  type DownloadItemProps (line 64) | interface DownloadItemProps {
  type MetadataDetail (line 74) | interface MetadataDetail {
  function DownloadItem (line 229) | function DownloadItem({

FILE: apps/web/src/components/download/playlist-download-group.tsx
  type PlaylistDownloadGroupProps (line 9) | interface PlaylistDownloadGroupProps {
  constant STORAGE_KEY_PREFIX (line 23) | const STORAGE_KEY_PREFIX = "playlist_expanded_";
  function PlaylistDownloadGroup (line 47) | function PlaylistDownloadGroup({

FILE: apps/web/src/components/download/playlist-download.tsx
  type PlaylistDownloadProps (line 18) | interface PlaylistDownloadProps {
  function PlaylistDownload (line 36) | function PlaylistDownload({

FILE: apps/web/src/components/download/single-video-download.tsx
  type SingleVideoState (line 29) | interface SingleVideoState {
  type SingleVideoDownloadProps (line 39) | interface SingleVideoDownloadProps {
  type FormatListProps (line 110) | interface FormatListProps {
  function SingleVideoDownload (line 469) | function SingleVideoDownload({

FILE: apps/web/src/components/download/types.ts
  type DownloadRecord (line 3) | type DownloadRecord = DownloadTask & {
  type StatusFilter (line 7) | type StatusFilter = "all" | "active" | "completed" | "error";

FILE: apps/web/src/components/layout/app-shell.tsx
  type AppPage (line 10) | type AppPage = "about" | "download" | "settings";
  type AppShellProps (line 12) | interface AppShellProps {

FILE: apps/web/src/components/pages/about-page.tsx
  type AboutResource (line 27) | interface AboutResource {
  type LatestVersionState (line 35) | type LatestVersionState =
  constant AUTO_UPDATE_KEY (line 41) | const AUTO_UPDATE_KEY = "vidbee.web.auto-update";
  constant PREVIEW_CHANNEL_KEY (line 42) | const PREVIEW_CHANNEL_KEY = "vidbee.web.preview-channel";
  constant SHARE_TARGET_URL (line 43) | const SHARE_TARGET_URL = "https://vidbee.org";
  constant APP_VERSION (line 44) | const APP_VERSION = __APP_VERSION__;

FILE: apps/web/src/components/pages/download-page.tsx
  type ConfirmAction (line 35) | type ConfirmAction =
  constant POLL_INTERVAL_MS (line 44) | const POLL_INTERVAL_MS = 2000;

FILE: apps/web/src/components/pages/settings-page.tsx
  type SettingsTab (line 58) | type SettingsTab = "advanced" | "cookies" | "general";
  type BrowserProfileValidationReason (line 60) | type BrowserProfileValidationReason =
  type BrowserProfileValidation (line 66) | interface BrowserProfileValidation {
  type ServerDirectoryEntry (line 71) | interface ServerDirectoryEntry {
  constant WINDOWS_PLATFORM (line 76) | const WINDOWS_PLATFORM = "win32";
  constant MAC_PLATFORM (line 77) | const MAC_PLATFORM = "darwin";
  constant MAX_SETTINGS_UPLOAD_BYTES (line 78) | const MAX_SETTINGS_UPLOAD_BYTES = 500_000;
  constant ABSOLUTE_WINDOWS_PATH_REGEX (line 94) | const ABSOLUTE_WINDOWS_PATH_REGEX = /^[A-Za-z]:\\/;

FILE: apps/web/src/lib/download-format-preferences.ts
  type WebDownloadSettings (line 10) | interface WebDownloadSettings {
  constant DEFAULT_WEB_DOWNLOAD_SETTINGS (line 16) | const DEFAULT_WEB_DOWNLOAD_SETTINGS: WebDownloadSettings = {

FILE: apps/web/src/lib/remote-image-proxy.ts
  constant IMAGE_PROXY_PATH (line 3) | const IMAGE_PROXY_PATH = "images/proxy";

FILE: apps/web/src/lib/web-settings.ts
  type OneClickQualityPreset (line 8) | type OneClickQualityPreset =
  type ThemeValue (line 15) | type ThemeValue = "light" | "dark" | "system";
  type WebAppSettings (line 17) | interface WebAppSettings {
  constant WEB_SETTINGS_STORAGE_KEY (line 41) | const WEB_SETTINGS_STORAGE_KEY = "vidbee.web.settings";

FILE: apps/web/src/routeTree.gen.ts
  type FileRoutesByFullPath (line 32) | interface FileRoutesByFullPath {
  type FileRoutesByTo (line 37) | interface FileRoutesByTo {
  type FileRoutesById (line 42) | interface FileRoutesById {
  type FileRouteTypes (line 48) | interface FileRouteTypes {
  type RootRouteChildren (line 56) | interface RootRouteChildren {
  type FileRoutesByPath (line 63) | interface FileRoutesByPath {
  type Register (line 100) | interface Register {

FILE: apps/web/src/router.tsx
  function getRouter (line 4) | function getRouter() {
  type Register (line 17) | interface Register {

FILE: apps/web/src/routes/__root.tsx
  function RootDocument (line 35) | function RootDocument({ children }: { children: React.ReactNode }) {
  function RootHydrationEffects (line 62) | function RootHydrationEffects() {

FILE: packages/db/src/history.ts
  type DownloadHistoryRow (line 34) | type DownloadHistoryRow = typeof downloadHistoryTable.$inferSelect
  type DownloadHistoryInsert (line 35) | type DownloadHistoryInsert = typeof downloadHistoryTable.$inferInsert

FILE: packages/downloader-core/src/browser-cookies-setting.ts
  type BrowserCookiesSetting (line 1) | interface BrowserCookiesSetting {

FILE: packages/downloader-core/src/downloader-core.ts
  type YtDlpExecProcess (line 29) | interface YtDlpExecProcess {
  type YtDlpWrapInstance (line 41) | interface YtDlpWrapInstance {
  type YtDlpWrapConstructor (line 45) | type YtDlpWrapConstructor = new (binaryPath: string) => YtDlpWrapInstance
  type ActiveTask (line 48) | interface ActiveTask {
  type RawVideoInfo (line 53) | interface RawVideoInfo {
  type RawPlaylistEntry (line 84) | interface RawPlaylistEntry {
  type RawPlaylistInfo (line 94) | interface RawPlaylistInfo {
  type ProgressPayload (line 100) | interface ProgressPayload {
  type DownloaderCoreOptions (line 108) | interface DownloaderCoreOptions {
  constant DEFAULT_DOWNLOAD_DIR (line 114) | const DEFAULT_DOWNLOAD_DIR = path.join(os.homedir(), 'Downloads', 'VidBee')
  constant DEFAULT_MAX_CONCURRENT (line 115) | const DEFAULT_MAX_CONCURRENT = 3
  constant MAX_TASK_LOG_LENGTH (line 116) | const MAX_TASK_LOG_LENGTH = 80_000
  constant FFMPEG_NOT_FOUND_ERROR (line 117) | const FFMPEG_NOT_FOUND_ERROR =
  constant MODULE_DIR (line 119) | const MODULE_DIR = path.dirname(fileURLToPath(import.meta.url))
  constant REPO_ROOT_FROM_MODULE (line 120) | const REPO_ROOT_FROM_MODULE = path.resolve(MODULE_DIR, '../../..')
  class DownloaderCore (line 489) | class DownloaderCore extends EventEmitter {
    method constructor (line 503) | constructor(options: DownloaderCoreOptions = {}) {
    method initialize (line 511) | async initialize(): Promise<void> {
    method getYtDlp (line 524) | private getYtDlp(): YtDlpWrapInstance {
    method publishHistory (line 531) | private publishHistory(): void {
    method resolveRuntimeSettings (line 535) | private resolveRuntimeSettings(
    method updateTask (line 553) | private updateTask(id: string, patch: Partial<DownloadTask>): Download...
    method runJsonCommand (line 573) | private async runJsonCommand<T>(args: string[]): Promise<T> {
    method getVideoInfo (line 597) | async getVideoInfo(url: string, runtimeSettings?: DownloadRuntimeSetti...
    method getPlaylistInfo (line 641) | async getPlaylistInfo(
    method startPlaylistDownload (line 681) | async startPlaylistDownload(input: PlaylistDownloadInput): Promise<Pla...
    method createDownload (line 754) | async createDownload(input: CreateDownloadInput): Promise<DownloadTask> {
    method processQueue (line 798) | private processQueue(): void {
    method cancelDownload (line 982) | async cancelDownload(id: string): Promise<boolean> {
    method removeHistoryItems (line 1003) | removeHistoryItems(ids: string[]): number {
    method removeHistoryByPlaylist (line 1030) | removeHistoryByPlaylist(playlistId: string): number {
    method listDownloads (line 1046) | listDownloads(): DownloadTask[] {
    method listHistory (line 1053) | listHistory(): DownloadTask[] {
    method getStatus (line 1059) | getStatus(): { active: number; pending: number } {

FILE: packages/downloader-core/src/format-preferences.ts
  type OneClickQualityPreset (line 1) | type OneClickQualityPreset = 'best' | 'good' | 'normal' | 'bad' | 'worst'
  type OneClickFormatSettings (line 3) | interface OneClickFormatSettings {

FILE: packages/downloader-core/src/types.ts
  type DownloadType (line 1) | type DownloadType = 'video' | 'audio'
  type DownloadStatus (line 3) | type DownloadStatus =
  type DownloadProgress (line 11) | interface DownloadProgress {
  type DownloadTask (line 19) | interface DownloadTask {
  type DownloadRuntimeSettings (line 50) | interface DownloadRuntimeSettings {
  type VideoInfoInput (line 62) | interface VideoInfoInput {
  type PlaylistInfoInput (line 67) | interface PlaylistInfoInput {
  type CreateDownloadInput (line 72) | interface CreateDownloadInput {
  type VideoFormat (line 98) | interface VideoFormat {
  type VideoInfo (line 117) | interface VideoInfo {
  type PlaylistEntry (line 131) | interface PlaylistEntry {
  type PlaylistInfo (line 139) | interface PlaylistInfo {
  type PlaylistDownloadInput (line 146) | interface PlaylistDownloadInput {
  type PlaylistDownloadEntry (line 160) | interface PlaylistDownloadEntry {
  type PlaylistDownloadResult (line 168) | interface PlaylistDownloadResult {
  type FilePathInput (line 179) | interface FilePathInput {
  type DirectoryListInput (line 183) | interface DirectoryListInput {
  type UploadSettingsFileKind (line 187) | type UploadSettingsFileKind = 'cookies' | 'config'
  type UploadSettingsFileInput (line 189) | interface UploadSettingsFileInput {
  type DirectoryEntry (line 195) | interface DirectoryEntry {
  type FileExistsOutput (line 200) | interface FileExistsOutput {
  type FileOperationOutput (line 204) | interface FileOperationOutput {
  type ListDirectoriesOutput (line 208) | interface ListDirectoriesOutput {
  type UploadSettingsFileOutput (line 214) | interface UploadSettingsFileOutput {

FILE: packages/downloader-core/src/yt-dlp-args.ts
  type YtDlpDownloadSettings (line 4) | interface YtDlpDownloadSettings {
  type YtDlpDownloadOptions (line 16) | interface YtDlpDownloadOptions {
  constant YOUTUBE_HOST_SUFFIXES (line 28) | const YOUTUBE_HOST_SUFFIXES = ['youtube.com', 'youtu.be', 'youtube-nocoo...
  constant YOUTUBE_SAFE_PLAYER_CLIENTS (line 29) | const YOUTUBE_SAFE_PLAYER_CLIENTS = 'default,-web,-web_safari'
  constant DEFAULT_FILENAME_TEMPLATE (line 30) | const DEFAULT_FILENAME_TEMPLATE = '%(title)s via VidBee.%(ext)s'

FILE: packages/i18n/src/languages.ts
  type LanguageDefinition (line 1) | interface LanguageDefinition {
  type LanguageCode (line 80) | type LanguageCode = keyof typeof languages
  function normalizeLanguageCode (line 91) | function normalizeLanguageCode(code: string | null | undefined): Languag...

FILE: packages/i18n/src/resources.ts
  type TranslationDictionary (line 18) | type TranslationDictionary = typeof en

FILE: packages/ui/src/components/ui/accordion.tsx
  function Accordion (line 6) | function Accordion({ ...props }: React.ComponentProps<typeof AccordionPr...
  function AccordionItem (line 10) | function AccordionItem({
  function AccordionTrigger (line 23) | function AccordionTrigger({
  function AccordionContent (line 45) | function AccordionContent({

FILE: packages/ui/src/components/ui/add-url-popover.tsx
  type AddUrlPopoverProps (line 8) | interface AddUrlPopoverProps {

FILE: packages/ui/src/components/ui/app-sidebar-icons.tsx
  type AppSidebarIcons (line 13) | interface AppSidebarIcons {

FILE: packages/ui/src/components/ui/app-sidebar.tsx
  type AppSidebarIcon (line 6) | interface AppSidebarIcon {
  type AppSidebarItem (line 11) | interface AppSidebarItem {
  type AppSidebarProps (line 23) | interface AppSidebarProps {
  function AppSidebar (line 73) | function AppSidebar({

FILE: packages/ui/src/components/ui/badge.tsx
  function Badge (line 25) | function Badge({

FILE: packages/ui/src/components/ui/button.tsx
  type ButtonProps (line 32) | interface ButtonProps

FILE: packages/ui/src/components/ui/checkbox.tsx
  function Checkbox (line 6) | function Checkbox({ className, ...props }: React.ComponentProps<typeof C...

FILE: packages/ui/src/components/ui/context-menu.tsx
  function ContextMenu (line 6) | function ContextMenu({ ...props }: React.ComponentProps<typeof ContextMe...
  function ContextMenuTrigger (line 10) | function ContextMenuTrigger({
  function ContextMenuGroup (line 16) | function ContextMenuGroup({ ...props }: React.ComponentProps<typeof Cont...
  function ContextMenuPortal (line 20) | function ContextMenuPortal({ ...props }: React.ComponentProps<typeof Con...
  function ContextMenuSub (line 24) | function ContextMenuSub({ ...props }: React.ComponentProps<typeof Contex...
  function ContextMenuRadioGroup (line 28) | function ContextMenuRadioGroup({
  function ContextMenuSubTrigger (line 34) | function ContextMenuSubTrigger({
  function ContextMenuSubContent (line 58) | function ContextMenuSubContent({
  function ContextMenuContent (line 74) | function ContextMenuContent({
  function ContextMenuItem (line 92) | function ContextMenuItem({
  function ContextMenuCheckboxItem (line 115) | function ContextMenuCheckboxItem({
  function ContextMenuRadioItem (line 141) | function ContextMenuRadioItem({
  function ContextMenuLabel (line 165) | function ContextMenuLabel({
  function ContextMenuSeparator (line 182) | function ContextMenuSeparator({
  function ContextMenuShortcut (line 195) | function ContextMenuShortcut({ className, ...props }: React.ComponentPro...

FILE: packages/ui/src/components/ui/dialog.tsx
  function Dialog (line 6) | function Dialog({ ...props }: React.ComponentProps<typeof DialogPrimitiv...
  function DialogTrigger (line 10) | function DialogTrigger({ ...props }: React.ComponentProps<typeof DialogP...
  function DialogPortal (line 14) | function DialogPortal({ ...props }: React.ComponentProps<typeof DialogPr...
  function DialogClose (line 18) | function DialogClose({ ...props }: React.ComponentProps<typeof DialogPri...
  function DialogOverlay (line 22) | function DialogOverlay({
  function DialogContent (line 38) | function DialogContent({
  function DialogHeader (line 72) | function DialogHeader({ className, ...props }: React.ComponentProps<'div...
  function DialogFooter (line 82) | function DialogFooter({ className, ...props }: React.ComponentProps<'div...
  function DialogTitle (line 92) | function DialogTitle({ className, ...props }: React.ComponentProps<typeo...
  function DialogDescription (line 102) | function DialogDescription({

FILE: packages/ui/src/components/ui/download-dialog-layout.tsx
  type DownloadDialogLayoutProps (line 9) | interface DownloadDialogLayoutProps {

FILE: packages/ui/src/components/ui/download-empty-state.tsx
  type DownloadEmptyStateProps (line 4) | interface DownloadEmptyStateProps {

FILE: packages/ui/src/components/ui/download-filter-bar.tsx
  type DownloadFilterItem (line 5) | interface DownloadFilterItem<TFilter extends string> {
  type DownloadFilterBarProps (line 11) | interface DownloadFilterBarProps<TFilter extends string> {

FILE: packages/ui/src/components/ui/dropdown-menu.tsx
  function DropdownMenu (line 6) | function DropdownMenu({ ...props }: React.ComponentProps<typeof Dropdown...
  function DropdownMenuPortal (line 10) | function DropdownMenuPortal({
  function DropdownMenuTrigger (line 16) | function DropdownMenuTrigger({
  function DropdownMenuContent (line 22) | function DropdownMenuContent({
  function DropdownMenuGroup (line 42) | function DropdownMenuGroup({ ...props }: React.ComponentProps<typeof Dro...
  function DropdownMenuItem (line 46) | function DropdownMenuItem({
  function DropdownMenuCheckboxItem (line 69) | function DropdownMenuCheckboxItem({
  function DropdownMenuRadioGroup (line 95) | function DropdownMenuRadioGroup({
  function DropdownMenuRadioItem (line 101) | function DropdownMenuRadioItem({
  function DropdownMenuLabel (line 125) | function DropdownMenuLabel({
  function DropdownMenuSeparator (line 142) | function DropdownMenuSeparator({
  function DropdownMenuShortcut (line 155) | function DropdownMenuShortcut({ className, ...props }: React.ComponentPr...
  function DropdownMenuSub (line 165) | function DropdownMenuSub({ ...props }: React.ComponentProps<typeof Dropd...
  function DropdownMenuSubTrigger (line 169) | function DropdownMenuSubTrigger({
  function DropdownMenuSubContent (line 193) | function DropdownMenuSubContent({

FILE: packages/ui/src/components/ui/feedback-link-buttons.tsx
  constant DOWNLOAD_FEEDBACK_ISSUE_TITLE (line 7) | const DOWNLOAD_FEEDBACK_ISSUE_TITLE = '[Bug]: Download error report'
  constant FEEDBACK_TWEET_PREFIX (line 9) | const FEEDBACK_TWEET_PREFIX = '@nexmoex VidBee'
  constant FEEDBACK_UNKNOWN_ERROR (line 10) | const FEEDBACK_UNKNOWN_ERROR = 'Unknown error'
  constant FEEDBACK_UNKNOWN_VALUE (line 11) | const FEEDBACK_UNKNOWN_VALUE = 'Unknown'
  constant FEEDBACK_SOURCE_LABEL (line 12) | const FEEDBACK_SOURCE_LABEL = 'Source URL'
  constant FEEDBACK_ERROR_LABEL (line 13) | const FEEDBACK_ERROR_LABEL = 'Error'
  constant FEEDBACK_COMMAND_LABEL (line 14) | const FEEDBACK_COMMAND_LABEL = 'yt-dlp command'
  constant FEEDBACK_MAX_GITHUB_URL_LENGTH (line 15) | const FEEDBACK_MAX_GITHUB_URL_LENGTH = 7000
  constant FAQ_URL (line 16) | const FAQ_URL = 'https://docs.vidbee.org/faq/'
  type FeedbackLinkButtonsProps (line 43) | interface FeedbackLinkButtonsProps {

FILE: packages/ui/src/components/ui/hover-card.tsx
  function HoverCard (line 5) | function HoverCard({ ...props }: React.ComponentProps<typeof HoverCardPr...
  function HoverCardTrigger (line 9) | function HoverCardTrigger({ ...props }: React.ComponentProps<typeof Hove...
  function HoverCardContent (line 13) | function HoverCardContent({

FILE: packages/ui/src/components/ui/image-with-placeholder.tsx
  type ImageWithPlaceholderProps (line 5) | interface ImageWithPlaceholderProps {
  function ImageWithPlaceholder (line 15) | function ImageWithPlaceholder({

FILE: packages/ui/src/components/ui/item.tsx
  function ItemGroup (line 7) | function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
  function ItemSeparator (line 17) | function ItemSeparator({ className, ...props }: React.ComponentProps<typ...
  function Item (line 59) | function Item({
  function ItemMedia (line 96) | function ItemMedia({
  function ItemContent (line 111) | function ItemContent({ className, ...props }: React.ComponentProps<'div'...
  function ItemTitle (line 121) | function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
  function ItemDescription (line 131) | function ItemDescription({ className, ...props }: React.ComponentProps<'...
  function ItemActions (line 145) | function ItemActions({ className, ...props }: React.ComponentProps<'div'...
  function ItemHeader (line 151) | function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
  function ItemFooter (line 161) | function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {

FILE: packages/ui/src/components/ui/popover.tsx
  function Popover (line 5) | function Popover({ ...props }: React.ComponentProps<typeof PopoverPrimit...
  function PopoverTrigger (line 9) | function PopoverTrigger({ ...props }: React.ComponentProps<typeof Popove...
  function PopoverContent (line 13) | function PopoverContent({
  function PopoverAnchor (line 35) | function PopoverAnchor({ ...props }: React.ComponentProps<typeof Popover...

FILE: packages/ui/src/components/ui/progress.tsx
  function Progress (line 5) | function Progress({

FILE: packages/ui/src/components/ui/radio-group.tsx
  function RadioGroup (line 6) | function RadioGroup({
  function RadioGroupItem (line 19) | function RadioGroupItem({

FILE: packages/ui/src/components/ui/remote-image.tsx
  type RemoteImageProps (line 5) | interface RemoteImageProps {
  constant DEFAULT_CACHE_TIMEOUT_MS (line 20) | const DEFAULT_CACHE_TIMEOUT_MS = 30_000
  constant DEFAULT_LOCAL_URL_PREFIXES (line 21) | const DEFAULT_LOCAL_URL_PREFIXES = ['file://', 'data:', 'blob:']
  function RemoteImage (line 37) | function RemoteImage({

FILE: packages/ui/src/components/ui/scroll-area.tsx
  function ScrollArea (line 5) | function ScrollArea({
  function ScrollBar (line 28) | function ScrollBar({

FILE: packages/ui/src/components/ui/separator.tsx
  function Separator (line 5) | function Separator({

FILE: packages/ui/src/components/ui/sheet.tsx
  function Sheet (line 6) | function Sheet({ ...props }: React.ComponentProps<typeof SheetPrimitive....
  function SheetTrigger (line 10) | function SheetTrigger({ ...props }: React.ComponentProps<typeof SheetPri...
  function SheetClose (line 14) | function SheetClose({ ...props }: React.ComponentProps<typeof SheetPrimi...
  function SheetPortal (line 18) | function SheetPortal({ ...props }: React.ComponentProps<typeof SheetPrim...
  function SheetOverlay (line 22) | function SheetOverlay({
  function SheetContent (line 38) | function SheetContent({
  function SheetHeader (line 75) | function SheetHeader({ className, ...props }: React.ComponentProps<'div'...
  function SheetFooter (line 85) | function SheetFooter({ className, ...props }: React.ComponentProps<'div'...
  function SheetTitle (line 95) | function SheetTitle({ className, ...props }: React.ComponentProps<typeof...
  function SheetDescription (line 105) | function SheetDescription({

FILE: packages/ui/src/components/ui/sidebar.tsx
  type Page (line 7) | type Page = 'home' | 'subscriptions' | 'settings' | 'about'
  type NavigationTarget (line 8) | type NavigationTarget = Page | 'supported-sites'
  type SidebarIcon (line 10) | interface SidebarIcon {
  type SidebarLabels (line 15) | interface SidebarLabels {
  type SidebarIcons (line 23) | interface SidebarIcons {
  type SidebarProps (line 31) | interface SidebarProps {
  type NavigationItem (line 44) | interface NavigationItem {
  type PageNavigationItem (line 51) | interface PageNavigationItem {
  function Sidebar (line 84) | function Sidebar({

FILE: packages/ui/src/components/ui/sonner.tsx
  type ToasterProps (line 4) | type ToasterProps = React.ComponentProps<typeof Sonner>

FILE: packages/ui/src/components/ui/table.tsx
  function Table (line 4) | function Table({ className, ...props }: React.ComponentProps<'table'>) {
  function TableHeader (line 16) | function TableHeader({ className, ...props }: React.ComponentProps<'thea...
  function TableBody (line 20) | function TableBody({ className, ...props }: React.ComponentProps<'tbody'...
  function TableFooter (line 30) | function TableFooter({ className, ...props }: React.ComponentProps<'tfoo...
  function TableRow (line 40) | function TableRow({ className, ...props }: React.ComponentProps<'tr'>) {
  function TableHead (line 53) | function TableHead({ className, ...props }: React.ComponentProps<'th'>) {
  function TableCell (line 66) | function TableCell({ className, ...props }: React.ComponentProps<'td'>) {
  function TableCaption (line 79) | function TableCaption({ className, ...props }: React.ComponentProps<'cap...

FILE: packages/ui/src/components/ui/tabs.tsx
  function Tabs (line 7) | function Tabs({ className, ...props }: React.ComponentProps<typeof TabsP...
  function TabsList (line 17) | function TabsList({ className, ...props }: React.ComponentProps<typeof T...
  function TabsTrigger (line 30) | function TabsTrigger({ className, ...props }: React.ComponentProps<typeo...
  function TabsContent (line 43) | function TabsContent({ className, ...props }: React.ComponentProps<typeo...

FILE: packages/ui/src/components/ui/textarea.tsx
  function Textarea (line 4) | function Textarea({ className, ...props }: React.ComponentProps<'textare...

FILE: packages/ui/src/components/ui/title-bar.tsx
  type TitleBarProps (line 6) | interface TitleBarProps {
  function TitleBar (line 21) | function TitleBar({

FILE: packages/ui/src/components/ui/tooltip.tsx
  function TooltipProvider (line 7) | function TooltipProvider({
  function Tooltip (line 20) | function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimit...
  function TooltipTrigger (line 28) | function TooltipTrigger({ ...props }: React.ComponentProps<typeof Toolti...
  function TooltipContent (line 32) | function TooltipContent({

FILE: packages/ui/src/lib/use-add-url-interaction.ts
  type UseAddUrlInteractionOptions (line 12) | interface UseAddUrlInteractionOptions {
  type UseAddUrlInteractionResult (line 23) | interface UseAddUrlInteractionResult {
Condensed preview — 366 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (1,807K chars).
[
  {
    "path": ".agents/skills/orpc-contract-first/SKILL.md",
    "chars": 1727,
    "preview": "---\nname: orpc-contract-first\ndescription: Guide for implementing oRPC contract-first API patterns in Dify frontend. Tri"
  },
  {
    "path": ".agents/skills/release-skills/SKILL.md",
    "chars": 13336,
    "preview": "---\nname: release-skills\ndescription: Universal release workflow. Auto-detects version files and changelogs. Supports No"
  },
  {
    "path": ".claude/CLAUDE.md",
    "chars": 5028,
    "preview": "# Ultracite Code Standards\n\nThis project uses **Ultracite**, a zero-config preset that enforces strict code quality stan"
  },
  {
    "path": ".cursor/hooks.json",
    "chars": 126,
    "preview": "{\n  \"version\": 1,\n  \"hooks\": {\n    \"afterFileEdit\": [\n      {\n        \"command\": \"pnpm dlx ultracite fix\"\n      }\n    ]\n"
  },
  {
    "path": ".dockerignore",
    "chars": 83,
    "preview": "node_modules\n.git\n.context\ndist\nout\nbuild\n**/node_modules\n**/dist\n**/out\n**/.turbo\n"
  },
  {
    "path": ".editorconfig",
    "chars": 148,
    "preview": "root = true\n\n[*]\ncharset = utf-8\nindent_style = space\nindent_size = 2\nend_of_line = lf\ninsert_final_newline = true\ntrim_"
  },
  {
    "path": ".gitattributes",
    "chars": 90,
    "preview": "* text=auto eol=lf\n*.{cmd,[cC][mM][dD]} text eol=crlf\n*.{bat,[bB][aA][tT]} text eol=crlf\n\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report.yml",
    "chars": 930,
    "preview": "name: Bug Report\ndescription: Report a problem or regression\ntitle: \"[Bug]: \"\nlabels:\n  - bug\nbody:\n  - type: textarea\n "
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "chars": 1008,
    "preview": "name: Feature Request\ndescription: Suggest an idea or improvement\ntitle: \"[Feature]: \"\nlabels:\n  - enhancement\nbody:\n  -"
  },
  {
    "path": ".github/workflows/build.yml",
    "chars": 5783,
    "preview": "name: Build\n\non:\n  workflow_call:\n    inputs:\n      upload_artifacts:\n        required: false\n        type: boolean\n    "
  },
  {
    "path": ".github/workflows/ci.yml",
    "chars": 148,
    "preview": "name: CI\n\non:\n  pull_request:\n    branches: [ main ]\n\njobs:\n  build:\n    uses: ./.github/workflows/build.yml\n    with:\n "
  },
  {
    "path": ".github/workflows/docker-publish.yml",
    "chars": 1872,
    "preview": "name: Docker Publish\n\non:\n  push:\n    branches:\n      - main\n    paths:\n      - 'apps/api/**'\n      - 'apps/desktop/reso"
  },
  {
    "path": ".github/workflows/extension-build.yml",
    "chars": 1146,
    "preview": "name: Build Extension\n\non:\n  workflow_call:\n    inputs:\n      upload_artifacts:\n        required: false\n        type: bo"
  },
  {
    "path": ".github/workflows/extension-publish.yml",
    "chars": 2949,
    "preview": "name: Publish Extension\n\non:\n  push:\n    branches: [ main ]\n    paths:\n      - apps/extension/package.json\n\njobs:\n  dete"
  },
  {
    "path": ".github/workflows/release.yml",
    "chars": 1593,
    "preview": "name: Build and Release Electron App\n\non:\n  push:\n    tags:\n      - v*.*.*\n  workflow_dispatch:\n\njobs:\n  build:\n    uses"
  },
  {
    "path": ".github/workflows/translator.yaml",
    "chars": 630,
    "preview": "name: 'translator'\non:\n  issues:\n    types: [opened, edited]\n  issue_comment:\n    types: [created, edited]\n  discussion:"
  },
  {
    "path": ".github/workflows/ytdlp-auto-release.yml",
    "chars": 2773,
    "preview": "name: Auto Release yt-dlp Patch\n\non:\n  schedule:\n    - cron: '17 */6 * * *'\n  workflow_dispatch:\n\npermissions:\n  content"
  },
  {
    "path": ".gitignore",
    "chars": 126,
    "preview": "node_modules\n/dist\napps/desktop/dist\napps/web/dist\napps/api/.data/\nout\n.conductor/\n.wxt\n.output\n.DS_Store\n.eslintcache\n*"
  },
  {
    "path": ".husky/pre-commit",
    "chars": 1960,
    "preview": "#!/bin/sh\n# Exit on any error\nset -e\n\n# Check if there are any staged files\nif [ -z \"$(git diff --cached --name-only)\" ]"
  },
  {
    "path": ".npmrc",
    "chars": 169,
    "preview": "electron_mirror=https://npmmirror.com/mirrors/electron/\nelectron_builder_binaries_mirror=https://npmmirror.com/mirrors/e"
  },
  {
    "path": ".vscode/extensions.json",
    "chars": 72,
    "preview": "{\n  \"recommendations\": [\"biomejs.biome\", \"bradlc.vscode-tailwindcss\"]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "chars": 1569,
    "preview": "{\n  \"editor.formatOnSave\": true,\n  \"editor.defaultFormatter\": \"esbenp.prettier-vscode\",\n  \"editor.codeActionsOnSave\": {\n"
  },
  {
    "path": "AGENTS.md",
    "chars": 5950,
    "preview": "1. use pnpm instead of npm\n2. use pnpm run check after tasks to check code\n3. Support i18n. When writing business logic,"
  },
  {
    "path": "CONTRIBUTING.md",
    "chars": 3815,
    "preview": "# Contributing to VidBee\n\nThank you for taking the time to improve VidBee. These notes keep the project maintainable and"
  },
  {
    "path": "LICENSE",
    "chars": 1063,
    "preview": "MIT License\n\nCopyright (c) 2025 VidBee\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof "
  },
  {
    "path": "README.md",
    "chars": 6370,
    "preview": "<div align=\"left\">\n  <a href=\"https://github.com/nexmoe/VidBee\">\n    <img src=\"apps/desktop/build/icon.png\" alt=\"Logo\" w"
  },
  {
    "path": "apps/api/Dockerfile",
    "chars": 895,
    "preview": "FROM node:22-alpine\n\nENV PNPM_HOME=/pnpm\nENV PATH=${PNPM_HOME}:${PATH}\nENV YTDLP_PATH=/usr/bin/yt-dlp\nENV FFMPEG_PATH=/u"
  },
  {
    "path": "apps/api/package.json",
    "chars": 752,
    "preview": "{\n  \"name\": \"api\",\n  \"private\": true,\n  \"type\": \"module\",\n  \"scripts\": {\n    \"dev\": \"tsx watch src/index.ts\",\n    \"start"
  },
  {
    "path": "apps/api/src/index.ts",
    "chars": 741,
    "preview": "import { createApiServer } from './server'\n\nconst host = process.env.VIDBEE_API_HOST?.trim() || '0.0.0.0'\nconst portValu"
  },
  {
    "path": "apps/api/src/lib/database-migrate.ts",
    "chars": 567,
    "preview": "import { existsSync } from 'node:fs'\nimport path from 'node:path'\nimport type { BetterSQLite3Database } from 'drizzle-or"
  },
  {
    "path": "apps/api/src/lib/downloader.ts",
    "chars": 1310,
    "preview": "import path from 'node:path'\nimport type { DownloadTask } from '@vidbee/downloader-core'\nimport { DownloaderCore } from "
  },
  {
    "path": "apps/api/src/lib/history-record-mapper.ts",
    "chars": 3785,
    "preview": "import type { DownloadHistoryInsert, DownloadHistoryRow } from '@vidbee/db/history'\nimport type { DownloadTask } from '@"
  },
  {
    "path": "apps/api/src/lib/history-store.ts",
    "chars": 2234,
    "preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport { downloadHistoryTable } from '@vidbee/db/history'\nimport t"
  },
  {
    "path": "apps/api/src/lib/rpc-router.ts",
    "chars": 14834,
    "preview": "import { spawn } from 'node:child_process'\nimport { randomUUID } from 'node:crypto'\nimport { constants as fsConstants } "
  },
  {
    "path": "apps/api/src/lib/sse.ts",
    "chars": 1412,
    "preview": "import type { ServerResponse } from 'node:http'\n\nconst HEARTBEAT_INTERVAL_MS = 15_000\n\nexport class SseHub {\n  private r"
  },
  {
    "path": "apps/api/src/lib/web-settings-store.ts",
    "chars": 1917,
    "preview": "import { mkdir, readFile, writeFile } from 'node:fs/promises'\nimport path from 'node:path'\nimport { WebAppSettingsSchema"
  },
  {
    "path": "apps/api/src/server.ts",
    "chars": 8333,
    "preview": "import { lookup } from 'node:dns/promises'\nimport type { ServerResponse } from 'node:http'\nimport net from 'node:net'\nim"
  },
  {
    "path": "apps/api/tsconfig.json",
    "chars": 229,
    "preview": "{\n  \"compilerOptions\": {\n    \"target\": \"ES2022\",\n    \"module\": \"ESNext\",\n    \"moduleResolution\": \"Bundler\",\n    \"strict\""
  },
  {
    "path": "apps/desktop/build/after-pack.cjs",
    "chars": 1710,
    "preview": "const { execFileSync } = require('node:child_process')\nconst fs = require('node:fs')\nconst path = require('node:path')\n\n"
  },
  {
    "path": "apps/desktop/build/entitlements.mac.plist",
    "chars": 491,
    "preview": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/P"
  },
  {
    "path": "apps/desktop/changelogs/CHANGELOG.fr.md",
    "chars": 14007,
    "preview": "# Journal des modifications de VidBee\n\nCette page ne présente que les évolutions visibles par les utilisateurs, sans dét"
  },
  {
    "path": "apps/desktop/changelogs/CHANGELOG.md",
    "chars": 13229,
    "preview": "# VidBee Changelog\n\nThis page only includes user-visible updates and avoids implementation details.\nFor full release not"
  },
  {
    "path": "apps/desktop/changelogs/CHANGELOG.ru.md",
    "chars": 12409,
    "preview": "# Журнал изменений VidBee\n\nНа этой странице указаны только заметные для пользователей изменения, без технических деталей"
  },
  {
    "path": "apps/desktop/changelogs/CHANGELOG.zh.md",
    "chars": 7406,
    "preview": "# VidBee 更新日志\n\n本页只记录你能直接感知到的更新,不展开技术实现细节。\n完整发布记录请查看 [GitHub Releases](https://github.com/nexmoe/VidBee/releases)。\n\n## [v"
  },
  {
    "path": "apps/desktop/components.json",
    "chars": 487,
    "preview": "{\n  \"$schema\": \"https://ui.shadcn.com/schema.json\",\n  \"style\": \"new-york\",\n  \"rsc\": false,\n  \"tsx\": true,\n  \"tailwind\": "
  },
  {
    "path": "apps/desktop/dev-app-update.yml",
    "chars": 117,
    "preview": "provider: generic\nurl: https://github.com/nexmoe/vidbee/releases/latest/download\nupdaterCacheDirName: vidbee-updater\n"
  },
  {
    "path": "apps/desktop/drizzle.config.ts",
    "chars": 174,
    "preview": "import { defineConfig } from 'drizzle-kit'\n\nexport default defineConfig({\n  schema: './src/main/lib/database/schema.ts',"
  },
  {
    "path": "apps/desktop/electron-builder.yml",
    "chars": 1451,
    "preview": "appId: com.vidbee\nproductName: VidBee\ndirectories:\n  buildResources: build\nafterPack: build/after-pack.cjs\nprotocols:\n  "
  },
  {
    "path": "apps/desktop/electron.vite.config.ts",
    "chars": 1132,
    "preview": "import { resolve } from 'node:path'\nimport tailwindcss from '@tailwindcss/vite'\nimport react from '@vitejs/plugin-react'"
  },
  {
    "path": "apps/desktop/package.json",
    "chars": 4165,
    "preview": "{\n  \"name\": \"vidbee\",\n  \"version\": \"1.3.5\",\n  \"description\": \"A modern Electron application for downloading videos and a"
  },
  {
    "path": "apps/desktop/release-metadata.json",
    "chars": 35,
    "preview": "{\n  \"ytDlpVersion\": \"2026.03.17\"\n}\n"
  },
  {
    "path": "apps/desktop/resources/.gitignore",
    "chars": 214,
    "preview": "# Ignore yt-dlp binaries (too large for git)\nyt-dlp.exe\nyt-dlp_macos\nyt-dlp_linux\nffmpeg.exe\nffmpeg_macos\nffmpeg_linux\nf"
  },
  {
    "path": "apps/desktop/resources/README.md",
    "chars": 3491,
    "preview": "# Resources Directory\n\nThis directory contains bundled resources for the application.\n\n## yt-dlp Binaries\n\nTo bundle yt-"
  },
  {
    "path": "apps/desktop/resources/drizzle/0000_swift_aaron_stack.sql",
    "chars": 1746,
    "preview": "CREATE TABLE `download_history` (\n\t`id` text PRIMARY KEY NOT NULL,\n\t`url` text NOT NULL,\n\t`title` text NOT NULL,\n\t`thumb"
  },
  {
    "path": "apps/desktop/resources/drizzle/0001_smiling_agent_zero.sql",
    "chars": 57,
    "preview": "ALTER TABLE `download_history` ADD `yt_dlp_command` text;"
  },
  {
    "path": "apps/desktop/resources/drizzle/0002_smooth_impossible_man.sql",
    "chars": 53,
    "preview": "ALTER TABLE `download_history` ADD `yt_dlp_log` text;"
  },
  {
    "path": "apps/desktop/resources/drizzle/meta/0000_snapshot.json",
    "chars": 11694,
    "preview": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"91501763-7554-435d-91d8-382c52ee65e4\",\n  \"prevId\": \"00000000-0000-00"
  },
  {
    "path": "apps/desktop/resources/drizzle/meta/0001_snapshot.json",
    "chars": 11887,
    "preview": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"a2ffc60d-d3a6-4bbb-96d3-fbc79bca6d8b\",\n  \"prevId\": \"91501763-7554-43"
  },
  {
    "path": "apps/desktop/resources/drizzle/meta/0002_snapshot.json",
    "chars": 12072,
    "preview": "{\n  \"version\": \"6\",\n  \"dialect\": \"sqlite\",\n  \"id\": \"3a269324-eb4d-44c4-860c-80d90f4f3df1\",\n  \"prevId\": \"a2ffc60d-d3a6-4b"
  },
  {
    "path": "apps/desktop/resources/drizzle/meta/_journal.json",
    "chars": 503,
    "preview": "{\n  \"version\": \"7\",\n  \"dialect\": \"sqlite\",\n  \"entries\": [\n    {\n      \"idx\": 0,\n      \"version\": \"6\",\n      \"when\": 1763"
  },
  {
    "path": "apps/desktop/scripts/check-locales.js",
    "chars": 2605,
    "preview": "#!/usr/bin/env node\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\ncon"
  },
  {
    "path": "apps/desktop/scripts/check-ytdlp.js",
    "chars": 2567,
    "preview": "#!/usr/bin/env node\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\ncon"
  },
  {
    "path": "apps/desktop/scripts/ensure-native-deps.mjs",
    "chars": 1304,
    "preview": "#!/usr/bin/env node\n\nimport { execSync, spawnSync } from 'node:child_process'\nimport path from 'node:path'\n\nconst deskto"
  },
  {
    "path": "apps/desktop/scripts/postinstall.mjs",
    "chars": 714,
    "preview": "#!/usr/bin/env node\n\nimport { execSync } from 'node:child_process'\nimport path from 'node:path'\n\nconst desktopRoot = pat"
  },
  {
    "path": "apps/desktop/scripts/set-console-encoding.js",
    "chars": 671,
    "preview": "/**\n * 设置控制台编码为 UTF-8,解决中文乱码问题\n * 这个脚本在 Windows 上设置控制台代码页为 UTF-8\n */\n\nconst { exec } = require('node:child_process')\ncon"
  },
  {
    "path": "apps/desktop/scripts/setup-dev-binaries.js",
    "chars": 32288,
    "preview": "#!/usr/bin/env node\n\n/**\n * Development environment setup script\n * Automatically downloads yt-dlp and ffmpeg binaries b"
  },
  {
    "path": "apps/desktop/scripts/ytdlp-auto-release.mjs",
    "chars": 7035,
    "preview": "#!/usr/bin/env node\n\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport { fileURLToPath } from 'node:url'\n\ncon"
  },
  {
    "path": "apps/desktop/src/main/assets.d.ts",
    "chars": 600,
    "preview": "/// <reference types=\"electron-vite/node\" />\n\ndeclare module '*.png?asset' {\n  const value: string\n  export default valu"
  },
  {
    "path": "apps/desktop/src/main/config/logger-config.ts",
    "chars": 1145,
    "preview": "import log from 'electron-log/main'\n\n/**\n * Configure electron-log\n * Set log format, file path, transport methods, etc."
  },
  {
    "path": "apps/desktop/src/main/download-engine/args-builder.ts",
    "chars": 1931,
    "preview": "import {\n  buildDownloadArgs as buildSharedDownloadArgs,\n  resolveAudioFormatSelector as resolveSharedAudioFormatSelecto"
  },
  {
    "path": "apps/desktop/src/main/download-engine/format-utils.ts",
    "chars": 4773,
    "preview": "import type {\n  AppSettings,\n  DownloadOptions,\n  OneClickQualityPreset,\n  VideoFormat\n} from '../../shared/types'\n\ncons"
  },
  {
    "path": "apps/desktop/src/main/index.ts",
    "chars": 18570,
    "preview": "import { existsSync } from 'node:fs'\nimport { isAbsolute, join, relative, resolve } from 'node:path'\nimport { electronAp"
  },
  {
    "path": "apps/desktop/src/main/ipc/index.ts",
    "chars": 1097,
    "preview": "import { createServices, type MergeIpcService } from 'electron-ipc-decorator'\nimport { AppService } from './services/app"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/app-service.ts",
    "chars": 2628,
    "preview": "import os from 'node:os'\nimport { app, BrowserWindow, dialog } from 'electron'\nimport { type IpcContext, IpcMethod, IpcS"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/browser-cookies-service.ts",
    "chars": 8240,
    "preview": "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport { type IpcContext, IpcMethod, IpcS"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/download-service.ts",
    "chars": 1818,
    "preview": "import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'\nimport type {\n  DownloadItem,\n  Download"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/file-system-service.ts",
    "chars": 10127,
    "preview": "import { execFile, execSync } from 'node:child_process'\nimport fs from 'node:fs/promises'\nimport os from 'node:os'\nimpor"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/history-service.ts",
    "chars": 1466,
    "preview": "import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'\nimport type { DownloadHistoryItem } from"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/settings-service.ts",
    "chars": 1757,
    "preview": "import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'\nimport type { AppSettings } from '../../"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/subscription-service.ts",
    "chars": 5321,
    "preview": "import path from 'node:path'\nimport { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'\nimport type"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/thumbnail-service.ts",
    "chars": 482,
    "preview": "import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'\nimport { thumbnailCache } from '../../li"
  },
  {
    "path": "apps/desktop/src/main/ipc/services/update-service.ts",
    "chars": 2450,
    "preview": "import { app } from 'electron'\nimport { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'\nimport { "
  },
  {
    "path": "apps/desktop/src/main/ipc/services/window-service.ts",
    "chars": 962,
    "preview": "import { BrowserWindow } from 'electron'\nimport { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'"
  },
  {
    "path": "apps/desktop/src/main/lib/command-utils.ts",
    "chars": 1499,
    "preview": "import {\n  appendYouTubeSafeExtractorArgs as appendSharedYouTubeSafeExtractorArgs,\n  buildVideoInfoArgs as buildSharedVi"
  },
  {
    "path": "apps/desktop/src/main/lib/database/migrate.ts",
    "chars": 1372,
    "preview": "import { existsSync } from 'node:fs'\nimport { join, resolve } from 'node:path'\nimport type { BetterSQLite3Database } fro"
  },
  {
    "path": "apps/desktop/src/main/lib/database/schema.ts",
    "chars": 2456,
    "preview": "import {\n  type DownloadHistoryInsert,\n  type DownloadHistoryRow,\n  downloadHistoryTable\n} from '@vidbee/db/history'\nimp"
  },
  {
    "path": "apps/desktop/src/main/lib/database-path.ts",
    "chars": 170,
    "preview": "import { join } from 'node:path'\nimport { app } from 'electron'\n\nexport const getDatabaseFilePath = (): string => {\n  re"
  },
  {
    "path": "apps/desktop/src/main/lib/database.ts",
    "chars": 1221,
    "preview": "import type { Database as BetterSqlite3Instance } from 'better-sqlite3'\nimport DatabaseConstructor from 'better-sqlite3'"
  },
  {
    "path": "apps/desktop/src/main/lib/download-engine.ts",
    "chars": 50651,
    "preview": "import { EventEmitter } from 'node:events'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport type { YTDlpEven"
  },
  {
    "path": "apps/desktop/src/main/lib/download-queue.ts",
    "chars": 4421,
    "preview": "import { EventEmitter } from 'node:events'\nimport type { DownloadItem, DownloadOptions } from '../../shared/types'\n\ninte"
  },
  {
    "path": "apps/desktop/src/main/lib/download-session-store.ts",
    "chars": 1805,
    "preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport { app } from 'electron'\nimport type { DownloadItem, Downloa"
  },
  {
    "path": "apps/desktop/src/main/lib/ffmpeg-manager.ts",
    "chars": 4113,
    "preview": "import { execSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:p"
  },
  {
    "path": "apps/desktop/src/main/lib/history-manager.ts",
    "chars": 10408,
    "preview": "import { eq, inArray } from 'drizzle-orm'\nimport type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3'\nimport"
  },
  {
    "path": "apps/desktop/src/main/lib/path-resolver.ts",
    "chars": 3088,
    "preview": "import fs from 'node:fs'\nimport path from 'node:path'\nimport type { PlaylistInfo, VideoInfo } from '../../shared/types'\n"
  },
  {
    "path": "apps/desktop/src/main/lib/progress-utils.ts",
    "chars": 1252,
    "preview": "import type { DownloadOptions, VideoFormat } from '../../shared/types'\n\nexport const clampPercent = (value?: number): nu"
  },
  {
    "path": "apps/desktop/src/main/lib/subscription-manager.ts",
    "chars": 12445,
    "preview": "import { randomUUID } from 'node:crypto'\nimport { EventEmitter } from 'node:events'\nimport fs from 'node:fs'\nimport { an"
  },
  {
    "path": "apps/desktop/src/main/lib/subscription-scheduler.ts",
    "chars": 16603,
    "preview": "import { EventEmitter } from 'node:events'\nimport fs from 'node:fs'\nimport log from 'electron-log/main'\nimport Parser fr"
  },
  {
    "path": "apps/desktop/src/main/lib/thumbnail-cache.ts",
    "chars": 4268,
    "preview": "import crypto from 'node:crypto'\nimport fs from 'node:fs'\nimport fsPromises from 'node:fs/promises'\nimport path from 'no"
  },
  {
    "path": "apps/desktop/src/main/lib/watermark-utils.ts",
    "chars": 9499,
    "preview": "import { spawn } from 'node:child_process'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path"
  },
  {
    "path": "apps/desktop/src/main/lib/youtube-extractor-args.ts",
    "chars": 700,
    "preview": "const YOUTUBE_HOST_SUFFIXES = ['youtube.com', 'youtu.be', 'youtube-nocookie.com'] as const\nconst YOUTUBE_SAFE_PLAYER_CLI"
  },
  {
    "path": "apps/desktop/src/main/lib/ytdlp-manager.ts",
    "chars": 6047,
    "preview": "import { execSync } from 'node:child_process'\nimport fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:p"
  },
  {
    "path": "apps/desktop/src/main/local-api.ts",
    "chars": 5281,
    "preview": "import crypto from 'node:crypto'\nimport http from 'node:http'\nimport type { AddressInfo } from 'node:net'\nimport log fro"
  },
  {
    "path": "apps/desktop/src/main/settings.ts",
    "chars": 2693,
    "preview": "import fs from 'node:fs'\nimport os from 'node:os'\nimport path from 'node:path'\nimport type { AppSettings } from '../shar"
  },
  {
    "path": "apps/desktop/src/main/tray.ts",
    "chars": 4295,
    "preview": "import { type LanguageCode, normalizeLanguageCode } from '@vidbee/i18n/languages'\nimport { app, BrowserWindow, Menu, nat"
  },
  {
    "path": "apps/desktop/src/main/utils/auto-launch.ts",
    "chars": 973,
    "preview": "import { app } from 'electron'\nimport log from 'electron-log/main'\n\nconst SUPPORTED_PLATFORMS = new Set(['darwin', 'win3"
  },
  {
    "path": "apps/desktop/src/main/utils/dock.ts",
    "chars": 300,
    "preview": "import { app } from 'electron'\n\n/**\n * Apply Dock visibility preference on macOS.\n */\nexport function applyDockVisibilit"
  },
  {
    "path": "apps/desktop/src/main/utils/logger.ts",
    "chars": 574,
    "preview": "/**\n * Main process logger utility\n * Directly use electron-log/main\n */\n\nimport log from 'electron-log/main'\n\n// Export"
  },
  {
    "path": "apps/desktop/src/main/utils/path-helpers.ts",
    "chars": 413,
    "preview": "import os from 'node:os'\nimport path from 'node:path'\n\nexport const resolvePathWithHome = (rawPath?: string | null): str"
  },
  {
    "path": "apps/desktop/src/preload/index.d.ts",
    "chars": 456,
    "preview": "import type { ElectronAPI } from '@electron-toolkit/preload'\nimport type { IpcServices } from '../main/ipc'\n\ndeclare glo"
  },
  {
    "path": "apps/desktop/src/preload/index.ts",
    "chars": 1516,
    "preview": "import { electronAPI } from '@electron-toolkit/preload'\nimport { contextBridge, ipcRenderer } from 'electron'\nimport { c"
  },
  {
    "path": "apps/desktop/src/renderer/index.html",
    "chars": 528,
    "preview": "<!doctype html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"UTF-8\">\n    <title>VidBee</title>\n    <meta\n      http-equi"
  },
  {
    "path": "apps/desktop/src/renderer/src/App.tsx",
    "chars": 9693,
    "preview": "import { Sidebar } from '@renderer/components/ui/sidebar'\nimport { Toaster } from '@renderer/components/ui/sonner'\nimpor"
  },
  {
    "path": "apps/desktop/src/renderer/src/assets/global.css",
    "chars": 112,
    "preview": "@import \"tailwindcss\";\n@import \"@vidbee/ui/theme.css\";\n@import \"tw-animate-css\";\n@import \"@vidbee/ui/base.css\";\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/assets/main.css",
    "chars": 477,
    "preview": "body {\n  margin: 0;\n  padding: 0;\n  font-family:\n    -apple-system, BlinkMacSystemFont, \"Segoe UI\", \"Roboto\", \"Oxygen\", "
  },
  {
    "path": "apps/desktop/src/renderer/src/assets/theme.css",
    "chars": 6364,
    "preview": ":root {\n  --background: oklch(1 0 0);\n  --foreground: oklch(0.1884 0.0128 248.5103);\n  --card: oklch(0.9881 0 0);\n  --ca"
  },
  {
    "path": "apps/desktop/src/renderer/src/assets/title-bar.css",
    "chars": 168,
    "preview": "/* Title bar drag region styles */\n.drag-region {\n  -webkit-app-region: drag;\n  app-region: drag;\n}\n\n.no-drag {\n  -webki"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/download/DownloadDialog.tsx",
    "chars": 29826,
    "preview": "import { AddUrlPopover } from '@renderer/components/ui/add-url-popover'\nimport { Button } from '@renderer/components/ui/"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/download/DownloadItem.tsx",
    "chars": 44703,
    "preview": "import { Badge } from '@renderer/components/ui/badge'\nimport { Button } from '@renderer/components/ui/button'\nimport { C"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/download/PlaylistDownload.tsx",
    "chars": 9986,
    "preview": "import { Checkbox } from '@renderer/components/ui/checkbox'\nimport { Input } from '@renderer/components/ui/input'\nimport"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/download/PlaylistDownloadGroup.tsx",
    "chars": 5865,
    "preview": "import { ChevronDown, ChevronRight, Trash2 } from 'lucide-react'\nimport { useEffect, useState } from 'react'\nimport { us"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/download/SingleVideoDownload.tsx",
    "chars": 28343,
    "preview": "import { Button } from '@renderer/components/ui/button'\nimport { ImageWithPlaceholder } from '@renderer/components/ui/im"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/download/UnifiedDownloadHistory.tsx",
    "chars": 20593,
    "preview": "import { Button } from '@renderer/components/ui/button'\nimport { CardContent, CardHeader } from '@renderer/components/ui"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/error/ErrorBoundary.tsx",
    "chars": 3850,
    "preview": "import { ipcServices } from '@renderer/lib/ipc'\nimport { logger } from '@renderer/lib/logger'\nimport { Component, type E"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/error/ErrorPage.tsx",
    "chars": 6433,
    "preview": "import { Button } from '@renderer/components/ui/button'\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/feedback/FeedbackLinks.tsx",
    "chars": 1322,
    "preview": "import { ipcServices } from '@renderer/lib/ipc'\nimport { useEffect, useState } from 'react'\n\ninterface AppInfo {\n  appVe"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/playlist/PlaylistPreviewCard.tsx",
    "chars": 3697,
    "preview": "import { Button } from '@renderer/components/ui/button'\nimport {\n  Card,\n  CardContent,\n  CardDescription,\n  CardHeader,"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/subscription/SubscriptionFormDialog.tsx",
    "chars": 11878,
    "preview": "import { Button } from '@renderer/components/ui/button'\nimport { Checkbox } from '@renderer/components/ui/checkbox'\nimpo"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/accordion.tsx",
    "chars": 195,
    "preview": "import {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger\n} from '@vidbee/ui/components/ui/accordion"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/add-url-popover.tsx",
    "chars": 99,
    "preview": "import { AddUrlPopover } from '@vidbee/ui/components/ui/add-url-popover'\n\nexport { AddUrlPopover }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/badge.tsx",
    "chars": 103,
    "preview": "import { Badge, badgeVariants } from '@vidbee/ui/components/ui/badge'\n\nexport { Badge, badgeVariants }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/button.tsx",
    "chars": 144,
    "preview": "import { Button, type ButtonProps, buttonVariants } from '@vidbee/ui/components/ui/button'\n\nexport { Button, type Button"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/card.tsx",
    "chars": 212,
    "preview": "import {\n  Card,\n  CardContent,\n  CardDescription,\n  CardFooter,\n  CardHeader,\n  CardTitle\n} from '@vidbee/ui/components"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/checkbox.tsx",
    "chars": 82,
    "preview": "import { Checkbox } from '@vidbee/ui/components/ui/checkbox'\n\nexport { Checkbox }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/context-menu.tsx",
    "chars": 726,
    "preview": "import {\n  ContextMenu,\n  ContextMenuCheckboxItem,\n  ContextMenuContent,\n  ContextMenuGroup,\n  ContextMenuItem,\n  Contex"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/dialog.tsx",
    "chars": 380,
    "preview": "import {\n  Dialog,\n  DialogClose,\n  DialogContent,\n  DialogDescription,\n  DialogFooter,\n  DialogHeader,\n  DialogOverlay,"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/download-dialog-layout.tsx",
    "chars": 120,
    "preview": "import { DownloadDialogLayout } from '@vidbee/ui/components/ui/download-dialog-layout'\n\nexport { DownloadDialogLayout }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/dropdown-menu.tsx",
    "chars": 757,
    "preview": "import {\n  DropdownMenu,\n  DropdownMenuCheckboxItem,\n  DropdownMenuContent,\n  DropdownMenuGroup,\n  DropdownMenuItem,\n  D"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/hover-card.tsx",
    "chars": 158,
    "preview": "import { HoverCard, HoverCardContent, HoverCardTrigger } from '@vidbee/ui/components/ui/hover-card'\n\nexport { HoverCard,"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/image-with-placeholder.tsx",
    "chars": 120,
    "preview": "import { ImageWithPlaceholder } from '@vidbee/ui/components/ui/image-with-placeholder'\n\nexport { ImageWithPlaceholder }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/input.tsx",
    "chars": 73,
    "preview": "import { Input } from '@vidbee/ui/components/ui/input'\n\nexport { Input }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/item.tsx",
    "chars": 340,
    "preview": "import {\n  Item,\n  ItemActions,\n  ItemContent,\n  ItemDescription,\n  ItemFooter,\n  ItemGroup,\n  ItemHeader,\n  ItemMedia,\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/label.tsx",
    "chars": 73,
    "preview": "import { Label } from '@vidbee/ui/components/ui/label'\n\nexport { Label }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/popover.tsx",
    "chars": 181,
    "preview": "import {\n  Popover,\n  PopoverAnchor,\n  PopoverContent,\n  PopoverTrigger\n} from '@vidbee/ui/components/ui/popover'\n\nexpor"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/progress.tsx",
    "chars": 82,
    "preview": "import { Progress } from '@vidbee/ui/components/ui/progress'\n\nexport { Progress }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/radio-group.tsx",
    "chars": 121,
    "preview": "import { RadioGroup, RadioGroupItem } from '@vidbee/ui/components/ui/radio-group'\n\nexport { RadioGroup, RadioGroupItem }"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/remote-image.tsx",
    "chars": 916,
    "preview": "import { APP_PROTOCOL_SCHEME } from '@shared/constants'\nimport {\n  RemoteImage as SharedRemoteImage,\n  type RemoteImageP"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/scroll-area.tsx",
    "chars": 111,
    "preview": "import { ScrollArea, ScrollBar } from '@vidbee/ui/components/ui/scroll-area'\n\nexport { ScrollArea, ScrollBar }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/select.tsx",
    "chars": 404,
    "preview": "import {\n  Select,\n  SelectContent,\n  SelectGroup,\n  SelectItem,\n  SelectLabel,\n  SelectScrollDownButton,\n  SelectScroll"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/separator.tsx",
    "chars": 85,
    "preview": "import { Separator } from '@vidbee/ui/components/ui/separator'\n\nexport { Separator }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/sheet.tsx",
    "chars": 297,
    "preview": "import {\n  Sheet,\n  SheetClose,\n  SheetContent,\n  SheetDescription,\n  SheetFooter,\n  SheetHeader,\n  SheetTitle,\n  SheetT"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/sidebar.tsx",
    "chars": 1966,
    "preview": "import { AppSidebar, type AppSidebarItem } from '@vidbee/ui/components/ui/app-sidebar'\nimport { appSidebarIcons } from '"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/sonner.tsx",
    "chars": 78,
    "preview": "import { Toaster } from '@vidbee/ui/components/ui/sonner'\n\nexport { Toaster }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/switch.tsx",
    "chars": 76,
    "preview": "import { Switch } from '@vidbee/ui/components/ui/switch'\n\nexport { Switch }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/table.tsx",
    "chars": 255,
    "preview": "import {\n  Table,\n  TableBody,\n  TableCaption,\n  TableCell,\n  TableFooter,\n  TableHead,\n  TableHeader,\n  TableRow\n} from"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/tabs.tsx",
    "chars": 142,
    "preview": "import { Tabs, TabsContent, TabsList, TabsTrigger } from '@vidbee/ui/components/ui/tabs'\n\nexport { Tabs, TabsContent, Ta"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/textarea.tsx",
    "chars": 82,
    "preview": "import { Textarea } from '@vidbee/ui/components/ui/textarea'\n\nexport { Textarea }\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/title-bar.tsx",
    "chars": 1617,
    "preview": "import { TitleBar as SharedTitleBar } from '@vidbee/ui/components/ui/title-bar'\nimport { useEffect, useState } from 'rea"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/ui/tooltip.tsx",
    "chars": 185,
    "preview": "import {\n  Tooltip,\n  TooltipContent,\n  TooltipProvider,\n  TooltipTrigger\n} from '@vidbee/ui/components/ui/tooltip'\n\nexp"
  },
  {
    "path": "apps/desktop/src/renderer/src/components/video/AdvancedOptions.tsx",
    "chars": 2942,
    "preview": "import {\n  Accordion,\n  AccordionContent,\n  AccordionItem,\n  AccordionTrigger\n} from '@renderer/components/ui/accordion'"
  },
  {
    "path": "apps/desktop/src/renderer/src/data/popularSites.ts",
    "chars": 1498,
    "preview": "export interface PopularSite {\n  id: string\n  url: string\n  domain: string\n}\n\nexport const popularSites: PopularSite[] ="
  },
  {
    "path": "apps/desktop/src/renderer/src/env.d.ts",
    "chars": 39,
    "preview": "/// <reference types=\"vite/client\" />\n\n"
  },
  {
    "path": "apps/desktop/src/renderer/src/hooks/use-cached-thumbnail.ts",
    "chars": 1116,
    "preview": "import { APP_PROTOCOL_SCHEME } from '@shared/constants'\nimport { useEffect, useState } from 'react'\nimport { ipcServices"
  },
  {
    "path": "apps/desktop/src/renderer/src/hooks/use-download-events.ts",
    "chars": 6269,
    "preview": "import type { DownloadItem } from '@shared/types'\nimport { useSetAtom, useStore } from 'jotai'\nimport { useCallback, use"
  },
  {
    "path": "apps/desktop/src/renderer/src/hooks/use-history-sync.ts",
    "chars": 1210,
    "preview": "import { useSetAtom } from 'jotai'\nimport { useEffect } from 'react'\n// import type { DownloadHistoryItem } from '../../"
  },
  {
    "path": "apps/desktop/src/renderer/src/hooks/use-ipc-example.ts",
    "chars": 3312,
    "preview": "import { ipcServices } from '@renderer/lib/ipc'\nimport { useState } from 'react'\nimport { toast } from 'sonner'\n\n/**\n * "
  },
  {
    "path": "apps/desktop/src/renderer/src/i18n.ts",
    "chars": 121,
    "preview": "import { initSharedI18n } from '@vidbee/i18n'\nimport i18n from 'i18next'\n\nvoid initSharedI18n(i18n)\n\nexport default i18n"
  },
  {
    "path": "apps/desktop/src/renderer/src/lib/ipc.ts",
    "chars": 1563,
    "preview": "/**\n * IPC Services for Renderer Process\n *\n * This file provides a convenient way to access IPC services in the rendere"
  },
  {
    "path": "apps/desktop/src/renderer/src/lib/logger.ts",
    "chars": 470,
    "preview": "/**\n * Renderer process logger utility\n * Use electron-log/renderer which automatically forwards logs to main process\n *"
  },
  {
    "path": "apps/desktop/src/renderer/src/lib/utils.ts",
    "chars": 166,
    "preview": "import { type ClassValue, clsx } from 'clsx'\nimport { twMerge } from 'tailwind-merge'\n\nexport function cn(...inputs: Cla"
  },
  {
    "path": "apps/desktop/src/renderer/src/main.tsx",
    "chars": 2679,
    "preview": "import './assets/main.css'\nimport './assets/global.css'\nimport 'flag-icons/css/flag-icons.min.css'\n\nimport { StrictMode "
  },
  {
    "path": "apps/desktop/src/renderer/src/pages/About.tsx",
    "chars": 19004,
    "preview": "import { useAppInfo } from '@renderer/components/feedback/FeedbackLinks'\nimport { Badge } from '@renderer/components/ui/"
  },
  {
    "path": "apps/desktop/src/renderer/src/pages/Home.tsx",
    "chars": 507,
    "preview": "import { UnifiedDownloadHistory } from '../components/download/UnifiedDownloadHistory'\n\ninterface HomeProps {\n  onOpenSu"
  },
  {
    "path": "apps/desktop/src/renderer/src/pages/Settings.tsx",
    "chars": 38792,
    "preview": "import { Button } from '@renderer/components/ui/button'\nimport { Input } from '@renderer/components/ui/input'\nimport {\n "
  },
  {
    "path": "apps/desktop/src/renderer/src/pages/Subscriptions.tsx",
    "chars": 26109,
    "preview": "import {\n  type SubscriptionFormData,\n  SubscriptionFormDialog\n} from '@renderer/components/subscription/SubscriptionFor"
  },
  {
    "path": "apps/desktop/src/renderer/src/store/downloads.ts",
    "chars": 6903,
    "preview": "import { atom } from 'jotai'\nimport type { DownloadHistoryItem, DownloadItem } from '../../../shared/types'\n\nexport type"
  },
  {
    "path": "apps/desktop/src/renderer/src/store/settings.ts",
    "chars": 1923,
    "preview": "import { normalizeLanguageCode } from '@vidbee/i18n/languages'\nimport { atom } from 'jotai'\nimport type { AppSettings } "
  },
  {
    "path": "apps/desktop/src/renderer/src/store/subscriptions.ts",
    "chars": 2245,
    "preview": "import type {\n  SubscriptionResolvedFeed,\n  SubscriptionRule,\n  SubscriptionUpdatePayload\n} from '@shared/types'\nimport "
  },
  {
    "path": "apps/desktop/src/renderer/src/store/update.ts",
    "chars": 379,
    "preview": "import { atom } from 'jotai'\n\ninterface UpdateReadyState {\n  ready: boolean\n  version?: string\n}\n\ninterface UpdateAvaila"
  },
  {
    "path": "apps/desktop/src/renderer/src/store/video.ts",
    "chars": 1461,
    "preview": "import { atom } from 'jotai'\nimport type { VideoInfo } from '../../../shared/types'\nimport { ipcServices } from '../lib/"
  },
  {
    "path": "apps/desktop/src/shared/constants.ts",
    "chars": 93,
    "preview": "export const APP_PROTOCOL = 'vidbee'\nexport const APP_PROTOCOL_SCHEME = `${APP_PROTOCOL}://`\n"
  },
  {
    "path": "apps/desktop/src/shared/types/index.ts",
    "chars": 6932,
    "preview": "import { defaultLanguageCode, type LanguageCode } from '@vidbee/i18n/languages'\n\n// Download related types\nexport interf"
  },
  {
    "path": "apps/desktop/src/shared/types/ipc.ts",
    "chars": 102,
    "preview": "// Re-export the IpcServices type from main process\nexport type { IpcServices } from '../../main/ipc'\n"
  },
  {
    "path": "apps/desktop/src/shared/utils/download-file.ts",
    "chars": 1947,
    "preview": "export const normalizeSavedFileName = (fileName?: string): string | undefined => {\n  if (!fileName) {\n    return undefin"
  },
  {
    "path": "apps/desktop/src/shared/utils/format-preferences.ts",
    "chars": 556,
    "preview": "import {\n  buildAudioFormatPreference as buildSharedAudioFormatPreference,\n  buildVideoFormatPreference as buildSharedVi"
  },
  {
    "path": "apps/desktop/tsconfig.json",
    "chars": 370,
    "preview": "{\n  \"files\": [],\n  \"references\": [\n    { \"path\": \"./tsconfig.node.json\" },\n    { \"path\": \"./tsconfig.web.json\" }\n  ],\n  "
  },
  {
    "path": "apps/desktop/tsconfig.node.json",
    "chars": 481,
    "preview": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.node.json\",\n  \"include\": [\n    \"electron.vite.config.*\",\n    \"src/ma"
  },
  {
    "path": "apps/desktop/tsconfig.web.json",
    "chars": 591,
    "preview": "{\n  \"extends\": \"@electron-toolkit/tsconfig/tsconfig.web.json\",\n  \"include\": [\n    \"src/renderer/src/env.d.ts\",\n    \"src/"
  },
  {
    "path": "apps/docs/.gitignore",
    "chars": 239,
    "preview": "# deps\n/node_modules\n\n# generated content\n.source\n\n# test & build\n/coverage\n/.next/\n/out/\n/build\n*.tsbuildinfo\n\n# misc\n."
  },
  {
    "path": "apps/docs/README.md",
    "chars": 1517,
    "preview": "# docs\n\nThis is a Next.js application generated with\n[Create Fumadocs](https://github.com/fuma-nama/fumadocs).\n\nRun deve"
  },
  {
    "path": "apps/docs/biome.config.json",
    "chars": 682,
    "preview": "{\n  \"$schema\": \"https://biomejs.dev/schemas/2.2.0/schema.json\",\n  \"vcs\": {\n    \"enabled\": true,\n    \"clientKind\": \"git\","
  },
  {
    "path": "apps/docs/content/cookies.mdx",
    "chars": 4173,
    "preview": "---\ntitle: Cookies\ndescription: Signed-in downloads, restricted content, and cookie setup\n---\n\nCookies reuse your browse"
  },
  {
    "path": "apps/docs/content/faq.mdx",
    "chars": 1184,
    "preview": "---\ntitle: FAQ\ndescription: Common questions and troubleshooting for VidBee\n---\n\n## What if a download fails or shows an"
  },
  {
    "path": "apps/docs/content/fr/cookies.mdx",
    "chars": 5018,
    "preview": "---\ntitle: Cookies\ndescription: Téléchargements avec connexion, contenu restreint et configuration des cookies\n---\n\nLes "
  },
  {
    "path": "apps/docs/content/fr/faq.mdx",
    "chars": 1406,
    "preview": "---\ntitle: FAQ\ndescription: Questions courantes et dépannage pour VidBee\n---\n\n## Que faire si un téléchargement échoue o"
  },
  {
    "path": "apps/docs/content/fr/index.mdx",
    "chars": 942,
    "preview": "---\ntitle: Introduction\ndescription: Documentation et FAQ du téléchargeur VidBee\n---\n\nVidBee est un téléchargeur de bure"
  },
  {
    "path": "apps/docs/content/fr/meta.json",
    "chars": 203,
    "preview": "{\n  \"title\": \"Documentation VidBee\",\n  \"pages\": [\n    \"index\",\n    \"---Téléchargement & Accès---\",\n    \"protocol\",\n    \""
  },
  {
    "path": "apps/docs/content/fr/protocol.mdx",
    "chars": 4472,
    "preview": "---\ntitle: Protocole vidbee://\ndescription: Téléchargement rapide via le protocole d’URL vidbee://\n---\n\nVidBee enregistr"
  },
  {
    "path": "apps/docs/content/fr/rss.mdx",
    "chars": 2743,
    "preview": "---\ntitle: RSS\ndescription: S’abonner aux flux RSS et ajouter automatiquement les téléchargements\n---\n\nVidBee peut surve"
  },
  {
    "path": "apps/docs/content/index.mdx",
    "chars": 813,
    "preview": "---\ntitle: Introduction\ndescription: VidBee desktop downloader documentation and FAQ\n---\n\nVidBee is a desktop downloader"
  },
  {
    "path": "apps/docs/content/meta.json",
    "chars": 185,
    "preview": "{\n  \"title\": \"VidBee Docs\",\n  \"pages\": [\n    \"index\",\n    \"---Download & Access---\",\n    \"protocol\",\n    \"cookies\",\n    "
  }
]

// ... and 166 more files (download for full content)

About this extraction

This page contains the full source code of the nexmoe/VidBee GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 366 files (1.6 MB), approximately 439.2k tokens, and a symbol index with 730 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!