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] types: [created, edited]
discussion_comment: discussion_comment:
types: [created, edited] types: [created, edited]
pull_request_target:
types: [opened, edited]
pull_request_review_comment:
types: [created, edited]
jobs: jobs:
translate: 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. 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):** **Supported browsers (platform dependent):**
- **Windows: Firefox only. Other browsers cannot be used for cookie reading.** - **Windows: Firefox only. Other browsers cannot be used for cookie reading.**
- macOS: Safari, Chrome, Edge, Brave, Firefox, and more. - macOS: All browsers supported.
- Linux: Chrome, Chromium, Brave, Firefox, and more. - Linux: All browsers supported.
**Steps:** **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. 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:** **Steps:**
1. Export a Netscape cookies file using a browser extension. 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 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 ## Start here
- [vidbee:// Protocol](./protocol.mdx): Quick download using URL protocol.
- [Cookies](./cookies.mdx): Configure signed-in sessions and restricted content. - [Cookies](./cookies.mdx): Configure signed-in sessions and restricted content.
- [FAQ](./faq.mdx): Common questions and troubleshooting. - [FAQ](./faq.mdx): Common questions and troubleshooting.

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
{ {
"title": "VidBee 文档", "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} */ /** @type {import('next').NextConfig} */
const config = { const config = {
output: 'export', // Only use static export for production builds, not dev mode
output: process.env.NODE_ENV === 'production' ? 'export' : undefined,
reactStrictMode: true, reactStrictMode: true,
// Use trailing slashes to avoid conflicts with route handlers that have file extensions // Use trailing slashes to avoid conflicts with route handlers that have file extensions
trailingSlash: true, 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); const page = source.getPage(params.slug, params.lang);
if (!page) notFound(); if (!page) notFound();
const baseUrl = 'https://docs.vidbee.org';
const canonicalUrl = `${baseUrl}${page.url}`;
return { return {
title: page.data.title, title: `${page.data.title} | VidBee Docs`,
description: page.data.description, description: page.data.description,
openGraph: { openGraph: {
images: getPageImage(page).url, images: getPageImage(page).url,
}, },
alternates: {
canonical: canonicalUrl,
},
}; };
} }

View File

@@ -1,12 +1,25 @@
import './global.css'; import './global.css';
import { Inter } from 'next/font/google'; import { Inter } from 'next/font/google';
import type { ReactNode } from 'react'; import type { ReactNode } from 'react';
import type { Metadata } from 'next';
import { i18n, isLocale } from '@/lib/i18n'; import { i18n, isLocale } from '@/lib/i18n';
const inter = Inter({ const inter = Inter({
subsets: ['latin'], 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({ export default async function Layout({
children, children,
params, 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", "name": "vidbee",
"version": "1.2.0", "version": "1.2.1",
"description": "A modern Electron application for downloading videos and audios", "description": "A modern Electron application for downloading videos and audios",
"main": "./out/main/index.js", "main": "./out/main/index.js",
"author": "VidBee", "author": "VidBee",

View File

@@ -35,7 +35,7 @@ const PLATFORM_CONFIG = {
ffprobeOutput: 'ffprobe.exe', ffprobeOutput: 'ffprobe.exe',
extract: 'unzip', extract: 'unzip',
release: { 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, assetPattern: /ffmpeg-master-latest-win64-gpl\.zip$/i,
binaryName: 'ffmpeg.exe' binaryName: 'ffmpeg.exe'
} }
@@ -87,7 +87,7 @@ const PLATFORM_CONFIG = {
ffprobeOutput: 'ffprobe', ffprobeOutput: 'ffprobe',
extract: 'tar', extract: 'tar',
release: { 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, assetPattern: /ffmpeg-master-latest-linux64-gpl\.tar\.xz$/i,
binaryName: 'ffmpeg' binaryName: 'ffmpeg'
} }

View File

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

View File

@@ -253,6 +253,10 @@ function setupDownloadEvents(): void {
mainWindow?.webContents.send('download:progress', { id, progress }) 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) => { downloadEngine.on('download-completed', (id: string) => {
mainWindow?.webContents.send('download:completed', id) 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() @IpcMethod()
async copyFileToClipboard(_context: IpcContext, filePath: string): Promise<boolean> { async copyFileToClipboard(_context: IpcContext, filePath: string): Promise<boolean> {
try { try {

View File

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

View File

@@ -628,16 +628,34 @@ class DownloadEngine extends EventEmitter {
`Starting playlist download: ${selectionSize} items from "${playlistInfo.title}"` `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 // Create download items for each video in the playlist
for (const entry of selectedEntries) { for (const entry of selectedEntries) {
const downloadId = `${groupId}_${Math.random().toString(36).substring(2, 10)}` 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 = { const downloadOptions: DownloadOptions = {
url: entry.url, url: entry.url,
type: options.type, type: options.type,
format: options.format, format: options.format,
audioFormat: options.type === 'audio' ? options.format : undefined, audioFormat: options.type === 'audio' ? options.format : undefined,
customDownloadPath: resolvedDownloadPath customDownloadPath: resolvedDownloadPath,
customFilenameTemplate
} }
const createdAt = Date.now() const createdAt = Date.now()
@@ -751,6 +769,46 @@ class DownloadEngine extends EventEmitter {
let totalParts = estimateProgressParts(options) let totalParts = estimateProgressParts(options)
let completedParts = 0 let completedParts = 0
let lastPercent = 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 // First, get detailed video info to capture basic metadata and formats
try { try {
@@ -925,6 +983,14 @@ class DownloadEngine extends EventEmitter {
this.activeDownloads.set(id, { controller, process: ytdlpProcess }) 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.queue.updateItemInfo(id, { status: 'downloading', startedAt: Date.now() })
this.scheduleSessionPersist() this.scheduleSessionPersist()
this.emit('download-started', id) this.emit('download-started', id)
@@ -1024,6 +1090,7 @@ class DownloadEngine extends EventEmitter {
// Handle completion // Handle completion
ytdlpProcess.on('close', async (code: number | null) => { ytdlpProcess.on('close', async (code: number | null) => {
flushLogUpdate()
this.activeDownloads.delete(id) this.activeDownloads.delete(id)
this.queue.downloadCompleted(id) this.queue.downloadCompleted(id)
@@ -1154,6 +1221,7 @@ class DownloadEngine extends EventEmitter {
// Handle errors // Handle errors
ytdlpProcess.on('error', (error: Error) => { ytdlpProcess.on('error', (error: Error) => {
flushLogUpdate()
scopedLoggers.download.error('Download process error for ID:', id, error) scopedLoggers.download.error('Download process error for ID:', id, error)
this.activeDownloads.delete(id) this.activeDownloads.delete(id)
this.queue.downloadCompleted(id) this.queue.downloadCompleted(id)
@@ -1329,6 +1397,9 @@ class DownloadEngine extends EventEmitter {
if (updates.ytDlpCommand !== undefined) { if (updates.ytDlpCommand !== undefined) {
historyUpdates.ytDlpCommand = updates.ytDlpCommand historyUpdates.ytDlpCommand = updates.ytDlpCommand
} }
if (updates.ytDlpLog !== undefined) {
historyUpdates.ytDlpLog = updates.ytDlpLog
}
if (updates.savedFileName !== undefined) { if (updates.savedFileName !== undefined) {
historyUpdates.savedFileName = updates.savedFileName historyUpdates.savedFileName = updates.savedFileName
} }
@@ -1377,7 +1448,7 @@ class DownloadEngine extends EventEmitter {
): void { ): void {
// Get the download item from the queue to get additional info // Get the download item from the queue to get additional info
const completedDownload = this.queue.getCompletedDownload(id) const completedDownload = this.queue.getCompletedDownload(id)
scopedLoggers.download.info('Completed download:', completedDownload) // scopedLoggers.download.info('Completed download:', completedDownload)
const completedAt = Date.now() const completedAt = Date.now()
this.upsertHistoryEntry(id, options, { this.upsertHistoryEntry(id, options, {
@@ -1425,6 +1496,7 @@ class DownloadEngine extends EventEmitter {
completedAt: updates.completedAt, completedAt: updates.completedAt,
error: updates.error, error: updates.error,
ytDlpCommand: updates.ytDlpCommand, ytDlpCommand: updates.ytDlpCommand,
ytDlpLog: updates.ytDlpLog,
description: updates.description, description: updates.description,
channel: updates.channel, channel: updates.channel,
uploader: updates.uploader, uploader: updates.uploader,

View File

@@ -37,6 +37,7 @@ const createDownloadHistoryTableSql = sql`
sort_key INTEGER NOT NULL, sort_key INTEGER NOT NULL,
error TEXT, error TEXT,
yt_dlp_command TEXT, yt_dlp_command TEXT,
yt_dlp_log TEXT,
description TEXT, description TEXT,
channel TEXT, channel TEXT,
uploader TEXT, uploader TEXT,
@@ -219,20 +220,53 @@ class HistoryManager {
} }
const deprecatedColumns = ['subscription_title', 'format', 'quality', 'codec'] 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 hasDeprecated = columns.some((column) => deprecatedColumns.includes(column.name))
const missingRequired = requiredColumns.some( const missingRequired = requiredColumns.filter(
(columnName) => !columns.some((column) => column.name === columnName) (columnName) => !columns.some((column) => column.name === columnName)
) )
const needsRebuild = hasDeprecated || missingRequired if (hasDeprecated) {
if (needsRebuild) {
this.rebuildDownloadHistoryTable() this.rebuildDownloadHistoryTable()
return
}
if (missingRequired.length > 0) {
this.addMissingColumns(missingRequired)
} }
} catch (error) { } catch (error) {
logger.error('history-db failed to inspect schema', 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 { private migrateLegacyPayloadTable(): void {
const database = this.getDatabase() const database = this.getDatabase()
logger.info('history-db migrating legacy payload schema to structured columns') logger.info('history-db migrating legacy payload schema to structured columns')
@@ -394,6 +428,7 @@ class HistoryManager {
sortKey: item.completedAt ?? item.downloadedAt, sortKey: item.completedAt ?? item.downloadedAt,
error: item.error ?? null, error: item.error ?? null,
ytDlpCommand: item.ytDlpCommand ?? null, ytDlpCommand: item.ytDlpCommand ?? null,
ytDlpLog: item.ytDlpLog ?? null,
description: item.description ?? null, description: item.description ?? null,
channel: item.channel ?? null, channel: item.channel ?? null,
uploader: item.uploader ?? null, uploader: item.uploader ?? null,
@@ -441,6 +476,7 @@ class HistoryManager {
completedAt: row.completedAt ?? undefined, completedAt: row.completedAt ?? undefined,
error: row.error ?? undefined, error: row.error ?? undefined,
ytDlpCommand: row.ytDlpCommand ?? undefined, ytDlpCommand: row.ytDlpCommand ?? undefined,
ytDlpLog: row.ytDlpLog ?? undefined,
description: row.description ?? undefined, description: row.description ?? undefined,
channel: row.channel ?? undefined, channel: row.channel ?? undefined,
uploader: row.uploader ?? 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() 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 = ( const filterFormatsByType = (
formats: VideoInfo['formats'], formats: VideoInfo['formats'],
activeTab: 'video' | 'audio' activeTab: 'video' | 'audio'
@@ -270,6 +276,30 @@ const FormatList = ({ formats, type, codec, selectedFormat, onFormatChange }: Fo
return `${mb.toFixed(2)} MB` 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) => { const formatVideoQuality = (format: VideoFormat) => {
if (format.height) { if (format.height) {
return `${format.height}p${format.fps === 60 ? '60' : ''}` 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() ? format.acodec.split('.')[0].toUpperCase()
: '' : ''
const sizeLabel = formatSize(format.filesize || format.filesize_approx) const sizeLabel = formatSize(format.filesize || format.filesize_approx)
const metaLabel = formatMetaLabel(format)
const isSelected = selectedFormat === format.format_id const isSelected = selectedFormat === format.format_id
return ( return (
@@ -363,12 +394,19 @@ const FormatList = ({ formats, type, codec, selectedFormat, onFormatChange }: Fo
{qualityLabel} {qualityLabel}
</span> </span>
<div className="flex-1 flex items-center gap-2 min-w-0"> <div className="flex-1 min-w-0">
<span className="text-xs text-muted-foreground truncate">{detailLabel}</span> <div className="flex items-center gap-2 min-w-0">
{thirdColumnLabel && thirdColumnLabel !== '-' && ( <span className="text-xs text-muted-foreground truncate">{detailLabel}</span>
<span className="shrink-0 px-1.5 py-0.5 rounded bg-muted text-[10px] font-medium text-muted-foreground"> {thirdColumnLabel && thirdColumnLabel !== '-' && (
{thirdColumnLabel} <span className="shrink-0 px-1.5 py-0.5 rounded bg-muted text-[10px] font-medium text-muted-foreground">
</span> {thirdColumnLabel}
</span>
)}
</div>
{metaLabel && (
<div className="mt-0.5 text-[10px] text-muted-foreground/70 leading-snug break-words">
{metaLabel}
</div>
)} )}
</div> </div>
@@ -401,7 +439,16 @@ export function SingleVideoDownload({
const relevantFormats = useMemo(() => { const relevantFormats = useMemo(() => {
if (!videoInfo?.formats) return [] 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]) }, [videoInfo?.formats, activeTab])
const containers = useMemo(() => { const containers = useMemo(() => {

View File

@@ -178,7 +178,7 @@ export function SubscriptionFormDialog({
const handleOpenRSSHubDocs = async () => { const handleOpenRSSHubDocs = async () => {
try { try {
await ipcServices.fs.openExternal('https://docs.rsshub.app/routes/social-media#youtube') await ipcServices.fs.openExternal('https://docs.rsshub.app/routes/')
} catch (error) { } catch (error) {
console.error('Failed to open RSSHub documentation:', error) console.error('Failed to open RSSHub documentation:', error)
toast.error(t('subscriptions.notifications.openLinkError')) toast.error(t('subscriptions.notifications.openLinkError'))
@@ -242,7 +242,7 @@ export function SubscriptionFormDialog({
<Input <Input
id={urlInputId} id={urlInputId}
value={url} value={url}
placeholder={t('subscriptions.placeholders.url')} placeholder="https://docs.rsshub.app/routes/youtube/user/@FKJ"
onChange={(event) => setUrl(event.target.value)} onChange={(event) => setUrl(event.target.value)}
/> />
{detectingFeed && ( {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 handleCompleted = (rawId: unknown) => {
const id = typeof rawId === 'string' ? rawId : '' const id = typeof rawId === 'string' ? rawId : ''
if (!id) { if (!id) {
@@ -129,13 +139,14 @@ export function useDownloadEvents() {
const startedSubscription = ipcEvents.on('download:started', handleStarted) const startedSubscription = ipcEvents.on('download:started', handleStarted)
const progressSubscription = ipcEvents.on('download:progress', handleProgress) const progressSubscription = ipcEvents.on('download:progress', handleProgress)
const logSubscription = ipcEvents.on('download:log', handleLog)
const completedSubscription = ipcEvents.on('download:completed', handleCompleted) const completedSubscription = ipcEvents.on('download:completed', handleCompleted)
const errorSubscription = ipcEvents.on('download:error', handleError) const errorSubscription = ipcEvents.on('download:error', handleError)
const cancelledSubscription = ipcEvents.on('download:cancelled', handleCancelled) const cancelledSubscription = ipcEvents.on('download:cancelled', handleCancelled)
return () => { return () => {
ipcEvents.removeListener('download:started', startedSubscription) ipcEvents.removeListener('download:started', startedSubscription)
ipcEvents.removeListener('download:progress', progressSubscription) ipcEvents.removeListener('download:progress', progressSubscription)
ipcEvents.removeListener('download:log', logSubscription)
ipcEvents.removeListener('download:completed', completedSubscription) ipcEvents.removeListener('download:completed', completedSubscription)
ipcEvents.removeListener('download:error', errorSubscription) ipcEvents.removeListener('download:error', errorSubscription)
ipcEvents.removeListener('download:cancelled', cancelledSubscription) ipcEvents.removeListener('download:cancelled', cancelledSubscription)

View File

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

View File

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

View File

@@ -119,6 +119,7 @@
"downloadPending": "Pending", "downloadPending": "Pending",
"downloadQueue": "Download Queue", "downloadQueue": "Download Queue",
"customDownloadFolder": "Custom download folder", "customDownloadFolder": "Custom download folder",
"retry": "Retry Download",
"autoFolderPlaceholder": "Automatic folder (based on metadata)", "autoFolderPlaceholder": "Automatic folder (based on metadata)",
"autoFolderHint": "Automatic folders are created from metadata.", "autoFolderHint": "Automatic folders are created from metadata.",
"useAutoFolder": "Use automatic folder", "useAutoFolder": "Use automatic folder",
@@ -158,6 +159,16 @@
"progress": "Progress", "progress": "Progress",
"showDetails": "Show details", "showDetails": "Show details",
"hideDetails": "Hide 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", "selectAudioFormat": "Select Audio Format",
"selectDownloadType": "Select download type", "selectDownloadType": "Select download type",
"selectFormat": "Select Format", "selectFormat": "Select Format",
@@ -269,6 +280,8 @@
"openInBrowser": "Click to open in browser", "openInBrowser": "Click to open in browser",
"removeAction": "Remove", "removeAction": "Remove",
"removeItem": "Remove Item", "removeItem": "Remove Item",
"deleteFile": "Delete File",
"deleteRecord": "Remove from List",
"select": "Select", "select": "Select",
"selectAll": "Select All", "selectAll": "Select All",
"selectVisible": "Select visible", "selectVisible": "Select visible",
@@ -489,9 +502,6 @@
"disabled": "Disabled", "disabled": "Disabled",
"onlyLatestShort": "Only latest" "onlyLatestShort": "Only latest"
}, },
"placeholders": {
"url": "https://rsshub.app/youtube/user/@FKJ"
},
"actions": { "actions": {
"add": "Add", "add": "Add",
"refresh": "Refresh", "refresh": "Refresh",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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