feat(subscriptions): follow one-click defaults (#91)

This commit is contained in:
Nexmoe
2026-01-13 21:41:00 +08:00
committed by GitHub
parent da3643bc79
commit 43cabf65e9
3 changed files with 97 additions and 87 deletions

View File

@@ -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,

View File

@@ -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

View 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('/')
}