Compare commits

..

6 Commits

Author SHA1 Message Date
Nexmoe
f646f287e6 chore: release v1.2.1 2026-01-20 22:48:17 +08:00
Nexmoe
603a4f051d feat(download): add log capture and file actions (#157)
* feat(download): add log capture and file actions

* fix(ci): update ffmpeg download source

* fix(ci): consolidate ffmpeg download source to yt-dlp repository

Update FFmpeg download URLs to use yt-dlp/FFmpeg-Builds as the primary source across all platforms and remove BtbN fallback repository reference.
2026-01-20 22:47:18 +08:00
Nexmoe
e2bfa334c0 docs(protocol): add vidbee protocol docs (#150) 2026-01-18 14:13:37 +08:00
Nexmoe
e8769fefb0 fix(download): disambiguate duplicate playlist titles (#145) 2026-01-18 13:34:39 +08:00
Nexmoe
b88411d229 fix(subscription): update rsshub links (#146)
* chore(docs): update rsshub documentation URLs to https://docs.rsshub.app/routes/

Update rsshub-related documentation URLs to use the official documentation base URL.

Changes:
- Update bilibili feed URL generation in subscription-service.ts
- Update RSSHub documentation link in SubscriptionFormDialog.tsx to point to docs.rsshub.app/routes/

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* chore(i18n): remove placeholder URL from i18n, use hardcoded URL instead

Remove the placeholders.url entry from all locale files and hardcode the
example URL directly in the SubscriptionFormDialog component. The placeholder
URL is a technical example and doesn't need internationalization.

Changes:
- Hardcode placeholder URL in SubscriptionFormDialog: "https://docs.rsshub.app/routes/youtube/user/@FKJ"
- Remove placeholders.url entry from all 14 locale files

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* fix(subscription): update bilibili rsshub url

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-18 13:34:32 +08:00
Nexmoe
b6c67b23f6 feat(docs): generate sitemap for docs site (#148)
* docs: add VidBee Docs suffix to all documentation page titles

Update the page title metadata generation to append "| VidBee Docs" to all documentation page titles for consistent branding across the site.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* docs: add canonical URLs to documentation pages

Add canonical URL metadata to all documentation pages to prevent search engine duplication issues and improve SEO rankings.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat(docs): add automatic sitemap

* fix(docs): align canonical base URL

* fix(docs): configure sitemap for static export

Add 'export const dynamic = "force-static"' to sitemap.ts to enable
static generation when using Next.js output: export mode. This fixes
the build error: "export const dynamic = 'force-static'/export const
revalidate not configured on route '/sitemap.xml'".

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

* chore(docs): add favicon and icon assets

Add favicons and app icons in various sizes for better browser
and platform support.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
2026-01-18 13:26:34 +08:00
55 changed files with 1522 additions and 424 deletions

View File

@@ -8,10 +8,6 @@ on:
types: [created, edited]
discussion_comment:
types: [created, edited]
pull_request_target:
types: [opened, edited]
pull_request_review_comment:
types: [created, edited]
jobs:
translate:

View File

@@ -17,11 +17,13 @@ Cookies reuse your browser's signed-in session so VidBee can download content th
In **Settings**, select your browser. VidBee will try to detect the browser profile path automatically. You can also enter the profile path manually.
![Browser cookies settings](/browser-cookies.png)
**Supported browsers (platform dependent):**
- **Windows: Firefox only. Other browsers cannot be used for cookie reading.**
- macOS: Safari, Chrome, Edge, Brave, Firefox, and more.
- Linux: Chrome, Chromium, Brave, Firefox, and more.
- macOS: All browsers supported.
- Linux: All browsers supported.
**Steps:**
@@ -36,6 +38,8 @@ On Windows, if you are not using Firefox, switch to the cookies file method.
You can import a **Netscape-formatted** cookies file. This is useful when reading the browser profile is not possible.
![Cookies file settings](/cookies-file.png)
**Steps:**
1. Export a Netscape cookies file using a browser extension.

View File

@@ -1,5 +1,5 @@
---
title: VidBee Docs
title: Introduction
description: VidBee desktop downloader documentation and FAQ
---
@@ -9,6 +9,7 @@ These docs focus on real-world usage and settings, especially signed-in download
## Start here
- [vidbee:// Protocol](./protocol.mdx): Quick download using URL protocol.
- [Cookies](./cookies.mdx): Configure signed-in sessions and restricted content.
- [FAQ](./faq.mdx): Common questions and troubleshooting.

View File

@@ -1,4 +1,4 @@
{
"title": "VidBee Docs",
"pages": ["index", "cookies", "faq"]
"pages": ["index", "protocol", "cookies", "faq"]
}

149
docs/content/protocol.mdx Normal file
View File

@@ -0,0 +1,149 @@
---
title: vidbee:// Protocol
description: Quick download using vidbee:// URL protocol
---
VidBee registers a custom URL protocol (`vidbee://`) that allows you to trigger downloads directly from web browsers, browser extensions, or userscripts.
## Basic Usage
The `vidbee://` protocol can be used to open VidBee and automatically start downloading videos.
### Protocol Format
```
vidbee://download?url=<encoded-video-url>
```
**Parameters:**
- `url` (required): The video URL to download, must be URL-encoded
### Example
To download a YouTube video:
```html
<a href="vidbee://download?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ">
Download with VidBee
</a>
```
Or in JavaScript:
```javascript
const videoUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
window.location.href = vidbeeUrl
```
## Opening VidBee
To simply open the VidBee app without starting a download:
```
vidbee://
```
## Use Cases
### Browser Extension
The VidBee browser extension uses this protocol to send the current tab's URL to the desktop app:
```javascript
const currentUrl = window.location.href
const deepLink = `vidbee://download?url=${encodeURIComponent(currentUrl)}`
window.location.href = deepLink
```
### Userscript Integration
The VidBee userscript adds quick download buttons to supported video sites:
```javascript
// Single click triggers download via protocol
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
window.location.href = vidbeeUrl
```
### Web Pages
You can add direct download links to your web pages:
```html
<!-- Simple link -->
<a href="vidbee://download?url=https%3A%2F%2Fexample.com%2Fvideo">
Download with VidBee
</a>
<!-- Button with JavaScript -->
<button onclick="openInVidBee('https://example.com/video')">
Quick Download
</button>
<script>
function openInVidBee(url) {
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(url)}`
window.location.href = vidbeeUrl
}
</script>
```
## Playlist Support
To download an entire playlist:
```
vidbee://download?url=<encoded-playlist-url>&type=playlist
```
**Parameters:**
- `url` (required): The playlist URL, must be URL-encoded
- `type`: Set to `playlist` to download all videos in the playlist
### Example
```javascript
const playlistUrl = 'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf'
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(playlistUrl)}&type=playlist`
window.location.href = vidbeeUrl
```
## How It Works
1. **Protocol Registration**: VidBee registers as the handler for the `vidbee://` protocol during installation
2. **URL Parsing**: When a `vidbee://download?url=...` link is clicked, the OS launches VidBee
3. **Queue Processing**: VidBee extracts the video URL and adds it to the download queue
4. **Auto-start**: The download begins automatically if the app is configured for auto-download
## Browser Compatibility
The `vidbee://` protocol works across all major browsers:
- Chrome/Edge/Brave
- Firefox
- Safari
## Security Notes
- Only URLs starting with `vidbee://` will trigger the app
- The app validates the URL format before processing
- Malformed URLs are ignored with a warning in the logs
## Troubleshooting
### Protocol Not Working
If clicking `vidbee://` links doesn't open VidBee:
1. **Check installation**: Ensure VidBee is properly installed
2. **Reinstall**: Try reinstalling VidBee to re-register the protocol
3. **OS permissions**: On macOS, check System Settings > Privacy & Security for any blocks
4. **Browser settings**: Some browsers may require you to allow the protocol on first use
### App Opens But Doesn't Download
If VidBee opens but the download doesn't start:
1. **Check URL encoding**: Ensure the video URL is properly encoded with `encodeURIComponent()`
2. **Check logs**: Open the app and check the developer console for errors
3. **Supported sites**: Verify the URL is from a [supported site](https://vidbee.org/supported-sites/)

View File

@@ -17,11 +17,13 @@ Cookie 用于复用浏览器的登录态,帮助 VidBee 下载需要登录或
在 **设置** 中选择你的浏览器VidBee 会尝试自动识别浏览器配置文件路径。你也可以手动填写配置文件路径。
![浏览器 Cookie 设置](/browser-cookies.png)
**支持的浏览器(按平台差异显示):**
- **Windows仅支持 Firefox。其他浏览器无法读取 Cookie。**
- macOSSafari、Chrome、Edge、Brave、Firefox 等
- LinuxChrome、Chromium、Brave、Firefox 等
- macOS全部支持
- Linux全部支持
**使用步骤:**
@@ -36,6 +38,8 @@ Cookie 用于复用浏览器的登录态,帮助 VidBee 下载需要登录或
你也可以导入 **Netscape 格式** 的 cookies 文件。这个方式适合在不方便读取浏览器配置文件时使用。
![Cookies 文件设置](/cookies-file.png)
**使用步骤:**
1. 使用浏览器扩展导出 Netscape cookies 文件。

View File

@@ -1,5 +1,5 @@
---
title: VidBee 文档
title: 简介
description: VidBee 桌面下载器使用说明与常见问题
---
@@ -9,6 +9,7 @@ VidBee 是一款基于 Electron 的桌面下载器,内置 yt-dlp 引擎,提
## 从这里开始
- [vidbee:// 协议](./protocol.mdx):使用 URL 协议快速下载。
- [Cookie 使用](./cookies.mdx):登录态与限制内容的下载配置。
- [常见问题](./faq.mdx):常见问题与排查思路。

View File

@@ -1,4 +1,4 @@
{
"title": "VidBee 文档",
"pages": ["index", "cookies", "faq"]
"pages": ["index", "protocol", "cookies", "faq"]
}

View File

@@ -0,0 +1,149 @@
---
title: vidbee:// 协议
description: 使用 vidbee:// URL 协议快速下载
---
VidBee 注册了自定义 URL 协议(`vidbee://`),允许您直接从网页浏览器、浏览器扩展或用户脚本触发下载。
## 基本用法
`vidbee://` 协议可用于打开 VidBee 并自动开始下载视频。
### 协议格式
```
vidbee://download?url=<编码后的视频URL>
```
**参数:**
- `url`(必需):要下载的视频 URL必须经过 URL 编码
### 示例
下载 YouTube 视频:
```html
<a href="vidbee://download?url=https%3A%2F%2Fwww.youtube.com%2Fwatch%3Fv%3DdQw4w9WgXcQ">
使用 VidBee 下载
</a>
```
或使用 JavaScript
```javascript
const videoUrl = 'https://www.youtube.com/watch?v=dQw4w9WgXcQ'
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
window.location.href = vidbeeUrl
```
## 打开 VidBee
仅打开 VidBee 应用而不开始下载:
```
vidbee://
```
## 使用场景
### 浏览器扩展
VidBee 浏览器扩展使用此协议将当前标签页的 URL 发送到桌面应用:
```javascript
const currentUrl = window.location.href
const deepLink = `vidbee://download?url=${encodeURIComponent(currentUrl)}`
window.location.href = deepLink
```
### 用户脚本集成
VidBee 用户脚本在支持的视频网站上添加快速下载按钮:
```javascript
// 单击通过协议触发下载
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(videoUrl)}`
window.location.href = vidbeeUrl
```
### 网页集成
您可以在网页中添加直接下载链接:
```html
<!-- 简单链接 -->
<a href="vidbee://download?url=https%3A%2F%2Fexample.com%2Fvideo">
使用 VidBee 下载
</a>
<!-- 带 JavaScript 的按钮 -->
<button onclick="openInVidBee('https://example.com/video')">
快速下载
</button>
<script>
function openInVidBee(url) {
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(url)}`
window.location.href = vidbeeUrl
}
</script>
```
## 播放列表支持
下载整个播放列表:
```
vidbee://download?url=<编码后的播放列表URL>&type=playlist
```
**参数:**
- `url`(必需):播放列表 URL必须经过 URL 编码
- `type`:设置为 `playlist` 以下载播放列表中的所有视频
### 示例
```javascript
const playlistUrl = 'https://www.youtube.com/playlist?list=PLrAXtmErZgOeiKm4sgNOknGvNjby9efdf'
const vidbeeUrl = `vidbee://download?url=${encodeURIComponent(playlistUrl)}&type=playlist`
window.location.href = vidbeeUrl
```
## 工作原理
1. **协议注册**VidBee 在安装过程中注册为 `vidbee://` 协议的处理程序
2. **URL 解析**:当点击 `vidbee://download?url=...` 链接时,操作系统会启动 VidBee
3. **队列处理**VidBee 提取视频 URL 并将其添加到下载队列
4. **自动开始**:如果应用配置为自动下载,下载会自动开始
## 浏览器兼容性
`vidbee://` 协议适用于所有主流浏览器:
- Chrome/Edge/Brave
- Firefox
- Safari
## 安全说明
- 仅以 `vidbee://` 开头的 URL 会触发应用
- 应用在处理前会验证 URL 格式
- 格式错误的 URL 将被忽略,并在日志中显示警告
## 故障排除
### 协议不工作
如果点击 `vidbee://` 链接无法打开 VidBee
1. **检查安装**:确保 VidBee 已正确安装
2. **重新安装**:尝试重新安装 VidBee 以重新注册协议
3. **操作系统权限**:在 macOS 上,检查系统设置 > 隐私与安全性 是否有任何阻止
4. **浏览器设置**:某些浏览器可能需要您在首次使用时允许该协议
### 应用打开但未下载
如果 VidBee 打开但下载未开始:
1. **检查 URL 编码**:确保视频 URL 使用 `encodeURIComponent()` 正确编码
2. **检查日志**:打开应用并检查开发者控制台是否有错误
3. **支持的网站**:验证 URL 是否来自[支持的网站](https://vidbee.org/supported-sites/)

View File

@@ -4,7 +4,8 @@ const withMDX = createMDX();
/** @type {import('next').NextConfig} */
const config = {
output: 'export',
// Only use static export for production builds, not dev mode
output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
reactStrictMode: true,
// Use trailing slashes to avoid conflicts with route handlers that have file extensions
trailingSlash: true,

101
docs/public/ICONS.md Normal file
View File

@@ -0,0 +1,101 @@
# VidBee Documentation Icons
This directory contains the VidBee logo in various sizes for use in the documentation site.
## Source
All icons are generated from the original VidBee application icon located at:
`/Users/air15/Documents/GitHub/VidBee/build/icon.png`
## Available Sizes
| Filename | Size | Use Case |
|----------|------|----------|
| `icon.png` | 512×512 | Default/Original icon |
| `icon-16.png` | 16×16 | Browser favicon (small) |
| `icon-32.png` | 32×32 | Browser favicon (standard) |
| `icon-48.png` | 48×48 | Browser favicon (large) |
| `icon-64.png` | 64×64 | Small UI elements |
| `icon-128.png` | 128×128 | Medium UI elements |
| `icon-192.png` | 192×192 | PWA icon (Android) |
| `icon-256.png` | 256×256 | Large UI elements |
| `icon-512.png` | 512×512 | PWA splash screen, high-res displays |
| `apple-touch-icon.png` | 180×180 | iOS/macOS home screen icon |
| `favicon.png` | 32×32 | Standard favicon |
## Usage in Next.js
### In `app/layout.tsx` or `app/favicon.ico`:
```tsx
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'VidBee Documentation',
description: 'Official VidBee documentation',
icons: {
icon: [
{ url: '/icon-16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icon-32.png', sizes: '32x32', type: 'image/png' },
{ url: '/icon-48.png', sizes: '48x48', type: 'image/png' },
],
apple: [
{ url: '/apple-touch-icon.png', sizes: '180x180', type: 'image/png' },
],
},
}
```
### For PWA (Progressive Web App):
Add to your `manifest.json` or `site.webmanifest`:
```json
{
"icons": [
{
"src": "/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}
```
## Regeneration
If you need to regenerate these icons from the source:
```bash
cd docs/public
SOURCE="/Users/air15/Documents/GitHub/VidBee/build/icon.png"
# Copy original
cp $SOURCE icon-original.png
# Generate sizes
sips -z 16 16 icon-original.png --out icon-16.png
sips -z 32 32 icon-original.png --out icon-32.png
sips -z 48 48 icon-original.png --out icon-48.png
sips -z 64 64 icon-original.png --out icon-64.png
sips -z 128 128 icon-original.png --out icon-128.png
sips -z 192 192 icon-original.png --out icon-192.png
sips -z 256 256 icon-original.png --out icon-256.png
sips -z 180 180 icon-original.png --out apple-touch-icon.png
# Create standard copies
cp icon-original.png icon-512.png
cp icon-original.png icon.png
cp icon-32.png favicon.png
```
## Notes
- All icons maintain the VidBee bee/honeycomb theme
- Icons use PNG format with transparency (RGBA)
- Generated using macOS `sips` tool for quality consistency

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 MiB

BIN
docs/public/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
docs/public/icon-128.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 KiB

BIN
docs/public/icon-16.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 924 B

BIN
docs/public/icon-192.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

BIN
docs/public/icon-256.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/public/icon-32.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

BIN
docs/public/icon-48.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

BIN
docs/public/icon-512.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/public/icon-64.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

BIN
docs/public/icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -57,11 +57,17 @@ export async function generateMetadata(
const page = source.getPage(params.slug, params.lang);
if (!page) notFound();
const baseUrl = 'https://docs.vidbee.org';
const canonicalUrl = `${baseUrl}${page.url}`;
return {
title: page.data.title,
title: `${page.data.title} | VidBee Docs`,
description: page.data.description,
openGraph: {
images: getPageImage(page).url,
},
alternates: {
canonical: canonicalUrl,
},
};
}

View File

@@ -1,12 +1,25 @@
import './global.css';
import { Inter } from 'next/font/google';
import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { i18n, isLocale } from '@/lib/i18n';
const inter = Inter({
subsets: ['latin'],
});
export const metadata: Metadata = {
icons: {
icon: [
{ url: '/favicon.png', sizes: '32x32', type: 'image/png' },
{ url: '/icon-16.png', sizes: '16x16', type: 'image/png' },
{ url: '/icon-32.png', sizes: '32x32', type: 'image/png' },
{ url: '/icon-192.png', sizes: '192x192', type: 'image/png' },
],
apple: '/apple-touch-icon.png',
},
};
export default async function Layout({
children,
params,

34
docs/src/app/sitemap.ts Normal file
View File

@@ -0,0 +1,34 @@
import type { MetadataRoute } from 'next';
import { i18n } from '@/lib/i18n';
import { source } from '@/lib/source';
export const dynamic = 'force-static';
const baseUrl = 'https://docs.vidbee.org';
function buildPath(segments: string[]): string {
if (segments.length === 0) {
return '/';
}
return `/${segments.join('/')}/`;
}
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
const params = source.generateParams();
return params.map(({ lang, slug }) => {
const segments: string[] = [];
if (lang && lang !== i18n.defaultLanguage) {
segments.push(lang);
}
if (slug && slug.length > 0) {
segments.push(...slug);
}
return {
url: `${baseUrl}${buildPath(segments)}`,
};
});
}

View File

@@ -1,6 +1,6 @@
{
"name": "vidbee",
"version": "1.2.0",
"version": "1.2.1",
"description": "A modern Electron application for downloading videos and audios",
"main": "./out/main/index.js",
"author": "VidBee",

View File

@@ -35,7 +35,7 @@ const PLATFORM_CONFIG = {
ffprobeOutput: 'ffprobe.exe',
extract: 'unzip',
release: {
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
repos: ['yt-dlp/FFmpeg-Builds', 'yt-dlp/FFmpeg-Builds'],
assetPattern: /ffmpeg-master-latest-win64-gpl\.zip$/i,
binaryName: 'ffmpeg.exe'
}
@@ -87,7 +87,7 @@ const PLATFORM_CONFIG = {
ffprobeOutput: 'ffprobe',
extract: 'tar',
release: {
repos: ['yt-dlp/FFmpeg-Builds', 'BtbN/FFmpeg-Builds'],
repos: ['yt-dlp/FFmpeg-Builds', 'yt-dlp/FFmpeg-Builds'],
assetPattern: /ffmpeg-master-latest-linux64-gpl\.tar\.xz$/i,
binaryName: 'ffmpeg'
}

View File

@@ -71,14 +71,7 @@ export const resolveAudioFormatSelector = (options: DownloadOptions): string =>
const isBilibiliUrl = (url: string): boolean => {
try {
const host = new URL(url).hostname.toLowerCase()
return (
host === 'bilibili.com' ||
host.endsWith('.bilibili.com') ||
host === 'b23.tv' ||
host.endsWith('.b23.tv') ||
host === 'bili.tv' ||
host.endsWith('.bili.tv')
)
return host.includes('bilibili.com') || host.includes('b23.tv') || host.includes('bili.tv')
} catch {
return false
}
@@ -98,7 +91,9 @@ export const buildDownloadArgs = (
// Format selection
if (options.type === 'video') {
const formatSelector = resolveVideoFormatSelector(options)
args.push('-f', formatSelector)
if (formatSelector) {
args.push('-f', formatSelector)
}
if (options.audioFormatIds && options.audioFormatIds.length > 0) {
args.push('--audio-multistreams')
} else if (formatSelector.includes('mergeall')) {

View File

@@ -253,6 +253,10 @@ function setupDownloadEvents(): void {
mainWindow?.webContents.send('download:progress', { id, progress })
})
downloadEngine.on('download-log', (id: string, logText: string) => {
mainWindow?.webContents.send('download:log', { id, log: logText })
})
downloadEngine.on('download-completed', (id: string) => {
mainWindow?.webContents.send('download:completed', id)
})

View File

@@ -106,6 +106,35 @@ class FileSystemService extends IpcService {
}
}
@IpcMethod()
async openFile(_context: IpcContext, filePath: string): Promise<boolean> {
try {
if (!filePath) {
return false
}
const sanitizedPath = this.sanitizePath(filePath)
const normalizedPath = path.normalize(sanitizedPath)
const stats = await fs.stat(normalizedPath).catch(() => null)
if (!stats || (!stats.isFile() && !stats.isDirectory())) {
scopedLoggers.system.error('File does not exist:', normalizedPath)
return false
}
const result = await shell.openPath(normalizedPath)
if (result) {
scopedLoggers.system.error('Failed to open file:', result)
return false
}
return true
} catch (error) {
scopedLoggers.system.error('Failed to open file:', error)
return false
}
}
@IpcMethod()
async copyFileToClipboard(_context: IpcContext, filePath: string): Promise<boolean> {
try {

View File

@@ -16,6 +16,7 @@ export const downloadHistoryTable = sqliteTable('download_history', {
sortKey: integer('sort_key', { mode: 'number' }).notNull(),
error: text('error'),
ytDlpCommand: text('yt_dlp_command'),
ytDlpLog: text('yt_dlp_log'),
description: text('description'),
channel: text('channel'),
uploader: text('uploader'),

View File

@@ -628,16 +628,34 @@ class DownloadEngine extends EventEmitter {
`Starting playlist download: ${selectionSize} items from "${playlistInfo.title}"`
)
const normalizedTitles = new Set<string>()
let hasDuplicateTitles = false
for (const entry of selectedEntries) {
const key = sanitizeTemplateValue(entry.title || '').toLowerCase()
if (normalizedTitles.has(key)) {
hasDuplicateTitles = true
break
}
normalizedTitles.add(key)
}
const indexWidth = hasDuplicateTitles
? String(Math.max(...selectedEntries.map((entry) => entry.index))).length
: 0
// Create download items for each video in the playlist
for (const entry of selectedEntries) {
const downloadId = `${groupId}_${Math.random().toString(36).substring(2, 10)}`
const customFilenameTemplate = hasDuplicateTitles
? `${String(entry.index).padStart(indexWidth, '0')} - %(title)s via VidBee.%(ext)s`
: undefined
const downloadOptions: DownloadOptions = {
url: entry.url,
type: options.type,
format: options.format,
audioFormat: options.type === 'audio' ? options.format : undefined,
customDownloadPath: resolvedDownloadPath
customDownloadPath: resolvedDownloadPath,
customFilenameTemplate
}
const createdAt = Date.now()
@@ -751,6 +769,46 @@ class DownloadEngine extends EventEmitter {
let totalParts = estimateProgressParts(options)
let completedParts = 0
let lastPercent = 0
let ytDlpLog = ''
let logFlushTimer: NodeJS.Timeout | null = null
let lastFlushedLog = ''
const normalizeLogChunk = (chunk: string): string =>
chunk.replace(/\r\n/g, '\n').replace(/\r/g, '\n')
const flushLogUpdate = (): void => {
if (logFlushTimer) {
clearTimeout(logFlushTimer)
logFlushTimer = null
}
if (ytDlpLog === lastFlushedLog) {
return
}
lastFlushedLog = ytDlpLog
this.updateDownloadInfo(id, { ytDlpLog })
this.emit('download-log', id, ytDlpLog)
}
const scheduleLogUpdate = (): void => {
if (logFlushTimer) {
return
}
logFlushTimer = setTimeout(() => {
flushLogUpdate()
}, 500)
}
const appendLogChunk = (chunk: string | Buffer): void => {
if (!chunk) {
return
}
const text = typeof chunk === 'string' ? chunk : chunk.toString()
if (!text) {
return
}
ytDlpLog += normalizeLogChunk(text)
scheduleLogUpdate()
}
// First, get detailed video info to capture basic metadata and formats
try {
@@ -925,6 +983,14 @@ class DownloadEngine extends EventEmitter {
this.activeDownloads.set(id, { controller, process: ytdlpProcess })
ytdlpProcess.ytDlpProcess?.stdout?.on('data', (data: Buffer) => {
appendLogChunk(data)
})
ytdlpProcess.ytDlpProcess?.stderr?.on('data', (data: Buffer) => {
appendLogChunk(data)
})
this.queue.updateItemInfo(id, { status: 'downloading', startedAt: Date.now() })
this.scheduleSessionPersist()
this.emit('download-started', id)
@@ -1024,6 +1090,7 @@ class DownloadEngine extends EventEmitter {
// Handle completion
ytdlpProcess.on('close', async (code: number | null) => {
flushLogUpdate()
this.activeDownloads.delete(id)
this.queue.downloadCompleted(id)
@@ -1154,6 +1221,7 @@ class DownloadEngine extends EventEmitter {
// Handle errors
ytdlpProcess.on('error', (error: Error) => {
flushLogUpdate()
scopedLoggers.download.error('Download process error for ID:', id, error)
this.activeDownloads.delete(id)
this.queue.downloadCompleted(id)
@@ -1329,6 +1397,9 @@ class DownloadEngine extends EventEmitter {
if (updates.ytDlpCommand !== undefined) {
historyUpdates.ytDlpCommand = updates.ytDlpCommand
}
if (updates.ytDlpLog !== undefined) {
historyUpdates.ytDlpLog = updates.ytDlpLog
}
if (updates.savedFileName !== undefined) {
historyUpdates.savedFileName = updates.savedFileName
}
@@ -1377,7 +1448,7 @@ class DownloadEngine extends EventEmitter {
): void {
// Get the download item from the queue to get additional info
const completedDownload = this.queue.getCompletedDownload(id)
scopedLoggers.download.info('Completed download:', completedDownload)
// scopedLoggers.download.info('Completed download:', completedDownload)
const completedAt = Date.now()
this.upsertHistoryEntry(id, options, {
@@ -1425,6 +1496,7 @@ class DownloadEngine extends EventEmitter {
completedAt: updates.completedAt,
error: updates.error,
ytDlpCommand: updates.ytDlpCommand,
ytDlpLog: updates.ytDlpLog,
description: updates.description,
channel: updates.channel,
uploader: updates.uploader,

View File

@@ -37,6 +37,7 @@ const createDownloadHistoryTableSql = sql`
sort_key INTEGER NOT NULL,
error TEXT,
yt_dlp_command TEXT,
yt_dlp_log TEXT,
description TEXT,
channel TEXT,
uploader TEXT,
@@ -219,20 +220,53 @@ class HistoryManager {
}
const deprecatedColumns = ['subscription_title', 'format', 'quality', 'codec']
const requiredColumns = ['yt_dlp_command']
const requiredColumns = ['yt_dlp_command', 'yt_dlp_log']
const hasDeprecated = columns.some((column) => deprecatedColumns.includes(column.name))
const missingRequired = requiredColumns.some(
const missingRequired = requiredColumns.filter(
(columnName) => !columns.some((column) => column.name === columnName)
)
const needsRebuild = hasDeprecated || missingRequired
if (needsRebuild) {
if (hasDeprecated) {
this.rebuildDownloadHistoryTable()
return
}
if (missingRequired.length > 0) {
this.addMissingColumns(missingRequired)
}
} catch (error) {
logger.error('history-db failed to inspect schema', error)
}
}
private addMissingColumns(columns: string[]): void {
if (columns.length === 0) {
return
}
const database = this.getDatabase()
const definitions: Record<string, string> = {
yt_dlp_command: 'TEXT',
yt_dlp_log: 'TEXT'
}
try {
database.transaction(
(tx) => {
for (const column of columns) {
const definition = definitions[column]
if (!definition) {
continue
}
tx.run(sql.raw(`ALTER TABLE download_history ADD COLUMN ${column} ${definition}`))
}
},
{ behavior: 'immediate' }
)
logger.info(`history-db added missing columns: ${columns.join(', ')}`)
} catch (error) {
logger.error('history-db failed to add missing columns', error)
}
}
private migrateLegacyPayloadTable(): void {
const database = this.getDatabase()
logger.info('history-db migrating legacy payload schema to structured columns')
@@ -394,6 +428,7 @@ class HistoryManager {
sortKey: item.completedAt ?? item.downloadedAt,
error: item.error ?? null,
ytDlpCommand: item.ytDlpCommand ?? null,
ytDlpLog: item.ytDlpLog ?? null,
description: item.description ?? null,
channel: item.channel ?? null,
uploader: item.uploader ?? null,
@@ -441,6 +476,7 @@ class HistoryManager {
completedAt: row.completedAt ?? 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,

File diff suppressed because it is too large Load Diff

View File

@@ -68,6 +68,12 @@ const getCodecShortName = (codec?: string): string => {
return codec.split('.')[0].toUpperCase()
}
const isHlsFormat = (format: VideoFormat): boolean =>
format.protocol === 'm3u8' || format.protocol === 'm3u8_native'
const isHttpProtocol = (format: VideoFormat): boolean =>
!!format.protocol && format.protocol.startsWith('http')
const filterFormatsByType = (
formats: VideoInfo['formats'],
activeTab: 'video' | 'audio'
@@ -270,6 +276,30 @@ const FormatList = ({ formats, type, codec, selectedFormat, onFormatChange }: Fo
return `${mb.toFixed(2)} MB`
}
const formatMetaLabel = (format: VideoFormat) => {
const parts: string[] = []
const pushPart = (label: string, value?: string) => {
if (!value) return
parts.push(`${label}:${value}`)
}
pushPart('proto', format.protocol)
pushPart('lang', format.language?.trim())
if (format.tbr) {
pushPart('tbr', `${Math.round(format.tbr)}k`)
}
if (typeof format.quality === 'number') {
pushPart('q', String(format.quality))
}
if (format.vcodec && format.vcodec !== 'none') {
pushPart('vcodec', format.vcodec)
}
if (format.acodec && format.acodec !== 'none') {
pushPart('acodec', format.acodec)
}
return parts.join(' • ')
}
const formatVideoQuality = (format: VideoFormat) => {
if (format.height) {
return `${format.height}p${format.fps === 60 ? '60' : ''}`
@@ -339,6 +369,7 @@ const FormatList = ({ formats, type, codec, selectedFormat, onFormatChange }: Fo
? format.acodec.split('.')[0].toUpperCase()
: ''
const sizeLabel = formatSize(format.filesize || format.filesize_approx)
const metaLabel = formatMetaLabel(format)
const isSelected = selectedFormat === format.format_id
return (
@@ -363,12 +394,19 @@ const FormatList = ({ formats, type, codec, selectedFormat, onFormatChange }: Fo
{qualityLabel}
</span>
<div className="flex-1 flex items-center gap-2 min-w-0">
<span className="text-xs text-muted-foreground truncate">{detailLabel}</span>
{thirdColumnLabel && thirdColumnLabel !== '-' && (
<span className="shrink-0 px-1.5 py-0.5 rounded bg-muted text-[10px] font-medium text-muted-foreground">
{thirdColumnLabel}
</span>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 min-w-0">
<span className="text-xs text-muted-foreground truncate">{detailLabel}</span>
{thirdColumnLabel && thirdColumnLabel !== '-' && (
<span className="shrink-0 px-1.5 py-0.5 rounded bg-muted text-[10px] font-medium text-muted-foreground">
{thirdColumnLabel}
</span>
)}
</div>
{metaLabel && (
<div className="mt-0.5 text-[10px] text-muted-foreground/70 leading-snug break-words">
{metaLabel}
</div>
)}
</div>
@@ -401,7 +439,16 @@ export function SingleVideoDownload({
const relevantFormats = useMemo(() => {
if (!videoInfo?.formats) return []
return filterFormatsByType(videoInfo.formats, activeTab)
const baseFormats = filterFormatsByType(videoInfo.formats, activeTab)
if (baseFormats.length === 0) return []
const hasHttpFormats = baseFormats.some(isHttpProtocol)
if (!hasHttpFormats) {
return baseFormats
}
const nonHlsFormats = baseFormats.filter((format) => !isHlsFormat(format))
return nonHlsFormats.length > 0 ? nonHlsFormats : baseFormats
}, [videoInfo?.formats, activeTab])
const containers = useMemo(() => {

View File

@@ -178,7 +178,7 @@ export function SubscriptionFormDialog({
const handleOpenRSSHubDocs = async () => {
try {
await ipcServices.fs.openExternal('https://docs.rsshub.app/routes/social-media#youtube')
await ipcServices.fs.openExternal('https://docs.rsshub.app/routes/')
} catch (error) {
console.error('Failed to open RSSHub documentation:', error)
toast.error(t('subscriptions.notifications.openLinkError'))
@@ -242,7 +242,7 @@ export function SubscriptionFormDialog({
<Input
id={urlInputId}
value={url}
placeholder={t('subscriptions.placeholders.url')}
placeholder="https://docs.rsshub.app/routes/youtube/user/@FKJ"
onChange={(event) => setUrl(event.target.value)}
/>
{detectingFeed && (

View File

@@ -96,6 +96,16 @@ export function useDownloadEvents() {
})
}
const handleLog = (rawData: unknown) => {
const data = rawData as { id?: string; log?: string }
const id = typeof data?.id === 'string' ? data.id : ''
if (!id) {
return
}
const logText = typeof data?.log === 'string' ? data.log : ''
updateDownload({ id, changes: { ytDlpLog: logText } })
}
const handleCompleted = (rawId: unknown) => {
const id = typeof rawId === 'string' ? rawId : ''
if (!id) {
@@ -129,13 +139,14 @@ export function useDownloadEvents() {
const startedSubscription = ipcEvents.on('download:started', handleStarted)
const progressSubscription = ipcEvents.on('download:progress', handleProgress)
const logSubscription = ipcEvents.on('download:log', handleLog)
const completedSubscription = ipcEvents.on('download:completed', handleCompleted)
const errorSubscription = ipcEvents.on('download:error', handleError)
const cancelledSubscription = ipcEvents.on('download:cancelled', handleCancelled)
return () => {
ipcEvents.removeListener('download:started', startedSubscription)
ipcEvents.removeListener('download:progress', progressSubscription)
ipcEvents.removeListener('download:log', logSubscription)
ipcEvents.removeListener('download:completed', completedSubscription)
ipcEvents.removeListener('download:error', errorSubscription)
ipcEvents.removeListener('download:cancelled', cancelledSubscription)

View File

@@ -119,6 +119,7 @@
"downloadPending": "قيد الانتظار",
"downloadQueue": "قائمة انتظار التحميل",
"customDownloadFolder": "مجلد تنزيل مخصص",
"retry": "إعادة المحاولة",
"autoFolderPlaceholder": "مجلد تلقائي (استنادًا إلى البيانات الوصفية)",
"autoFolderHint": "يتم إنشاء المجلدات التلقائية من البيانات الوصفية.",
"useAutoFolder": "استخدام المجلد التلقائي",
@@ -158,6 +159,16 @@
"progress": "التقدم",
"showDetails": "إظهار التفاصيل",
"hideDetails": "إخفاء التفاصيل",
"viewLogs": "عرض السجلات",
"detailsTab": "التفاصيل",
"logsTab": "السجلات",
"logs": {
"live": "سجلات مباشرة",
"history": "سجلات محفوظة",
"command": "أمر yt-dlp",
"empty": "لا توجد سجلات بعد.",
"scrollPaused": "تم إيقاف التمرير"
},
"selectAudioFormat": "اختر تنسيق الصوت",
"selectDownloadType": "اختر نوع التنزيل",
"selectFormat": "اختر التنسيق",
@@ -269,6 +280,8 @@
"openInBrowser": "انقر لفتح في المتصفح",
"removeAction": "إزالة",
"removeItem": "إزالة العنصر",
"deleteFile": "حذف الملف",
"deleteRecord": "إزالة من القائمة",
"select": "تحديد",
"selectAll": "تحديد الكل",
"selectVisible": "تحديد المرئي",
@@ -489,9 +502,6 @@
"disabled": "معطل",
"onlyLatestShort": "الأحدث فقط"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "إضافة",
"refresh": "تحديث",
@@ -660,4 +670,4 @@
"popularSection": "المنصات الرئيسية",
"viewAll": "عرض جميع المواقع المدعومة"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "Ausstehend",
"downloadQueue": "Download-Warteschlange",
"customDownloadFolder": "Benutzerdefinierter Download-Ordner",
"retry": "Download erneut versuchen",
"autoFolderPlaceholder": "Automatischer Ordner (basierend auf Metadaten)",
"autoFolderHint": "Automatische Ordner werden aus Metadaten erstellt.",
"useAutoFolder": "Automatischen Ordner verwenden",
@@ -158,6 +159,16 @@
"progress": "Fortschritt",
"showDetails": "Details anzeigen",
"hideDetails": "Details ausblenden",
"viewLogs": "Logs anzeigen",
"detailsTab": "Details",
"logsTab": "Logs",
"logs": {
"live": "Live-Logs",
"history": "Gespeicherte Logs",
"command": "yt-dlp-Befehl",
"empty": "Noch keine Logs.",
"scrollPaused": "Scrollen pausiert"
},
"selectAudioFormat": "Audio-Format auswählen",
"selectDownloadType": "Download-Typ auswählen",
"selectFormat": "Format auswählen",
@@ -269,6 +280,8 @@
"openInBrowser": "Klicken, um im Browser zu öffnen",
"removeAction": "Entfernen",
"removeItem": "Element entfernen",
"deleteFile": "Datei löschen",
"deleteRecord": "Aus Liste entfernen",
"select": "Auswählen",
"selectAll": "Alle auswählen",
"selectVisible": "Sichtbare auswählen",
@@ -489,9 +502,6 @@
"disabled": "Deaktiviert",
"onlyLatestShort": "Nur neueste"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Hinzufügen",
"refresh": "Aktualisieren",
@@ -660,4 +670,4 @@
"popularSection": "Hauptplattformen",
"viewAll": "Alle unterstützten Websites anzeigen"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "Pending",
"downloadQueue": "Download Queue",
"customDownloadFolder": "Custom download folder",
"retry": "Retry Download",
"autoFolderPlaceholder": "Automatic folder (based on metadata)",
"autoFolderHint": "Automatic folders are created from metadata.",
"useAutoFolder": "Use automatic folder",
@@ -158,6 +159,16 @@
"progress": "Progress",
"showDetails": "Show details",
"hideDetails": "Hide details",
"viewLogs": "View logs",
"detailsTab": "Details",
"logsTab": "Logs",
"logs": {
"live": "Live logs",
"history": "Saved logs",
"command": "yt-dlp command",
"empty": "No logs yet.",
"scrollPaused": "Scroll paused"
},
"selectAudioFormat": "Select Audio Format",
"selectDownloadType": "Select download type",
"selectFormat": "Select Format",
@@ -269,6 +280,8 @@
"openInBrowser": "Click to open in browser",
"removeAction": "Remove",
"removeItem": "Remove Item",
"deleteFile": "Delete File",
"deleteRecord": "Remove from List",
"select": "Select",
"selectAll": "Select All",
"selectVisible": "Select visible",
@@ -489,9 +502,6 @@
"disabled": "Disabled",
"onlyLatestShort": "Only latest"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Add",
"refresh": "Refresh",

View File

@@ -119,6 +119,7 @@
"downloadPending": "Pendiente",
"downloadQueue": "Cola de Descarga",
"customDownloadFolder": "Carpeta de descarga personalizada",
"retry": "Reintentar descarga",
"autoFolderPlaceholder": "Carpeta automática (basada en metadatos)",
"autoFolderHint": "Las carpetas automáticas se crean a partir de metadatos.",
"useAutoFolder": "Usar carpeta automática",
@@ -158,6 +159,16 @@
"progress": "Progreso",
"showDetails": "Mostrar detalles",
"hideDetails": "Ocultar detalles",
"viewLogs": "Ver registros",
"detailsTab": "Detalles",
"logsTab": "Registros",
"logs": {
"live": "Registros en vivo",
"history": "Registros guardados",
"command": "Comando de yt-dlp",
"empty": "Aún no hay registros.",
"scrollPaused": "Desplazamiento en pausa"
},
"selectAudioFormat": "Seleccionar Formato de Audio",
"selectDownloadType": "Seleccionar tipo de descarga",
"selectFormat": "Seleccionar Formato",
@@ -269,6 +280,8 @@
"openInBrowser": "Haz clic para abrir en el navegador",
"removeAction": "Eliminar",
"removeItem": "Eliminar Elemento",
"deleteFile": "Eliminar Archivo",
"deleteRecord": "Eliminar de la Lista",
"select": "Seleccionar",
"selectAll": "Seleccionar todo",
"selectVisible": "Seleccionar visibles",
@@ -489,9 +502,6 @@
"disabled": "Deshabilitado",
"onlyLatestShort": "Solo último"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Agregar",
"refresh": "Actualizar",
@@ -660,4 +670,4 @@
"popularSection": "Plataformas principales",
"viewAll": "Ver todos los sitios soportados"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "En attente",
"downloadQueue": "File de Téléchargement",
"customDownloadFolder": "Dossier de téléchargement personnalisé",
"retry": "Relancer le téléchargement",
"autoFolderPlaceholder": "Dossier automatique (basé sur les métadonnées)",
"autoFolderHint": "Les dossiers automatiques sont créés à partir des métadonnées.",
"useAutoFolder": "Utiliser le dossier automatique",
@@ -158,6 +159,16 @@
"progress": "Progrès",
"showDetails": "Afficher les détails",
"hideDetails": "Masquer les détails",
"viewLogs": "Voir les journaux",
"detailsTab": "Détails",
"logsTab": "Journaux",
"logs": {
"live": "Journaux en direct",
"history": "Journaux enregistrés",
"command": "Commande yt-dlp",
"empty": "Aucun journal pour le moment.",
"scrollPaused": "Défilement en pause"
},
"selectAudioFormat": "Sélectionner le Format Audio",
"selectDownloadType": "Sélectionner le type de téléchargement",
"selectFormat": "Sélectionner le Format",
@@ -269,6 +280,8 @@
"openInBrowser": "Cliquez pour ouvrir dans le navigateur",
"removeAction": "Supprimer",
"removeItem": "Supprimer l'Élément",
"deleteFile": "Supprimer le Fichier",
"deleteRecord": "Supprimer de la Liste",
"select": "Sélectionner",
"selectAll": "Tout sélectionner",
"selectVisible": "Sélectionner les visibles",
@@ -489,9 +502,6 @@
"disabled": "Désactivé",
"onlyLatestShort": "Seulement le dernier"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Ajouter",
"refresh": "Rafraîchir",
@@ -660,4 +670,4 @@
"popularSection": "Plateformes principales",
"viewAll": "Voir tous les sites supportés"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "Menunggu",
"downloadQueue": "Antrian Unduhan",
"customDownloadFolder": "Folder unduhan khusus",
"retry": "Coba ulang unduhan",
"autoFolderPlaceholder": "Folder otomatis (berdasarkan metadata)",
"autoFolderHint": "Folder otomatis dibuat dari metadata.",
"useAutoFolder": "Gunakan folder otomatis",
@@ -158,6 +159,16 @@
"progress": "Kemajuan",
"showDetails": "Tampilkan detail",
"hideDetails": "Sembunyikan detail",
"viewLogs": "Lihat log",
"detailsTab": "Detail",
"logsTab": "Log",
"logs": {
"live": "Log langsung",
"history": "Log tersimpan",
"command": "Perintah yt-dlp",
"empty": "Belum ada log.",
"scrollPaused": "Pengguliran dijeda"
},
"selectAudioFormat": "Pilih Format Audio",
"selectDownloadType": "Pilih jenis unduhan",
"selectFormat": "Pilih Format",
@@ -269,6 +280,8 @@
"openInBrowser": "Klik untuk membuka di browser",
"removeAction": "Hapus",
"removeItem": "Hapus Item",
"deleteFile": "Hapus File",
"deleteRecord": "Hapus dari Daftar",
"select": "Pilih",
"selectAll": "Pilih semua",
"selectVisible": "Pilih yang terlihat",
@@ -489,9 +502,6 @@
"disabled": "Dinonaktifkan",
"onlyLatestShort": "Hanya terbaru"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Tambah",
"refresh": "Segarkan",
@@ -660,4 +670,4 @@
"popularSection": "Platform utama",
"viewAll": "Lihat semua situs yang didukung"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "In attesa",
"downloadQueue": "Coda Download",
"customDownloadFolder": "Cartella di download personalizzata",
"retry": "Riprova download",
"autoFolderPlaceholder": "Cartella automatica (in base ai metadati)",
"autoFolderHint": "Le cartelle automatiche vengono create dai metadati.",
"useAutoFolder": "Usa cartella automatica",
@@ -158,6 +159,16 @@
"progress": "Progresso",
"showDetails": "Mostra dettagli",
"hideDetails": "Nascondi dettagli",
"viewLogs": "Visualizza log",
"detailsTab": "Dettagli",
"logsTab": "Log",
"logs": {
"live": "Log in tempo reale",
"history": "Log salvati",
"command": "Comando yt-dlp",
"empty": "Nessun log ancora.",
"scrollPaused": "Scorrimento in pausa"
},
"selectAudioFormat": "Seleziona Formato Audio",
"selectDownloadType": "Seleziona tipo di download",
"selectFormat": "Seleziona Formato",
@@ -269,6 +280,8 @@
"openInBrowser": "Clicca per aprire nel browser",
"removeAction": "Rimuovi",
"removeItem": "Rimuovi Elemento",
"deleteFile": "Elimina File",
"deleteRecord": "Rimuovi dall'Elenco",
"select": "Seleziona",
"selectAll": "Seleziona tutto",
"selectVisible": "Seleziona visibili",
@@ -489,9 +502,6 @@
"disabled": "Disabilitato",
"onlyLatestShort": "Solo più recente"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Aggiungere",
"refresh": "Aggiorna",
@@ -660,4 +670,4 @@
"popularSection": "Piattaforme principali",
"viewAll": "Visualizza tutti i siti supportati"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "保留中",
"downloadQueue": "ダウンロードキュー",
"customDownloadFolder": "カスタムダウンロードフォルダー",
"retry": "ダウンロードを再試行",
"autoFolderPlaceholder": "自動フォルダー(メタデータに基づく)",
"autoFolderHint": "自動フォルダーはメタデータから作成されます。",
"useAutoFolder": "自動フォルダーを使用",
@@ -158,6 +159,16 @@
"progress": "進行状況",
"showDetails": "詳細を表示",
"hideDetails": "詳細を隠す",
"viewLogs": "ログを表示",
"detailsTab": "詳細",
"logsTab": "ログ",
"logs": {
"live": "ライブログ",
"history": "保存済みログ",
"command": "yt-dlp コマンド",
"empty": "ログはまだありません。",
"scrollPaused": "スクロールを一時停止"
},
"selectAudioFormat": "オーディオフォーマットを選択",
"selectDownloadType": "ダウンロードの種類を選択",
"selectFormat": "フォーマットを選択",
@@ -269,6 +280,8 @@
"openInBrowser": "ブラウザで開くにはクリック",
"removeAction": "削除",
"removeItem": "アイテムを削除",
"deleteFile": "ファイルを削除",
"deleteRecord": "リストから削除",
"select": "選択",
"selectAll": "すべて選択",
"selectVisible": "表示中を選択",
@@ -489,9 +502,6 @@
"disabled": "無効",
"onlyLatestShort": "最新のもののみ"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "追加",
"refresh": "リフレッシュ",
@@ -660,4 +670,4 @@
"popularSection": "主要プラットフォーム",
"viewAll": "サポートされているすべてのサイトを表示"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "대기 중",
"downloadQueue": "다운로드 큐",
"customDownloadFolder": "사용자 지정 다운로드 폴더",
"retry": "다운로드 재시도",
"autoFolderPlaceholder": "자동 폴더(메타데이터 기반)",
"autoFolderHint": "자동 폴더는 메타데이터에서 생성됩니다.",
"useAutoFolder": "자동 폴더 사용",
@@ -158,6 +159,16 @@
"progress": "진행률",
"showDetails": "세부정보 표시",
"hideDetails": "세부정보 숨기기",
"viewLogs": "로그 보기",
"detailsTab": "세부정보",
"logsTab": "로그",
"logs": {
"live": "실시간 로그",
"history": "저장된 로그",
"command": "yt-dlp 명령어",
"empty": "아직 로그가 없습니다.",
"scrollPaused": "스크롤 일시 중지"
},
"selectAudioFormat": "오디오 형식 선택",
"selectDownloadType": "다운로드 유형 선택",
"selectFormat": "형식 선택",
@@ -269,6 +280,8 @@
"openInBrowser": "브라우저에서 열려면 클릭",
"removeAction": "제거",
"removeItem": "항목 제거",
"deleteFile": "파일 삭제",
"deleteRecord": "목록에서 제거",
"select": "선택",
"selectAll": "모두 선택",
"selectVisible": "표시된 항목 선택",
@@ -489,9 +502,6 @@
"disabled": "장애가 있는",
"onlyLatestShort": "최신만"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "추가하다",
"refresh": "새로 고치다",
@@ -660,4 +670,4 @@
"popularSection": "주요 플랫폼",
"viewAll": "지원되는 모든 사이트 보기"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "Pendente",
"downloadQueue": "Fila de Download",
"customDownloadFolder": "Pasta de download personalizada",
"retry": "Tentar baixar novamente",
"autoFolderPlaceholder": "Pasta automática (com base nos metadados)",
"autoFolderHint": "Pastas automáticas são criadas a partir de metadados.",
"useAutoFolder": "Usar pasta automática",
@@ -158,6 +159,16 @@
"progress": "Progresso",
"showDetails": "Mostrar detalhes",
"hideDetails": "Ocultar detalhes",
"viewLogs": "Ver logs",
"detailsTab": "Detalhes",
"logsTab": "Logs",
"logs": {
"live": "Logs ao vivo",
"history": "Logs salvos",
"command": "Comando do yt-dlp",
"empty": "Ainda não há logs.",
"scrollPaused": "Rolagem pausada"
},
"selectAudioFormat": "Selecionar Formato de Áudio",
"selectDownloadType": "Selecionar tipo de download",
"selectFormat": "Selecionar Formato",
@@ -269,6 +280,8 @@
"openInBrowser": "Clique para abrir no navegador",
"removeAction": "Remover",
"removeItem": "Remover Item",
"deleteFile": "Remover Arquivo",
"deleteRecord": "Remover da Lista",
"select": "Selecionar",
"selectAll": "Selecionar tudo",
"selectVisible": "Selecionar visíveis",
@@ -489,9 +502,6 @@
"disabled": "Desabilitado",
"onlyLatestShort": "Apenas o mais recente"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Adicionar",
"refresh": "Atualizar",
@@ -660,4 +670,4 @@
"popularSection": "Plataformas principais",
"viewAll": "Ver todos os sites suportados"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "Ожидание",
"downloadQueue": "Очередь загрузки",
"customDownloadFolder": "Пользовательская папка загрузки",
"retry": "Повторить загрузку",
"autoFolderPlaceholder": "Автоматическая папка (на основе метаданных)",
"autoFolderHint": "Автоматические папки создаются из метаданных.",
"useAutoFolder": "Использовать автоматическую папку",
@@ -158,6 +159,16 @@
"progress": "Прогресс",
"showDetails": "Показать детали",
"hideDetails": "Скрыть детали",
"viewLogs": "Посмотреть логи",
"detailsTab": "Детали",
"logsTab": "Логи",
"logs": {
"live": "Логи в реальном времени",
"history": "Сохранённые логи",
"command": "Команда yt-dlp",
"empty": "Пока нет логов.",
"scrollPaused": "Прокрутка приостановлена"
},
"selectAudioFormat": "Выбрать формат аудио",
"selectDownloadType": "Выберите тип загрузки",
"selectFormat": "Выбрать формат",
@@ -269,6 +280,8 @@
"openInBrowser": "Нажмите, чтобы открыть в браузере",
"removeAction": "Удалить",
"removeItem": "Удалить элемент",
"deleteFile": "Удалить файл",
"deleteRecord": "Удалить из списка",
"select": "Выбрать",
"selectAll": "Выбрать все",
"selectVisible": "Выбрать видимые",
@@ -489,9 +502,6 @@
"disabled": "Отключено",
"onlyLatestShort": "Только последнее"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "Добавить",
"refresh": "Обновить",
@@ -660,4 +670,4 @@
"popularSection": "Основные платформы",
"viewAll": "Просмотреть все поддерживаемые сайты"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "待處理",
"downloadQueue": "下載佇列",
"customDownloadFolder": "自訂下載資料夾",
"retry": "重試下載",
"autoFolderPlaceholder": "自動資料夾(依中繼資料)",
"autoFolderHint": "自動資料夾會由中繼資料建立。",
"useAutoFolder": "使用自動資料夾",
@@ -158,6 +159,16 @@
"progress": "進度",
"showDetails": "顯示詳情",
"hideDetails": "隱藏詳細信息",
"viewLogs": "查看日誌",
"detailsTab": "詳細資料",
"logsTab": "日誌",
"logs": {
"live": "即時日誌",
"history": "已儲存日誌",
"command": "yt-dlp 指令",
"empty": "尚無日誌。",
"scrollPaused": "捲動已暫停"
},
"selectAudioFormat": "選擇音訊格式",
"selectDownloadType": "選擇下載類型",
"selectFormat": "選擇格式",
@@ -269,6 +280,8 @@
"openInBrowser": "點擊在瀏覽器中開啟",
"removeAction": "移除",
"removeItem": "移除項目",
"deleteFile": "刪除檔案",
"deleteRecord": "從列表中移除",
"select": "選取",
"selectAll": "全選",
"selectVisible": "選取可見項目",
@@ -489,9 +502,6 @@
"disabled": "殘疾人",
"onlyLatestShort": "僅最新"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "添加",
"refresh": "重新整理",
@@ -660,4 +670,4 @@
"popularSection": "主流平台",
"viewAll": "檢視全部支援的網站"
}
}
}

View File

@@ -119,6 +119,7 @@
"downloadPending": "待处理",
"downloadQueue": "下载队列",
"customDownloadFolder": "自定义下载文件夹",
"retry": "重试下载",
"autoFolderPlaceholder": "自动文件夹(基于元数据)",
"autoFolderHint": "自动文件夹由元数据创建。",
"useAutoFolder": "使用自动文件夹",
@@ -158,6 +159,16 @@
"progress": "进度",
"showDetails": "显示详情",
"hideDetails": "隐藏详细信息",
"viewLogs": "查看日志",
"detailsTab": "详情",
"logsTab": "日志",
"logs": {
"live": "实时日志",
"history": "已保存日志",
"command": "yt-dlp 命令",
"empty": "暂无日志。",
"scrollPaused": "滚动已暂停"
},
"selectAudioFormat": "选择音频格式",
"selectDownloadType": "选择下载类型",
"selectFormat": "选择格式",
@@ -269,6 +280,8 @@
"openInBrowser": "点击在浏览器中打开",
"removeAction": "移除",
"removeItem": "移除项目",
"deleteFile": "删除文件",
"deleteRecord": "从列表中移除",
"select": "选择",
"selectAll": "全选",
"selectVisible": "选择可见项",
@@ -489,9 +502,6 @@
"disabled": "残疾人",
"onlyLatestShort": "仅最新"
},
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": {
"add": "添加",
"refresh": "刷新",
@@ -660,4 +670,4 @@
"popularSection": "主流平台",
"viewAll": "查看全部支持的网站"
}
}
}

View File

@@ -28,6 +28,7 @@ const toHistoryRecord = (item: DownloadHistoryItem): DownloadRecord => ({
progress: undefined,
error: item.error,
ytDlpCommand: item.ytDlpCommand,
ytDlpLog: item.ytDlpLog,
downloadPath: item.downloadPath,
speed: undefined,
duration: item.duration,
@@ -190,8 +191,9 @@ export const downloadStatsAtom = atom((get) => {
(acc, item) => {
acc.total += 1
if (
item.entryType === 'active' &&
(item.status === 'downloading' || item.status === 'processing' || item.status === 'pending')
(item.entryType === 'active' && item.status === 'downloading') ||
item.status === 'processing' ||
item.status === 'pending'
) {
acc.active += 1
}
@@ -209,8 +211,8 @@ export const activeDownloadsCountAtom = atom((get) => {
let count = 0
for (const item of downloads.values()) {
if (
item.entryType === 'active' &&
(item.status === 'downloading' || item.status === 'processing')
(item.entryType === 'active' && item.status === 'downloading') ||
item.status === 'processing'
) {
count++
}

View File

@@ -67,6 +67,7 @@ export interface DownloadItem {
error?: string
speed?: string
ytDlpCommand?: string
ytDlpLog?: string
// Enhanced video information
duration?: number
fileSize?: number
@@ -117,6 +118,7 @@ export interface DownloadHistoryItem {
completedAt?: number
error?: string
ytDlpCommand?: string
ytDlpLog?: string
// Additional metadata
description?: string
channel?: string