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.
This commit is contained in:
Nexmoe
2026-01-20 22:47:18 +08:00
committed by GitHub
parent e2bfa334c0
commit 603a4f051d
25 changed files with 1015 additions and 355 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -769,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 {
@@ -943,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)
@@ -1042,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)
@@ -1172,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)
@@ -1347,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
}
@@ -1395,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, {
@@ -1443,6 +1496,7 @@ class DownloadEngine extends EventEmitter {
completedAt: updates.completedAt,
error: updates.error,
ytDlpCommand: updates.ytDlpCommand,
ytDlpLog: updates.ytDlpLog,
description: updates.description,
channel: updates.channel,
uploader: updates.uploader,

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

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

View File

@@ -119,6 +119,7 @@
"downloadPending": "قيد الانتظار",
"downloadQueue": "قائمة انتظار التحميل",
"customDownloadFolder": "مجلد تنزيل مخصص",
"retry": "إعادة المحاولة",
"autoFolderPlaceholder": "مجلد تلقائي (استنادًا إلى البيانات الوصفية)",
"autoFolderHint": "يتم إنشاء المجلدات التلقائية من البيانات الوصفية.",
"useAutoFolder": "استخدام المجلد التلقائي",
@@ -158,6 +159,16 @@
"progress": "التقدم",
"showDetails": "إظهار التفاصيل",
"hideDetails": "إخفاء التفاصيل",
"viewLogs": "عرض السجلات",
"detailsTab": "التفاصيل",
"logsTab": "السجلات",
"logs": {
"live": "سجلات مباشرة",
"history": "سجلات محفوظة",
"command": "أمر yt-dlp",
"empty": "لا توجد سجلات بعد.",
"scrollPaused": "تم إيقاف التمرير"
},
"selectAudioFormat": "اختر تنسيق الصوت",
"selectDownloadType": "اختر نوع التنزيل",
"selectFormat": "اختر التنسيق",
@@ -269,6 +280,8 @@
"openInBrowser": "انقر لفتح في المتصفح",
"removeAction": "إزالة",
"removeItem": "إزالة العنصر",
"deleteFile": "حذف الملف",
"deleteRecord": "إزالة من القائمة",
"select": "تحديد",
"selectAll": "تحديد الكل",
"selectVisible": "تحديد المرئي",

View File

@@ -119,6 +119,7 @@
"downloadPending": "Ausstehend",
"downloadQueue": "Download-Warteschlange",
"customDownloadFolder": "Benutzerdefinierter Download-Ordner",
"retry": "Download erneut versuchen",
"autoFolderPlaceholder": "Automatischer Ordner (basierend auf Metadaten)",
"autoFolderHint": "Automatische Ordner werden aus Metadaten erstellt.",
"useAutoFolder": "Automatischen Ordner verwenden",
@@ -158,6 +159,16 @@
"progress": "Fortschritt",
"showDetails": "Details anzeigen",
"hideDetails": "Details ausblenden",
"viewLogs": "Logs anzeigen",
"detailsTab": "Details",
"logsTab": "Logs",
"logs": {
"live": "Live-Logs",
"history": "Gespeicherte Logs",
"command": "yt-dlp-Befehl",
"empty": "Noch keine Logs.",
"scrollPaused": "Scrollen pausiert"
},
"selectAudioFormat": "Audio-Format auswählen",
"selectDownloadType": "Download-Typ auswählen",
"selectFormat": "Format auswählen",
@@ -269,6 +280,8 @@
"openInBrowser": "Klicken, um im Browser zu öffnen",
"removeAction": "Entfernen",
"removeItem": "Element entfernen",
"deleteFile": "Datei löschen",
"deleteRecord": "Aus Liste entfernen",
"select": "Auswählen",
"selectAll": "Alle auswählen",
"selectVisible": "Sichtbare auswählen",

View File

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

View File

@@ -119,6 +119,7 @@
"downloadPending": "Pendiente",
"downloadQueue": "Cola de Descarga",
"customDownloadFolder": "Carpeta de descarga personalizada",
"retry": "Reintentar descarga",
"autoFolderPlaceholder": "Carpeta automática (basada en metadatos)",
"autoFolderHint": "Las carpetas automáticas se crean a partir de metadatos.",
"useAutoFolder": "Usar carpeta automática",
@@ -158,6 +159,16 @@
"progress": "Progreso",
"showDetails": "Mostrar detalles",
"hideDetails": "Ocultar detalles",
"viewLogs": "Ver registros",
"detailsTab": "Detalles",
"logsTab": "Registros",
"logs": {
"live": "Registros en vivo",
"history": "Registros guardados",
"command": "Comando de yt-dlp",
"empty": "Aún no hay registros.",
"scrollPaused": "Desplazamiento en pausa"
},
"selectAudioFormat": "Seleccionar Formato de Audio",
"selectDownloadType": "Seleccionar tipo de descarga",
"selectFormat": "Seleccionar Formato",
@@ -269,6 +280,8 @@
"openInBrowser": "Haz clic para abrir en el navegador",
"removeAction": "Eliminar",
"removeItem": "Eliminar Elemento",
"deleteFile": "Eliminar Archivo",
"deleteRecord": "Eliminar de la Lista",
"select": "Seleccionar",
"selectAll": "Seleccionar todo",
"selectVisible": "Seleccionar visibles",

View File

@@ -119,6 +119,7 @@
"downloadPending": "En attente",
"downloadQueue": "File de Téléchargement",
"customDownloadFolder": "Dossier de téléchargement personnalisé",
"retry": "Relancer le téléchargement",
"autoFolderPlaceholder": "Dossier automatique (basé sur les métadonnées)",
"autoFolderHint": "Les dossiers automatiques sont créés à partir des métadonnées.",
"useAutoFolder": "Utiliser le dossier automatique",
@@ -158,6 +159,16 @@
"progress": "Progrès",
"showDetails": "Afficher les détails",
"hideDetails": "Masquer les détails",
"viewLogs": "Voir les journaux",
"detailsTab": "Détails",
"logsTab": "Journaux",
"logs": {
"live": "Journaux en direct",
"history": "Journaux enregistrés",
"command": "Commande yt-dlp",
"empty": "Aucun journal pour le moment.",
"scrollPaused": "Défilement en pause"
},
"selectAudioFormat": "Sélectionner le Format Audio",
"selectDownloadType": "Sélectionner le type de téléchargement",
"selectFormat": "Sélectionner le Format",
@@ -269,6 +280,8 @@
"openInBrowser": "Cliquez pour ouvrir dans le navigateur",
"removeAction": "Supprimer",
"removeItem": "Supprimer l'Élément",
"deleteFile": "Supprimer le Fichier",
"deleteRecord": "Supprimer de la Liste",
"select": "Sélectionner",
"selectAll": "Tout sélectionner",
"selectVisible": "Sélectionner les visibles",

View File

@@ -119,6 +119,7 @@
"downloadPending": "Menunggu",
"downloadQueue": "Antrian Unduhan",
"customDownloadFolder": "Folder unduhan khusus",
"retry": "Coba ulang unduhan",
"autoFolderPlaceholder": "Folder otomatis (berdasarkan metadata)",
"autoFolderHint": "Folder otomatis dibuat dari metadata.",
"useAutoFolder": "Gunakan folder otomatis",
@@ -158,6 +159,16 @@
"progress": "Kemajuan",
"showDetails": "Tampilkan detail",
"hideDetails": "Sembunyikan detail",
"viewLogs": "Lihat log",
"detailsTab": "Detail",
"logsTab": "Log",
"logs": {
"live": "Log langsung",
"history": "Log tersimpan",
"command": "Perintah yt-dlp",
"empty": "Belum ada log.",
"scrollPaused": "Pengguliran dijeda"
},
"selectAudioFormat": "Pilih Format Audio",
"selectDownloadType": "Pilih jenis unduhan",
"selectFormat": "Pilih Format",
@@ -269,6 +280,8 @@
"openInBrowser": "Klik untuk membuka di browser",
"removeAction": "Hapus",
"removeItem": "Hapus Item",
"deleteFile": "Hapus File",
"deleteRecord": "Hapus dari Daftar",
"select": "Pilih",
"selectAll": "Pilih semua",
"selectVisible": "Pilih yang terlihat",

View File

@@ -119,6 +119,7 @@
"downloadPending": "In attesa",
"downloadQueue": "Coda Download",
"customDownloadFolder": "Cartella di download personalizzata",
"retry": "Riprova download",
"autoFolderPlaceholder": "Cartella automatica (in base ai metadati)",
"autoFolderHint": "Le cartelle automatiche vengono create dai metadati.",
"useAutoFolder": "Usa cartella automatica",
@@ -158,6 +159,16 @@
"progress": "Progresso",
"showDetails": "Mostra dettagli",
"hideDetails": "Nascondi dettagli",
"viewLogs": "Visualizza log",
"detailsTab": "Dettagli",
"logsTab": "Log",
"logs": {
"live": "Log in tempo reale",
"history": "Log salvati",
"command": "Comando yt-dlp",
"empty": "Nessun log ancora.",
"scrollPaused": "Scorrimento in pausa"
},
"selectAudioFormat": "Seleziona Formato Audio",
"selectDownloadType": "Seleziona tipo di download",
"selectFormat": "Seleziona Formato",
@@ -269,6 +280,8 @@
"openInBrowser": "Clicca per aprire nel browser",
"removeAction": "Rimuovi",
"removeItem": "Rimuovi Elemento",
"deleteFile": "Elimina File",
"deleteRecord": "Rimuovi dall'Elenco",
"select": "Seleziona",
"selectAll": "Seleziona tutto",
"selectVisible": "Seleziona visibili",

View File

@@ -119,6 +119,7 @@
"downloadPending": "保留中",
"downloadQueue": "ダウンロードキュー",
"customDownloadFolder": "カスタムダウンロードフォルダー",
"retry": "ダウンロードを再試行",
"autoFolderPlaceholder": "自動フォルダー(メタデータに基づく)",
"autoFolderHint": "自動フォルダーはメタデータから作成されます。",
"useAutoFolder": "自動フォルダーを使用",
@@ -158,6 +159,16 @@
"progress": "進行状況",
"showDetails": "詳細を表示",
"hideDetails": "詳細を隠す",
"viewLogs": "ログを表示",
"detailsTab": "詳細",
"logsTab": "ログ",
"logs": {
"live": "ライブログ",
"history": "保存済みログ",
"command": "yt-dlp コマンド",
"empty": "ログはまだありません。",
"scrollPaused": "スクロールを一時停止"
},
"selectAudioFormat": "オーディオフォーマットを選択",
"selectDownloadType": "ダウンロードの種類を選択",
"selectFormat": "フォーマットを選択",
@@ -269,6 +280,8 @@
"openInBrowser": "ブラウザで開くにはクリック",
"removeAction": "削除",
"removeItem": "アイテムを削除",
"deleteFile": "ファイルを削除",
"deleteRecord": "リストから削除",
"select": "選択",
"selectAll": "すべて選択",
"selectVisible": "表示中を選択",

View File

@@ -119,6 +119,7 @@
"downloadPending": "대기 중",
"downloadQueue": "다운로드 큐",
"customDownloadFolder": "사용자 지정 다운로드 폴더",
"retry": "다운로드 재시도",
"autoFolderPlaceholder": "자동 폴더(메타데이터 기반)",
"autoFolderHint": "자동 폴더는 메타데이터에서 생성됩니다.",
"useAutoFolder": "자동 폴더 사용",
@@ -158,6 +159,16 @@
"progress": "진행률",
"showDetails": "세부정보 표시",
"hideDetails": "세부정보 숨기기",
"viewLogs": "로그 보기",
"detailsTab": "세부정보",
"logsTab": "로그",
"logs": {
"live": "실시간 로그",
"history": "저장된 로그",
"command": "yt-dlp 명령어",
"empty": "아직 로그가 없습니다.",
"scrollPaused": "스크롤 일시 중지"
},
"selectAudioFormat": "오디오 형식 선택",
"selectDownloadType": "다운로드 유형 선택",
"selectFormat": "형식 선택",
@@ -269,6 +280,8 @@
"openInBrowser": "브라우저에서 열려면 클릭",
"removeAction": "제거",
"removeItem": "항목 제거",
"deleteFile": "파일 삭제",
"deleteRecord": "목록에서 제거",
"select": "선택",
"selectAll": "모두 선택",
"selectVisible": "표시된 항목 선택",

View File

@@ -119,6 +119,7 @@
"downloadPending": "Pendente",
"downloadQueue": "Fila de Download",
"customDownloadFolder": "Pasta de download personalizada",
"retry": "Tentar baixar novamente",
"autoFolderPlaceholder": "Pasta automática (com base nos metadados)",
"autoFolderHint": "Pastas automáticas são criadas a partir de metadados.",
"useAutoFolder": "Usar pasta automática",
@@ -158,6 +159,16 @@
"progress": "Progresso",
"showDetails": "Mostrar detalhes",
"hideDetails": "Ocultar detalhes",
"viewLogs": "Ver logs",
"detailsTab": "Detalhes",
"logsTab": "Logs",
"logs": {
"live": "Logs ao vivo",
"history": "Logs salvos",
"command": "Comando do yt-dlp",
"empty": "Ainda não há logs.",
"scrollPaused": "Rolagem pausada"
},
"selectAudioFormat": "Selecionar Formato de Áudio",
"selectDownloadType": "Selecionar tipo de download",
"selectFormat": "Selecionar Formato",
@@ -269,6 +280,8 @@
"openInBrowser": "Clique para abrir no navegador",
"removeAction": "Remover",
"removeItem": "Remover Item",
"deleteFile": "Remover Arquivo",
"deleteRecord": "Remover da Lista",
"select": "Selecionar",
"selectAll": "Selecionar tudo",
"selectVisible": "Selecionar visíveis",

View File

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

View File

@@ -119,6 +119,7 @@
"downloadPending": "待處理",
"downloadQueue": "下載佇列",
"customDownloadFolder": "自訂下載資料夾",
"retry": "重試下載",
"autoFolderPlaceholder": "自動資料夾(依中繼資料)",
"autoFolderHint": "自動資料夾會由中繼資料建立。",
"useAutoFolder": "使用自動資料夾",
@@ -158,6 +159,16 @@
"progress": "進度",
"showDetails": "顯示詳情",
"hideDetails": "隱藏詳細信息",
"viewLogs": "查看日誌",
"detailsTab": "詳細資料",
"logsTab": "日誌",
"logs": {
"live": "即時日誌",
"history": "已儲存日誌",
"command": "yt-dlp 指令",
"empty": "尚無日誌。",
"scrollPaused": "捲動已暫停"
},
"selectAudioFormat": "選擇音訊格式",
"selectDownloadType": "選擇下載類型",
"selectFormat": "選擇格式",
@@ -269,6 +280,8 @@
"openInBrowser": "點擊在瀏覽器中開啟",
"removeAction": "移除",
"removeItem": "移除項目",
"deleteFile": "刪除檔案",
"deleteRecord": "從列表中移除",
"select": "選取",
"selectAll": "全選",
"selectVisible": "選取可見項目",

View File

@@ -119,6 +119,7 @@
"downloadPending": "待处理",
"downloadQueue": "下载队列",
"customDownloadFolder": "自定义下载文件夹",
"retry": "重试下载",
"autoFolderPlaceholder": "自动文件夹(基于元数据)",
"autoFolderHint": "自动文件夹由元数据创建。",
"useAutoFolder": "使用自动文件夹",
@@ -158,6 +159,16 @@
"progress": "进度",
"showDetails": "显示详情",
"hideDetails": "隐藏详细信息",
"viewLogs": "查看日志",
"detailsTab": "详情",
"logsTab": "日志",
"logs": {
"live": "实时日志",
"history": "已保存日志",
"command": "yt-dlp 命令",
"empty": "暂无日志。",
"scrollPaused": "滚动已暂停"
},
"selectAudioFormat": "选择音频格式",
"selectDownloadType": "选择下载类型",
"selectFormat": "选择格式",
@@ -269,6 +280,8 @@
"openInBrowser": "点击在浏览器中打开",
"removeAction": "移除",
"removeItem": "移除项目",
"deleteFile": "删除文件",
"deleteRecord": "从列表中移除",
"select": "选择",
"selectAll": "全选",
"selectVisible": "选择可见项",

View File

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

View File

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