/* eslint-disable @typescript-eslint/no-explicit-any */ import parseTorrentName from 'parse-torrent-name'; import { parseStringPromise } from 'xml2js'; import { getConfig, setCachedConfig } from '@/lib/config'; import { getMagnetBaseUrl, universalMagnetFetch } from '@/lib/magnet.client'; import { db, getStorage } from '@/lib/db'; import { EmailService } from '@/lib/email.service'; import { addOpenListOfflineDownload, getOfflineDownloadBasePath, joinOpenListPath, } from '@/lib/openlist-offline-download'; import { AnimeSubscription, AnimeSubscriptionDownloadTool } from '@/types/anime-subscription'; const downloadTools: AnimeSubscriptionDownloadTool[] = ['aria2', 'qBittorrent', 'Transmission']; const pickRssText = (value: any): string => { if (value === undefined || value === null) return ''; const first = Array.isArray(value) ? value[0] : value; if (first === undefined || first === null) return ''; if (typeof first === 'object') return String(first._ ?? first.$?.url ?? first.$?.href ?? ''); return String(first); }; function getAnimeSubscriptionDownloadTool(tool: unknown): AnimeSubscriptionDownloadTool { return typeof tool === 'string' && downloadTools.includes(tool as AnimeSubscriptionDownloadTool) ? tool as AnimeSubscriptionDownloadTool : 'aria2'; } /** * 从标题中提取集数 */ export function extractEpisode(title: string): number | null { const parsed = parseTorrentName(title); if (parsed.episode) { return parsed.episode; } // 备用正则匹配 const patterns = [ /\[(\d+)\]/, // [01] /第(\d+)[集话]/, // 第01集 /EP?(\d+)/i, // EP01, E01 /\s(\d+)\s/, // 空格01空格 ]; for (const pattern of patterns) { const match = title.match(pattern); if (match) { return parseInt(match[1], 10); } } return null; } /** * 解析逗号分隔关键词(兼容中文逗号) */ function parseKeywords(text: string): string[] { return text .replace(/,/g, ',') .split(',') .map((k) => k.trim()) .filter(Boolean); } /** * 检查标题是否匹配过滤条件(包含关键词,AND:必须全部命中) */ export function matchesFilter(title: string, filterText: string): boolean { if (!filterText) return true; // 支持多个关键词,用逗号分隔,必须全部匹配 const keywords = parseKeywords(filterText); return keywords.every((keyword) => title.includes(keyword)); } /** * 检查标题是否命中排除关键词(OR:任一命中即排除) */ export function matchesExclude(title: string, excludeText?: string): boolean { if (!excludeText) return false; const keywords = parseKeywords(excludeText); return keywords.some((keyword) => title.includes(keyword)); } /** * 搜索 ACG 资源(直接调用搜索逻辑,不通过 HTTP) */ export async function searchACG( keyword: string, source: 'acgrip' | 'mikan' | 'dmhy' | 'nyaa' ) { const trimmedKeyword = keyword.trim(); const config = await getConfig(); let searchUrl: string; switch (source) { case 'mikan': { const baseUrl = getMagnetBaseUrl( 'https://mikanani.me', config.SiteConfig.MagnetMikanReverseProxy ); searchUrl = `${baseUrl}/RSS/Search?searchstr=${encodeURIComponent(trimmedKeyword)}`; break; } case 'dmhy': { const baseUrl = getMagnetBaseUrl( 'http://share.dmhy.org', config.SiteConfig.MagnetDmhyReverseProxy ); searchUrl = `${baseUrl}/topics/rss/rss.xml?keyword=${encodeURIComponent(trimmedKeyword)}`; break; } case 'nyaa': { const baseUrl = getMagnetBaseUrl( 'https://nyaa.si', config.SiteConfig.MagnetNyaaReverseProxy ); searchUrl = `${baseUrl}/?page=rss&q=${encodeURIComponent(trimmedKeyword)}&c=1_0&f=0`; break; } case 'acgrip': default: { const baseUrl = getMagnetBaseUrl( 'https://acg.rip', config.SiteConfig.MagnetAcgripReverseProxy ); searchUrl = `${baseUrl}/page/1.xml?term=${encodeURIComponent(trimmedKeyword)}`; break; } } const response = await universalMagnetFetch(searchUrl, config.SiteConfig.MagnetProxy, { headers: { 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36', }, }); if (!response.ok) { throw new Error(`${source} API 请求失败: ${response.status}`); } const xmlData = await response.text(); const parsed = await parseStringPromise(xmlData); if (!parsed?.rss?.channel?.[0]?.item) { return []; } const items = parsed.rss.channel[0].item; // 统一格式。注意:Nyaa RSS 的 link 是 .torrent 下载地址,guid 才是详情页。 return items.map((item: any) => { const title = pickRssText(item.title); const rawLink = pickRssText(item.link); const rawGuid = pickRssText(item.guid); const pubDate = pickRssText(item.pubDate); const description = pickRssText(item.description) || pickRssText(item['content:encoded']); const enclosureUrl = pickRssText(item.enclosure?.[0]?.$?.url) || pickRssText(item.enclosure?.[0]?.$?.href); const isNyaa = source === 'nyaa'; const link = isNyaa ? (rawGuid || rawLink) : rawLink; const torrentUrl = isNyaa ? rawLink : enclosureUrl; const guid = rawGuid || link || torrentUrl || `${title}-${pubDate}`; return { title, link, guid, pubDate, torrentUrl, description, }; }); } /** * 添加离线下载任务 */ export async function addOfflineDownload( torrentUrl: string, downloadPath: string ) { const config = await getConfig(); const downloadTool = getAnimeSubscriptionDownloadTool( config.AnimeSubscriptionConfig?.DownloadTool ); await addOpenListOfflineDownload(config, downloadPath, torrentUrl, downloadTool); } /** * 发送追番更新通知和邮件 */ async function sendAnimeUpdateNotifications( subscription: AnimeSubscription, episodes: number[] ) { const config = await getConfig(); const storage = getStorage(); // 获取站长用户名 - 从用户列表中查找 owner 角色 let ownerUsername: string | null = null; try { const allUsers = await db.getAllUsers(); for (const username of allUsers) { const userInfo = await db.getUserInfoV2(username); if (userInfo?.role === 'owner') { ownerUsername = username; break; } } } catch (error) { console.error('[AnimeSubscription] 获取站长用户名失败:', error); } if (!ownerUsername) { console.warn('[AnimeSubscription] 未找到站长用户,跳过通知'); return; } // 准备通知内容 const episodeList = episodes.join('、'); const notificationTitle = `追番更新:${subscription.title}`; const notificationMessage = `您订阅的番剧《${subscription.title}》有新集数更新:第 ${episodeList} 集,已下载到私人影库`; // 需要通知的用户列表(去重) const usersToNotify: string[] = [ownerUsername]; // 如果创建者不是站长,也通知创建者 if (subscription.createdBy && subscription.createdBy !== ownerUsername) { usersToNotify.push(subscription.createdBy); } // 发送站内通知 for (const username of usersToNotify) { try { await storage.addNotification(username, { id: crypto.randomUUID(), type: 'anime_subscription_update', title: notificationTitle, message: notificationMessage, timestamp: Date.now(), read: false, metadata: { subscriptionId: subscription.id, subscriptionTitle: subscription.title, episodes: episodes, }, }); console.log(`[AnimeSubscription] 已发送站内通知给用户: ${username}`); } catch (error) { console.error(`[AnimeSubscription] 发送站内通知失败 (${username}):`, error); } } // 发送邮件通知(如果已启用) const emailConfig = config.EmailConfig; if (!emailConfig?.enabled) { return; } // 获取需要发送邮件的用户邮箱 const emailsToSend: Array<{ username: string; email: string }> = []; for (const username of usersToNotify) { try { const userInfo = await db.getUserInfoV2(username); // 使用可选的 email 字段 const email = (userInfo as any)?.email; if (email) { emailsToSend.push({ username, email }); } } catch (error) { console.error(`[AnimeSubscription] 获取用户邮箱失败 (${username}):`, error); } } // 发送邮件 for (const { username, email } of emailsToSend) { try { const emailHtml = `
您好,${username}!
您订阅的番剧有新集数更新:
新增集数:第 ${episodeList} 集
搜索源:${subscription.source === 'acgrip' ? 'ACG.RIP' : subscription.source === 'mikan' ? '蜜柑' : subscription.source === 'nyaa' ? 'Nyaa' : '动漫花园'}
这些集数已自动添加到 OpenList 离线下载队列。
此邮件由系统自动发送,请勿回复。