Compare commits
6 Commits
113-downlo
...
v1.2.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
f646f287e6 | ||
|
|
603a4f051d | ||
|
|
e2bfa334c0 | ||
|
|
e8769fefb0 | ||
|
|
b88411d229 | ||
|
|
b6c67b23f6 |
4
.github/workflows/translator.yaml
vendored
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
**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.
|
||||
|
||||

|
||||
|
||||
**Steps:**
|
||||
|
||||
1. Export a Netscape cookies file using a browser extension.
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "VidBee Docs",
|
||||
"pages": ["index", "cookies", "faq"]
|
||||
"pages": ["index", "protocol", "cookies", "faq"]
|
||||
}
|
||||
|
||||
149
docs/content/protocol.mdx
Normal 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/)
|
||||
@@ -17,11 +17,13 @@ Cookie 用于复用浏览器的登录态,帮助 VidBee 下载需要登录或
|
||||
|
||||
在 **设置** 中选择你的浏览器,VidBee 会尝试自动识别浏览器配置文件路径。你也可以手动填写配置文件路径。
|
||||
|
||||

|
||||
|
||||
**支持的浏览器(按平台差异显示):**
|
||||
|
||||
- **Windows:仅支持 Firefox。其他浏览器无法读取 Cookie。**
|
||||
- macOS:Safari、Chrome、Edge、Brave、Firefox 等。
|
||||
- Linux:Chrome、Chromium、Brave、Firefox 等。
|
||||
- macOS:全部支持。
|
||||
- Linux:全部支持。
|
||||
|
||||
**使用步骤:**
|
||||
|
||||
@@ -36,6 +38,8 @@ Cookie 用于复用浏览器的登录态,帮助 VidBee 下载需要登录或
|
||||
|
||||
你也可以导入 **Netscape 格式** 的 cookies 文件。这个方式适合在不方便读取浏览器配置文件时使用。
|
||||
|
||||

|
||||
|
||||
**使用步骤:**
|
||||
|
||||
1. 使用浏览器扩展导出 Netscape cookies 文件。
|
||||
|
||||
@@ -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):常见问题与排查思路。
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{
|
||||
"title": "VidBee 文档",
|
||||
"pages": ["index", "cookies", "faq"]
|
||||
"pages": ["index", "protocol", "cookies", "faq"]
|
||||
}
|
||||
|
||||
149
docs/content/zh/protocol.mdx
Normal 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/)
|
||||
@@ -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
@@ -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
|
||||
BIN
docs/public/apple-touch-icon.png
Normal file
|
After Width: | Height: | Size: 9.9 KiB |
BIN
docs/public/browser-cookies.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/public/cookies-file.png
Normal file
|
After Width: | Height: | Size: 1.9 MiB |
BIN
docs/public/favicon.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/public/icon-128.png
Normal file
|
After Width: | Height: | Size: 6.9 KiB |
BIN
docs/public/icon-16.png
Normal file
|
After Width: | Height: | Size: 924 B |
BIN
docs/public/icon-192.png
Normal file
|
After Width: | Height: | Size: 11 KiB |
BIN
docs/public/icon-256.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/public/icon-32.png
Normal file
|
After Width: | Height: | Size: 1.8 KiB |
BIN
docs/public/icon-48.png
Normal file
|
After Width: | Height: | Size: 2.6 KiB |
BIN
docs/public/icon-512.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/public/icon-64.png
Normal file
|
After Width: | Height: | Size: 3.4 KiB |
BIN
docs/public/icon-original.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
BIN
docs/public/icon.png
Normal file
|
After Width: | Height: | Size: 14 KiB |
@@ -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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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
@@ -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)}`,
|
||||
};
|
||||
});
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
@@ -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'
|
||||
}
|
||||
|
||||
@@ -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')) {
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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'),
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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 && (
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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": "عرض جميع المواقع المدعومة"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "サポートされているすべてのサイトを表示"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "지원되는 모든 사이트 보기"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "Просмотреть все поддерживаемые сайты"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "檢視全部支援的網站"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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": "查看全部支持的网站"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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++
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||