Files
VidBee/src/main/ipc/services/subscription-service.ts
Nexmoe 86e6b93476 Add custom download folders and refine RSS dialog #37 (#44)
* Add custom download folders

* Refine RSS defaults and dialog layout

* Record template folder in download path

* Store template folder in history paths

* Resolve template vars for history paths

* Default RSS directory to Subscriptions
2025-12-20 13:05:12 +08:00

168 lines
4.8 KiB
TypeScript

import path from 'node:path'
import { type IpcContext, IpcMethod, IpcService } from 'electron-ipc-decorator'
import type {
SubscriptionCreatePayload,
SubscriptionResolvedFeed,
SubscriptionRule,
SubscriptionUpdatePayload
} from '../../../shared/types'
import { DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE } from '../../../shared/types'
import { sanitizeFilenameTemplate } from '../../download-engine/args-builder'
import { subscriptionManager } from '../../lib/subscription-manager'
import { subscriptionScheduler } from '../../lib/subscription-scheduler'
import { settingsManager } from '../../settings'
interface CreateSubscriptionOptions {
url: string
keywords?: string[]
tags?: string[]
onlyDownloadLatest?: boolean
downloadDirectory?: string
namingTemplate?: string
enabled?: boolean
}
const ensureUrlHasProtocol = (value: string): string => {
if (!value) {
return value
}
if (!/^https?:\/\//i.test(value)) {
return `https://${value}`
}
return value
}
const resolveFeedFromInput = (rawUrl: string): SubscriptionResolvedFeed => {
const normalized = ensureUrlHasProtocol(rawUrl.trim())
const youTubeChannelMatch = normalized.match(/youtube\.com\/channel\/([A-Za-z0-9_-]+)/i)
if (youTubeChannelMatch) {
return {
sourceUrl: normalized,
feedUrl: `https://www.youtube.com/feeds/videos.xml?channel_id=${youTubeChannelMatch[1]}`,
platform: 'youtube'
}
}
if (/youtube\.com\/feeds\/videos\.xml/i.test(normalized)) {
return {
sourceUrl: normalized,
feedUrl: normalized,
platform: 'youtube'
}
}
const youTubeUserMatch = normalized.match(/youtube\.com\/(?:user|c)\/([^/?]+)/i)
if (youTubeUserMatch) {
return {
sourceUrl: normalized,
feedUrl: `https://www.youtube.com/feeds/videos.xml?user=${youTubeUserMatch[1]}`,
platform: 'youtube'
}
}
const youTubeHandleMatch = normalized.match(/youtube\.com\/(@[^/?]+)/i)
if (youTubeHandleMatch) {
const handle = youTubeHandleMatch[1].replace('@', '')
return {
sourceUrl: normalized,
feedUrl: `https://www.youtube.com/feeds/videos.xml?user=${handle}`,
platform: 'youtube'
}
}
const biliSpaceMatch = normalized.match(/bilibili\.com\/(?:space|user)\/(\d+)/i)
if (biliSpaceMatch) {
return {
sourceUrl: normalized,
feedUrl: `https://rsshub.app/bilibili/user/video/${biliSpaceMatch[1]}`,
platform: 'bilibili'
}
}
if (/rsshub\.app\/bilibili/i.test(normalized)) {
return {
sourceUrl: normalized,
feedUrl: normalized,
platform: 'bilibili'
}
}
return {
sourceUrl: normalized,
feedUrl: normalized,
platform: 'custom'
}
}
class SubscriptionService extends IpcService {
static readonly groupName = 'subscriptions'
@IpcMethod()
list(_context: IpcContext): SubscriptionRule[] {
return subscriptionManager.getAll()
}
@IpcMethod()
resolve(_context: IpcContext, url: string): SubscriptionResolvedFeed {
return resolveFeedFromInput(url)
}
@IpcMethod()
async create(
_context: IpcContext,
options: CreateSubscriptionOptions
): Promise<SubscriptionRule> {
const resolved = resolveFeedFromInput(options.url)
const settings = settingsManager.getAll()
const defaultDownloadDirectory = path.join(settings.downloadPath, 'Subscriptions')
const payload: SubscriptionCreatePayload = {
sourceUrl: resolved.sourceUrl,
feedUrl: resolved.feedUrl,
platform: resolved.platform,
keywords: options.keywords,
tags: options.tags,
onlyDownloadLatest:
options.onlyDownloadLatest ?? settings.subscriptionOnlyLatestDefault ?? true,
downloadDirectory: options.downloadDirectory || defaultDownloadDirectory,
namingTemplate: sanitizeFilenameTemplate(
options.namingTemplate || DEFAULT_SUBSCRIPTION_FILENAME_TEMPLATE
),
enabled: options.enabled ?? true
}
const created = subscriptionManager.add(payload)
void subscriptionScheduler.runNow(created.id)
return created
}
@IpcMethod()
update(
_context: IpcContext,
id: string,
updates: SubscriptionUpdatePayload
): SubscriptionRule | undefined {
const normalized: SubscriptionUpdatePayload = { ...updates }
if (typeof normalized.namingTemplate === 'string') {
normalized.namingTemplate = sanitizeFilenameTemplate(normalized.namingTemplate)
}
return subscriptionManager.update(id, normalized)
}
@IpcMethod()
remove(_context: IpcContext, id: string): boolean {
return subscriptionManager.remove(id)
}
@IpcMethod()
async refresh(_context: IpcContext, id?: string): Promise<void> {
await subscriptionScheduler.runNow(id)
}
@IpcMethod()
async queueItem(_context: IpcContext, id: string, itemId: string): Promise<boolean> {
return subscriptionScheduler.queueItem(id, itemId)
}
}
export { SubscriptionService }