feat(subscriptions): follow one-click defaults (#91)
This commit is contained in:
@@ -4,6 +4,10 @@ import log from 'electron-log/main'
|
||||
import Parser from 'rss-parser'
|
||||
import type { SubscriptionFeedItem, SubscriptionRule } from '../../shared/types'
|
||||
import { DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE } from '../../shared/types'
|
||||
import {
|
||||
buildAudioFormatPreference,
|
||||
buildVideoFormatPreference
|
||||
} from '../../shared/utils/format-preferences'
|
||||
import { settingsManager } from '../settings'
|
||||
import { downloadEngine } from './download-engine'
|
||||
import { historyManager } from './history-manager'
|
||||
@@ -481,6 +485,11 @@ export class SubscriptionScheduler extends EventEmitter {
|
||||
const downloadDirectory = subscription.downloadDirectory?.trim() || settings.downloadPath
|
||||
const namingTemplate =
|
||||
subscription.namingTemplate?.trim() || DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE
|
||||
const downloadType = settings.oneClickDownloadType ?? 'video'
|
||||
const formatPreference =
|
||||
downloadType === 'video'
|
||||
? buildVideoFormatPreference(settings)
|
||||
: buildAudioFormatPreference(settings)
|
||||
ensureDirectoryExists(downloadDirectory)
|
||||
|
||||
const tags = Array.from(new Set([subscription.platform, ...subscription.tags]))
|
||||
@@ -488,7 +497,8 @@ export class SubscriptionScheduler extends EventEmitter {
|
||||
try {
|
||||
downloadEngine.startDownload(downloadId, {
|
||||
url,
|
||||
type: 'video',
|
||||
type: downloadType,
|
||||
format: formatPreference,
|
||||
customDownloadPath: downloadDirectory,
|
||||
customFilenameTemplate: namingTemplate,
|
||||
tags,
|
||||
|
||||
@@ -14,8 +14,11 @@ import {
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@renderer/components/ui/tabs'
|
||||
|
||||
import { cn } from '@renderer/lib/utils'
|
||||
|
||||
import type { AppSettings, OneClickQualityPreset, PlaylistInfo, VideoFormat } from '@shared/types'
|
||||
import type { PlaylistInfo, VideoFormat } from '@shared/types'
|
||||
import {
|
||||
buildAudioFormatPreference,
|
||||
buildVideoFormatPreference
|
||||
} from '@shared/utils/format-preferences'
|
||||
import { useAtom, useSetAtom } from 'jotai'
|
||||
import { AlertCircle, FolderOpen, List, Loader2, Plus, Video } from 'lucide-react'
|
||||
import { useCallback, useEffect, useId, useMemo, useState } from 'react'
|
||||
@@ -37,34 +40,6 @@ import {
|
||||
} from '../../store/video'
|
||||
import { VideoInfoCard, type VideoInfoCardState } from '../video/VideoInfoCard'
|
||||
|
||||
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
||||
best: null,
|
||||
good: 1080,
|
||||
normal: 720,
|
||||
bad: 480,
|
||||
worst: 360
|
||||
}
|
||||
|
||||
const qualityPresetToAudioAbr: Record<OneClickQualityPreset, number | null> = {
|
||||
best: 320,
|
||||
good: 256,
|
||||
normal: 192,
|
||||
bad: 128,
|
||||
worst: 96
|
||||
}
|
||||
|
||||
const dedupe = (candidates: Array<string | undefined>): string[] => {
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) continue
|
||||
if (seen.has(candidate)) continue
|
||||
seen.add(candidate)
|
||||
result.push(candidate)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const isLikelyUrl = (value: string): boolean => {
|
||||
try {
|
||||
const parsed = new URL(value)
|
||||
@@ -74,19 +49,6 @@ const isLikelyUrl = (value: string): boolean => {
|
||||
}
|
||||
}
|
||||
|
||||
const getQualityPreset = (settings: AppSettings): OneClickQualityPreset =>
|
||||
settings.oneClickQuality ?? 'best'
|
||||
|
||||
const buildAudioSelectors = (preset: OneClickQualityPreset): string[] => {
|
||||
if (preset === 'worst') {
|
||||
return dedupe(['worstaudio', 'bestaudio'])
|
||||
}
|
||||
|
||||
const abrLimit = qualityPresetToAudioAbr[preset]
|
||||
// Prefer audio-only selectors so video+audio merges remain valid.
|
||||
return dedupe([abrLimit ? `bestaudio[abr<=${abrLimit}]` : undefined, 'bestaudio'])
|
||||
}
|
||||
|
||||
const isAudioOnlyFormat = (format: VideoFormat): boolean =>
|
||||
!!format.acodec && format.acodec !== 'none' && (!format.video_ext || format.video_ext === 'none')
|
||||
|
||||
@@ -140,49 +102,6 @@ const pickBestAudioFormatsByLanguage = (formats: VideoFormat[]): string[] => {
|
||||
})
|
||||
.filter((id): id is string => !!id)
|
||||
}
|
||||
|
||||
const buildVideoFormatPreference = (settings: AppSettings): string => {
|
||||
const preset = getQualityPreset(settings)
|
||||
|
||||
if (preset === 'worst') {
|
||||
// Prefer separate streams, then fall back to single-file selectors.
|
||||
return 'worstvideo+worstaudio/worst/best'
|
||||
}
|
||||
|
||||
const maxHeight = qualityPresetToVideoHeight[preset]
|
||||
const videoCandidates = dedupe([
|
||||
maxHeight ? `bestvideo[height<=${maxHeight}]` : undefined,
|
||||
'bestvideo'
|
||||
])
|
||||
|
||||
const audioSelectors = buildAudioSelectors(preset)
|
||||
const combinations: string[] = []
|
||||
|
||||
for (const video of videoCandidates) {
|
||||
for (const audio of audioSelectors) {
|
||||
combinations.push(`${video}+${audio}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (audioSelectors.includes('none')) {
|
||||
for (const video of videoCandidates) {
|
||||
combinations.push(video)
|
||||
}
|
||||
} else {
|
||||
// Prefer merged formats, then allow 'best' as a compatibility fallback.
|
||||
combinations.push('bestvideo+bestaudio')
|
||||
}
|
||||
|
||||
combinations.push('best')
|
||||
|
||||
return dedupe(combinations).join('/')
|
||||
}
|
||||
|
||||
const buildAudioFormatPreference = (settings: AppSettings): string => {
|
||||
const selectors = buildAudioSelectors(getQualityPreset(settings))
|
||||
return dedupe([...selectors, 'best']).join('/')
|
||||
}
|
||||
|
||||
interface DownloadDialogProps {
|
||||
onOpenSupportedSites?: () => void
|
||||
onOpenSettings?: () => void
|
||||
|
||||
81
src/shared/utils/format-preferences.ts
Normal file
81
src/shared/utils/format-preferences.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import type { AppSettings, OneClickQualityPreset } from '../types'
|
||||
|
||||
const qualityPresetToVideoHeight: Record<OneClickQualityPreset, number | null> = {
|
||||
best: null,
|
||||
good: 1080,
|
||||
normal: 720,
|
||||
bad: 480,
|
||||
worst: 360
|
||||
}
|
||||
|
||||
const qualityPresetToAudioAbr: Record<OneClickQualityPreset, number | null> = {
|
||||
best: 320,
|
||||
good: 256,
|
||||
normal: 192,
|
||||
bad: 128,
|
||||
worst: 96
|
||||
}
|
||||
|
||||
const dedupe = (candidates: Array<string | undefined>): string[] => {
|
||||
const seen = new Set<string>()
|
||||
const result: string[] = []
|
||||
for (const candidate of candidates) {
|
||||
if (!candidate) continue
|
||||
if (seen.has(candidate)) continue
|
||||
seen.add(candidate)
|
||||
result.push(candidate)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const getQualityPreset = (settings: AppSettings): OneClickQualityPreset =>
|
||||
settings.oneClickQuality ?? 'best'
|
||||
|
||||
const buildAudioSelectors = (preset: OneClickQualityPreset): string[] => {
|
||||
if (preset === 'worst') {
|
||||
return dedupe(['worstaudio', 'bestaudio'])
|
||||
}
|
||||
|
||||
const abrLimit = qualityPresetToAudioAbr[preset]
|
||||
return dedupe([abrLimit ? `bestaudio[abr<=${abrLimit}]` : undefined, 'bestaudio'])
|
||||
}
|
||||
|
||||
export const buildVideoFormatPreference = (settings: AppSettings): string => {
|
||||
const preset = getQualityPreset(settings)
|
||||
|
||||
if (preset === 'worst') {
|
||||
return 'worstvideo+worstaudio/worst/best'
|
||||
}
|
||||
|
||||
const maxHeight = qualityPresetToVideoHeight[preset]
|
||||
const videoCandidates = dedupe([
|
||||
maxHeight ? `bestvideo[height<=${maxHeight}]` : undefined,
|
||||
'bestvideo'
|
||||
])
|
||||
|
||||
const audioSelectors = buildAudioSelectors(preset)
|
||||
const combinations: string[] = []
|
||||
|
||||
for (const video of videoCandidates) {
|
||||
for (const audio of audioSelectors) {
|
||||
combinations.push(`${video}+${audio}`)
|
||||
}
|
||||
}
|
||||
|
||||
if (audioSelectors.includes('none')) {
|
||||
for (const video of videoCandidates) {
|
||||
combinations.push(video)
|
||||
}
|
||||
} else {
|
||||
combinations.push('bestvideo+bestaudio')
|
||||
}
|
||||
|
||||
combinations.push('best')
|
||||
|
||||
return dedupe(combinations).join('/')
|
||||
}
|
||||
|
||||
export const buildAudioFormatPreference = (settings: AppSettings): string => {
|
||||
const selectors = buildAudioSelectors(getQualityPreset(settings))
|
||||
return dedupe([...selectors, 'best']).join('/')
|
||||
}
|
||||
Reference in New Issue
Block a user