refactor: streamline download path handling and remove unused output path logic

This commit is contained in:
Nexmoe
2025-10-24 19:51:25 +08:00
parent 4dfac6bb34
commit 0821aea72b
8 changed files with 126 additions and 495 deletions

View File

@@ -231,6 +231,7 @@ class DownloadEngine extends EventEmitter {
}
const createdAt = Date.now()
const settings = settingsManager.getAll()
const item: DownloadItem = {
id,
@@ -247,7 +248,7 @@ class DownloadEngine extends EventEmitter {
title: item.title,
status: 'pending',
downloadedAt: createdAt,
outputPath: options.outputPath
downloadPath: settings.downloadPath
})
}
@@ -255,7 +256,7 @@ class DownloadEngine extends EventEmitter {
scopedLoggers.download.info('Starting download execution for ID:', id, 'URL:', options.url)
const ytdlp = ytdlpManager.getInstance()
const settings = settingsManager.getAll()
const downloadPath = options.outputPath || settings.downloadPath
const downloadPath = settings.downloadPath
// Set environment variables for proper encoding on Windows
if (process.platform === 'win32') {
@@ -408,39 +409,8 @@ class DownloadEngine extends EventEmitter {
}
)
// Handle yt-dlp events to capture output file path and format info
let actualOutputPath: string | null = null
// Handle yt-dlp events to capture format info
ytdlpProcess.on('ytDlpEvent', (eventType: string, eventData: string) => {
// Look for download destination messages
scopedLoggers.download.info('ytDlpEvent:', eventType, eventData)
if (eventType === 'download' && eventData.includes('Destination:')) {
const match = eventData.match(/Destination:\s*(.+)/)
if (match?.[1]) {
actualOutputPath = match[1].trim()
}
}
// Also look for other output path patterns
if (
eventType === 'download' &&
(eventData.includes('has already been downloaded') ||
eventData.includes('has already been downloaded'))
) {
const match = eventData.match(/\[download\]\s*(.+?)\s+has already been downloaded/)
if (match?.[1]) {
actualOutputPath = match[1].trim()
}
}
// Look for final output path in download messages
if (eventType === 'download' && eventData.includes('[download] 100%')) {
const match = eventData.match(/\[download\]\s*100%.*?of\s*(.+?)\s+at/)
if (match?.[1]) {
actualOutputPath = match[1].trim()
}
}
// Look for format selection messages
if (eventType === 'info' && eventData.includes('format')) {
// Extract format info from yt-dlp output
@@ -484,31 +454,15 @@ class DownloadEngine extends EventEmitter {
this.queue.downloadCompleted(id)
if (code === 0) {
// Use actual output path from yt-dlp, or fallback to simple generated path
let finalOutputPath: string
if (actualOutputPath) {
finalOutputPath = actualOutputPath
scopedLoggers.download.info(
'Using actual output path from yt-dlp for ID:',
id,
'Path:',
finalOutputPath
)
} else {
// Simple fallback: generate path based on video title and format
const title = videoInfo?.title || 'Unknown'
const sanitizedTitle = title.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50)
const extension =
options.type === 'audio' ? options.extractFormat || 'mp3' : actualFormat || 'mp4'
const fileName = `${sanitizedTitle}.${extension}`
finalOutputPath = path.join(downloadPath, fileName)
scopedLoggers.download.warn(
'Using fallback output path for ID:',
id,
'Path:',
finalOutputPath
)
}
// Generate file path using downloadPath + title + ext
const title = videoInfo?.title || 'Unknown'
const sanitizedTitle = title.replace(/[<>:"/\\|?*]/g, '_').substring(0, 50)
const extension =
options.type === 'audio' ? options.extractFormat || 'mp3' : actualFormat || 'mp4'
const fileName = `${sanitizedTitle}.${extension}`
const finalOutputPath = path.join(downloadPath, fileName)
scopedLoggers.download.info('Generated file path for ID:', id, 'Path:', finalOutputPath)
let fileSize: number | undefined
try {
@@ -529,7 +483,6 @@ class DownloadEngine extends EventEmitter {
this.updateDownloadInfo(id, {
status: 'completed',
outputPath: finalOutputPath,
completedAt: Date.now(),
fileSize,
format: actualFormat || undefined,
@@ -538,7 +491,7 @@ class DownloadEngine extends EventEmitter {
})
scopedLoggers.download.info('Download completed successfully for ID:', id)
this.emit('download-completed', id)
this.addToHistory(id, options, 'completed', undefined, finalOutputPath)
this.addToHistory(id, options, 'completed', undefined)
} else {
scopedLoggers.download.error(
'Download failed with exit code for ID:',
@@ -617,9 +570,6 @@ class DownloadEngine extends EventEmitter {
if (updates.duration !== undefined) {
historyUpdates.duration = updates.duration
}
if (updates.outputPath !== undefined) {
historyUpdates.outputPath = updates.outputPath
}
if (updates.fileSize !== undefined) {
historyUpdates.fileSize = updates.fileSize
}
@@ -669,20 +619,17 @@ class DownloadEngine extends EventEmitter {
id: string,
options: DownloadOptions,
status: DownloadHistoryItem['status'],
error?: string,
actualOutputPath?: string
error?: string
): void {
// Get the download item from the queue to get additional info
const completedDownload = this.queue.getCompletedDownload(id)
scopedLoggers.download.info('Completed download:', completedDownload)
const completedAt = Date.now()
const finalOutputPath = actualOutputPath || options.outputPath
this.upsertHistoryEntry(id, options, {
title: completedDownload?.item.title || `Download ${id}`,
thumbnail: completedDownload?.item.thumbnail,
status,
outputPath: finalOutputPath,
completedAt,
error,
duration: completedDownload?.item.duration,
@@ -711,7 +658,7 @@ class DownloadEngine extends EventEmitter {
thumbnail: updates.thumbnail,
type: options.type,
status: updates.status || 'pending',
outputPath: updates.outputPath,
downloadPath: updates.downloadPath,
fileSize: updates.fileSize,
duration: updates.duration,
downloadedAt: updates.downloadedAt ?? Date.now(),

View File

@@ -1,5 +1,4 @@
import { execFile } from 'node:child_process'
import type { Dirent } from 'node:fs'
import fs from 'node:fs/promises'
import os from 'node:os'
import path from 'node:path'
@@ -61,12 +60,6 @@ class FileSystemService extends IpcService {
}
if (stats?.isDirectory()) {
const candidate = await this.findLikelyFile(normalizedPath, normalizedPath)
if (candidate) {
shell.showItemInFolder(candidate)
return true
}
const result = await shell.openPath(normalizedPath)
if (result) {
console.error('Failed to open directory:', result)
@@ -75,43 +68,25 @@ class FileSystemService extends IpcService {
return true
}
const fallbackDirectory = path.dirname(normalizedPath)
const fallbackCandidate = await this.findLikelyFile(fallbackDirectory, normalizedPath)
if (fallbackCandidate) {
shell.showItemInFolder(fallbackCandidate)
// If the exact path doesn't exist, try to open the parent directory
const parentDirectory = path.dirname(normalizedPath)
const parentStats = await fs.stat(parentDirectory).catch(() => null)
if (parentStats?.isDirectory()) {
const result = await shell.openPath(parentDirectory)
if (result) {
console.error('Failed to open parent directory:', result)
return false
}
return true
}
const result = await shell.openPath(fallbackDirectory)
if (!result) {
return true
}
console.error('Failed to open directory:', result)
console.error('File or directory does not exist:', normalizedPath)
return false
} catch (error) {
try {
const directory = path.dirname(path.normalize(this.sanitizePath(filePath)))
const dirStats = await fs.stat(directory)
if (dirStats.isDirectory()) {
const fallbackCandidate = await this.findLikelyFile(directory, directory)
if (fallbackCandidate) {
shell.showItemInFolder(fallbackCandidate)
return true
}
const result = await shell.openPath(directory)
if (result) {
console.error('Failed to open directory:', result)
return false
}
return true
}
} catch (dirError) {
console.error('Failed to open parent directory:', dirError)
}
console.error('Failed to open file location:', error)
return false
}
return false
}
@IpcMethod()
@@ -156,54 +131,6 @@ class FileSystemService extends IpcService {
}
}
private async findLikelyFile(directory: string, expectedPath: string): Promise<string | null> {
try {
const dirStats = await fs.stat(directory)
if (!dirStats.isDirectory()) {
return null
}
const entries = await fs.readdir(directory, { withFileTypes: true })
const files = entries.filter((entry: Dirent) => entry.isFile())
if (files.length === 0) {
return null
}
const expectedBase = path.basename(expectedPath).toLowerCase()
const expectedName = path.parse(expectedPath).name.toLowerCase()
const exactMatch = files.find((entry) => entry.name.toLowerCase() === expectedBase)
if (exactMatch) {
return path.join(directory, exactMatch.name)
}
if (expectedName) {
const partialMatch = files.find((entry) => entry.name.toLowerCase().includes(expectedName))
if (partialMatch) {
return path.join(directory, partialMatch.name)
}
}
let latestMatch: { filePath: string; mtimeMs: number } | null = null
for (const entry of files) {
const candidatePath = path.join(directory, entry.name)
try {
const candidateStats = await fs.stat(candidatePath)
if (!latestMatch || candidateStats.mtimeMs > latestMatch.mtimeMs) {
latestMatch = { filePath: candidatePath, mtimeMs: candidateStats.mtimeMs }
}
} catch (statError) {
console.error('Failed to stat candidate file:', statError)
}
}
return latestMatch?.filePath ?? null
} catch (error) {
console.error('Failed to search for matching file:', error)
return null
}
}
@IpcMethod()
async openExternal(_context: IpcContext, url: string): Promise<boolean> {
try {
@@ -288,6 +215,58 @@ class FileSystemService extends IpcService {
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;')
}
@IpcMethod()
async deleteFile(_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((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code === 'ENOENT') {
return null
}
throw error
})
if (!stats) {
return false
}
if (stats.isFile()) {
await fs.unlink(normalizedPath).catch((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code !== 'ENOENT') {
throw error
}
})
return true
}
if (stats.isDirectory()) {
const entries = await fs.readdir(normalizedPath)
if (entries.length === 0) {
await fs.rmdir(normalizedPath).catch((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code !== 'ENOENT') {
throw error
}
})
return true
}
}
return false
} catch (error) {
console.error('Failed to delete file:', error)
return false
}
}
}
export { FileSystemService }

View File

@@ -1,9 +1,6 @@
import fs from 'node:fs/promises'
import path from 'node:path'
import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
import type { DownloadHistoryItem } from '../../../shared/types'
import { historyManager } from '../../lib/history-manager'
import { settingsManager } from '../../settings'
class HistoryService extends IpcService {
static readonly groupName = 'history'
@@ -24,282 +21,10 @@ class HistoryService extends IpcService {
}
@IpcMethod()
async removeHistoryItem(_context: IpcContext, id: string, outputPath?: string): Promise<boolean> {
const record = historyManager.getHistoryById(id)
await this.deleteOutputResource(record, outputPath)
removeHistoryItem(_context: IpcContext, id: string): boolean {
return historyManager.removeHistoryItem(id)
}
private async deleteOutputResource(
record: DownloadHistoryItem | undefined,
fallbackOutputPath?: string
): Promise<void> {
try {
const candidatePaths = new Set<string>()
if (record?.outputPath) {
candidatePaths.add(record.outputPath)
}
if (fallbackOutputPath) {
candidatePaths.add(fallbackOutputPath)
}
for (const candidate of candidatePaths) {
if (await this.tryDeletePath(candidate)) {
return
}
}
const directories = this.collectCandidateDirectories(candidatePaths)
if (directories.length === 0) {
const defaultDownloadPath = settingsManager.get('downloadPath')
if (defaultDownloadPath) {
directories.push(defaultDownloadPath)
}
}
const matchKeys = this.collectMatchKeys(record, candidatePaths)
if (matchKeys.length === 0) {
return
}
const extensions = this.collectExtensions(candidatePaths, record)
for (const directory of directories) {
const matchedPath = await this.findMatchingFile(directory, matchKeys, extensions, record)
if (matchedPath && (await this.tryDeletePath(matchedPath))) {
return
}
}
} catch (error) {
console.error('Failed to delete output resource:', error)
}
}
private sanitizePath(target: string): string {
return target.trim().replace(/^['"]|['"]$/g, '')
}
private normalizeMatchString(value?: string): string {
if (!value) {
return ''
}
return value
.normalize('NFKC')
.toLowerCase()
.replace(/[^\p{L}\p{N}]+/gu, '')
}
private collectCandidateDirectories(candidatePaths: Set<string>): string[] {
const directories = new Set<string>()
for (const candidate of candidatePaths) {
const sanitized = this.sanitizePath(candidate)
if (!sanitized) continue
const normalized = path.normalize(sanitized)
if (!path.isAbsolute(normalized)) continue
const directory = path.dirname(normalized)
if (directory) {
directories.add(directory)
}
}
return Array.from(directories)
}
private collectExtensions(
candidatePaths: Set<string>,
record: DownloadHistoryItem | undefined
): Set<string> {
const extensions = new Set<string>()
const addExtension = (ext?: string) => {
if (!ext) {
return
}
const trimmed = ext.trim()
if (!trimmed) {
return
}
const normalized = trimmed.startsWith('.')
? trimmed.toLowerCase()
: `.${trimmed.toLowerCase()}`
extensions.add(normalized)
}
for (const candidate of candidatePaths) {
const sanitized = this.sanitizePath(candidate)
if (!sanitized) continue
addExtension(path.extname(sanitized))
}
addExtension(record?.outputPath ? path.extname(record.outputPath) : undefined)
addExtension(record?.selectedFormat?.ext)
addExtension(record?.selectedFormat?.video_ext)
addExtension(record?.selectedFormat?.audio_ext)
if (extensions.size === 0) {
const fallback =
record?.type === 'audio'
? ['.mp3', '.m4a', '.aac', '.ogg', '.opus', '.flac', '.wav']
: ['.mp4', '.mkv', '.webm', '.mov', '.avi']
for (const ext of fallback) {
extensions.add(ext)
}
}
return extensions
}
private collectMatchKeys(
record: DownloadHistoryItem | undefined,
candidatePaths: Set<string>
): string[] {
const keys = new Set<string>()
const fallbackKeys: string[] = []
const tryAddKey = (value?: string) => {
if (!value) return
const normalized = this.normalizeMatchString(value)
if (!normalized) return
if (normalized.length >= 3) {
keys.add(normalized)
} else {
fallbackKeys.push(normalized)
}
}
for (const candidate of candidatePaths) {
const sanitized = this.sanitizePath(candidate)
if (!sanitized) continue
const baseName = path.parse(sanitized).name
tryAddKey(baseName)
}
tryAddKey(record?.title)
tryAddKey(record?.id)
if (keys.size === 0) {
for (const key of fallbackKeys) {
keys.add(key)
}
}
return Array.from(keys)
}
private async findMatchingFile(
directory: string,
matchKeys: string[],
extensions: Set<string>,
record: DownloadHistoryItem | undefined
): Promise<string | null> {
try {
const dirStats = await fs.stat(directory).catch((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code === 'ENOENT') {
return null
}
throw error
})
if (!dirStats || !dirStats.isDirectory()) {
return null
}
const entries = await fs.readdir(directory, { withFileTypes: true })
const matches: Array<{ path: string; diff: number }> = []
const targetTimestamp = record?.completedAt ?? record?.downloadedAt
const maxDiff = 10 * 60 * 1000 // 10 minutes
for (const entry of entries) {
if (!entry.isFile()) continue
const entryPath = path.join(directory, entry.name)
const entryExt = path.extname(entry.name).toLowerCase()
if (extensions.size > 0 && entryExt && !extensions.has(entryExt)) {
continue
}
const normalizedName = this.normalizeMatchString(entry.name)
if (!normalizedName && matchKeys.length > 0) continue
const hasMatchKeys = matchKeys.length > 0
const isMatch = hasMatchKeys
? matchKeys.some((key) => key && normalizedName.includes(key))
: true
if (!isMatch) continue
const stats = await fs.stat(entryPath).catch(() => null)
if (!stats) continue
if (targetTimestamp) {
const diff = Math.abs(stats.mtimeMs - targetTimestamp)
if (diff > maxDiff) {
continue
}
matches.push({ path: entryPath, diff })
} else {
if (!hasMatchKeys) {
continue
}
matches.push({ path: entryPath, diff: Number.POSITIVE_INFINITY })
}
}
if (matches.length === 0) {
return null
}
matches.sort((a, b) => a.diff - b.diff)
return matches[0]?.path ?? null
} catch (error) {
console.error('Failed to search for matching file:', error)
return null
}
}
private async tryDeletePath(rawPath: string): Promise<boolean> {
const sanitizedPath = this.sanitizePath(rawPath)
if (!sanitizedPath) return false
const normalizedPath = path.normalize(sanitizedPath)
const stats = await fs.stat(normalizedPath).catch((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code === 'ENOENT') {
return null
}
throw error
})
if (!stats) {
return false
}
if (stats.isFile()) {
await fs.unlink(normalizedPath).catch((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code !== 'ENOENT') {
throw error
}
})
return true
}
if (stats.isDirectory()) {
const entries = await fs.readdir(normalizedPath)
if (entries.length === 0) {
await fs.rmdir(normalizedPath).catch((error) => {
const err = error as NodeJS.ErrnoException
if (err?.code !== 'ENOENT') {
throw error
}
})
return true
}
}
return false
}
@IpcMethod()
getHistoryCount(_context: IpcContext): {
active: number

View File

@@ -3,18 +3,8 @@ import { Button } from '@renderer/components/ui/button'
import { ImageWithPlaceholder } from '@renderer/components/ui/image-with-placeholder'
import { Progress } from '@renderer/components/ui/progress'
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip'
import { useSetAtom } from 'jotai'
import {
AlertCircle,
CheckCircle2,
Copy,
ExternalLink,
FolderOpen,
Loader2,
Play,
Trash2,
X
} from 'lucide-react'
import { useAtomValue, useSetAtom } from 'jotai'
import { AlertCircle, CheckCircle2, Copy, FolderOpen, Loader2, Play, Trash2, X } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { toast } from 'sonner'
import { useCachedThumbnail } from '../../hooks/use-cached-thumbnail'
@@ -24,6 +14,14 @@ import {
removeDownloadAtom,
removeHistoryRecordAtom
} from '../../store/downloads'
import { settingsAtom } from '../../store/settings'
// Helper function to generate file path with proper path separators
const generateFilePath = (downloadPath: string, title: string, format: string): string => {
const fileName = `${title}.${format}`
// Use proper path joining for cross-platform compatibility
return `${downloadPath}/${fileName}`.replace(/\//g, '\\')
}
interface DownloadItemProps {
download: DownloadRecord
@@ -54,6 +52,7 @@ const formatDate = (timestamp?: number) => {
export function DownloadItem({ download }: DownloadItemProps) {
const { t } = useTranslation()
const settings = useAtomValue(settingsAtom)
const removeDownload = useSetAtom(removeDownloadAtom)
const removeHistory = useSetAtom(removeHistoryRecordAtom)
const isHistory = download.entryType === 'history'
@@ -76,14 +75,14 @@ export function DownloadItem({ download }: DownloadItemProps) {
}
}
const handleOpenFileLocation = async () => {
if (!download.outputPath) {
toast.error(t('notifications.openFolderFailed'))
return
}
const handleOpenFolder = async () => {
try {
const success = await ipcServices.fs.openFileLocation(download.outputPath)
// Generate file path using downloadPath + title + ext
const downloadPath = download.downloadPath || settings.downloadPath
const format = download.format || (download.type === 'audio' ? 'mp3' : 'mp4')
const filePath = generateFilePath(downloadPath, download.title, format)
const success = await ipcServices.fs.openFileLocation(filePath)
if (!success) {
toast.error(t('notifications.openFolderFailed'))
}
@@ -92,33 +91,20 @@ export function DownloadItem({ download }: DownloadItemProps) {
toast.error(t('notifications.openFolderFailed'))
}
}
const handleOpenFile = async () => {
if (!download.outputPath) {
toast.error(t('notifications.openFileFailed'))
return
}
try {
await ipcServices.fs.openFileLocation(download.outputPath)
} catch (error) {
console.error('Failed to open file:', error)
toast.error(t('notifications.openFileFailed'))
}
}
const handleOpenFolder = async () => {
await handleOpenFileLocation()
}
// need title, downloadPath, format
const handleCopyToClipboard = async () => {
if (!download.outputPath) {
if (!download.title || !download.downloadPath || !download.format) {
toast.error(t('notifications.copyFailed'))
return
}
try {
const success = await ipcServices.fs.copyFileToClipboard(download.outputPath)
// Generate file path using downloadPath + title + ext
const downloadPath = download.downloadPath
const format = download.format
const filePath = generateFilePath(downloadPath, download.title, format)
const success = await ipcServices.fs.copyFileToClipboard(filePath)
if (!success) {
toast.error(t('notifications.copyFailed'))
return
@@ -130,10 +116,21 @@ export function DownloadItem({ download }: DownloadItemProps) {
}
}
// need id
const handleRemoveHistory = async () => {
if (!isHistory) return
try {
await ipcServices.history.removeHistoryItem(download.id, download.outputPath)
// Generate file path using downloadPath + title + ext
const downloadPath = download.downloadPath || settings.downloadPath
const format = download.format || (download.type === 'audio' ? 'mp3' : 'mp4')
const filePath = generateFilePath(downloadPath, download.title, format)
// Remove from history first
await ipcServices.history.removeHistoryItem(download.id)
// Then try to delete the file
await ipcServices.fs.deleteFile(filePath)
removeHistory(download.id)
toast.success(t('notifications.itemRemoved'))
} catch (error) {
@@ -152,7 +149,7 @@ export function DownloadItem({ download }: DownloadItemProps) {
case 'processing':
return <Loader2 className="h-4 w-4 animate-spin text-primary" />
case 'pending':
return <Loader2 className="h-4 w-4 text-muted-foreground" />
return <Loader2 className="h-4 w-4 animate-spin text-muted-foreground" />
case 'cancelled':
return <X className="h-4 w-4 text-muted-foreground" />
default:
@@ -290,7 +287,7 @@ export function DownloadItem({ download }: DownloadItemProps) {
<div className={actionsContainerClass}>
{isHistory ? (
<>
{download.outputPath && (
{download.status === 'completed' && (
<>
<Tooltip>
<TooltipTrigger asChild>
@@ -299,6 +296,7 @@ export function DownloadItem({ download }: DownloadItemProps) {
size="icon"
className="h-8 w-8 shrink-0"
onClick={handleCopyToClipboard}
disabled={!download.title || !download.downloadPath || !download.format}
>
<Copy className="h-4 w-4" />
</Button>
@@ -342,23 +340,8 @@ export function DownloadItem({ download }: DownloadItemProps) {
</>
) : (
<>
{download.status === 'completed' && download.outputPath && (
{download.status === 'completed' && (
<>
<Tooltip>
<TooltipTrigger asChild>
<Button
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0"
onClick={handleOpenFile}
>
<ExternalLink className="h-4 w-4" />
</Button>
</TooltipTrigger>
<TooltipContent>
<p>{t('history.openFile')}</p>
</TooltipContent>
</Tooltip>
<Tooltip>
<TooltipTrigger asChild>
<Button

View File

@@ -185,7 +185,6 @@
"openFileLocation": "Open File Location",
"openFolder": "Open Folder",
"openDownloadFolder": "Open Download Folder",
"outputPath": "Output Path",
"removeItem": "Remove Item",
"stats": {
"cancelled": "Cancelled",

View File

@@ -176,7 +176,6 @@
"openFileLocation": "打开文件位置",
"openFolder": "打开文件夹",
"openDownloadFolder": "打开下载文件夹",
"outputPath": "输出路径",
"removeItem": "移除项目",
"stats": {
"cancelled": "已取消",

View File

@@ -4,6 +4,7 @@ import type { DownloadHistoryItem, DownloadItem } from '../../../shared/types'
export type DownloadRecord = DownloadItem & {
entryType: 'active' | 'history'
downloadedAt?: number
downloadPath?: string
}
const recordKey = (entryType: DownloadRecord['entryType'], id: string) => `${entryType}:${id}`
@@ -22,7 +23,7 @@ const toHistoryRecord = (item: DownloadHistoryItem): DownloadRecord => ({
status: item.status,
progress: undefined,
error: item.error,
outputPath: item.outputPath,
downloadPath: item.downloadPath,
speed: undefined,
duration: item.duration,
fileSize: item.fileSize,

View File

@@ -54,7 +54,6 @@ export interface DownloadItem {
status: DownloadStatus
progress?: DownloadProgress
error?: string
outputPath?: string
speed?: string
// Enhanced video information
duration?: number
@@ -83,7 +82,7 @@ export interface DownloadHistoryItem {
thumbnail?: string
type: 'video' | 'audio' | 'extract'
status: DownloadStatus
outputPath?: string
downloadPath?: string
fileSize?: number
duration?: number
downloadedAt: number
@@ -113,7 +112,6 @@ export interface DownloadOptions {
startTime?: string
endTime?: string
downloadSubs?: boolean
outputPath?: string
}
export interface PlaylistInfo {