diff --git a/src/server/services/nyaa.ts b/src/server/services/nyaa.ts index 7faec9a..fe147a5 100644 --- a/src/server/services/nyaa.ts +++ b/src/server/services/nyaa.ts @@ -2,7 +2,13 @@ import { XMLParser } from 'fast-xml-parser' import type { NyaaItem } from '../types.js' const NYAA_BASE = 'https://nyaa.si' -const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: '@_' }) + +const parser = new XMLParser({ + ignoreAttributes: false, + attributeNamePrefix: '@_', + // Always treat as an array, even when there is only one result + isArray: (_name, jpath) => jpath === 'rss.channel.item', +}) /** * Build a Nyaa RSS URL from a search query and optional category. @@ -45,28 +51,45 @@ export async function searchNyaa(query: string, category = '1_2'): Promise + // fast-xml-parser uses '#text' for mixed-content nodes + if ('#text' in obj) return String(obj['#text']) + } + return '' +} + function parseItem(item: Record): NyaaItem { - const guid = String(item['guid'] ?? '') - // guid is like https://nyaa.si/view/1234567 - const torrent_id = guid.split('/').pop() ?? guid + // https://nyaa.si/view/1234567 + // fast-xml-parser gives us { "@_isPermaLink": "true", "#text": "https://nyaa.si/view/1234567" } + const guidStr = textOf(item['guid']) + const torrent_id = guidStr.split('/').pop() ?? guidStr - const link = String(item['link'] ?? '') - // link in the RSS feed is the magnet or torrent link; torrent download is /download/.torrent - const torrent_url = torrent_id - ? `${NYAA_BASE}/download/${torrent_id}.torrent` - : link + // In Nyaa RSS, is the direct .torrent download URL: + // https://nyaa.si/download/1234567.torrent + const linkStr = textOf(item['link']) + const torrent_url = linkStr || (torrent_id ? `${NYAA_BASE}/download/${torrent_id}.torrent` : '') - // Nyaa RSS uses nyaa: namespace for extended fields + // Nyaa namespace fields (nyaa:seeders, nyaa:size, etc.) const magnet = item['nyaa:magnetUri'] ?? item['nyaa:magnetLink'] ?? null - const category = String(item['nyaa:category'] ?? item['category'] ?? '') - const size = String(item['nyaa:size'] ?? '') + const category = textOf(item['nyaa:category'] ?? item['category']) + const size = textOf(item['nyaa:size']) const seeders = Number(item['nyaa:seeders'] ?? 0) const leechers = Number(item['nyaa:leechers'] ?? 0) const downloads = Number(item['nyaa:downloads'] ?? 0) return { torrent_id, - title: String(item['title'] ?? ''), + title: textOf(item['title']), torrent_url, magnet_url: magnet ? String(magnet) : null, category, @@ -74,28 +97,28 @@ function parseItem(item: Record): NyaaItem { seeders, leechers, downloads, - published: String(item['pubDate'] ?? ''), + published: textOf(item['pubDate']), } } /** * Parse an episode number from a torrent title. - * Handles common patterns: " - 12", "[12]", "E12", "EP12", " 12 " + * Handles common patterns: " - 12", "[12]", "E12", "EP12", "S01E12" * Returns the matched string or 'unknown'. */ export function parseEpisodeCode(title: string): string { - // Match patterns like " - 12 " or " - 12v2" + // Match " - 12 " or " - 12v2" let m = title.match(/\s-\s(\d{1,4}(?:\.\d)?(?:v\d)?)\s/) if (m) return m[1] - // Match [12] or [12v2] + // Match [12] or [12v2] (but skip hash-like 6+ char hex blocks e.g. [CC3FE38D]) m = title.match(/\[(\d{1,4}(?:\.\d)?(?:v\d)?)\]/) if (m) return m[1] - // Match EP12 or E12 - m = title.match(/[Ee][Pp]?(\d{1,4})/) - if (m) return m[1] // Match S01E12 m = title.match(/[Ss]\d{1,2}[Ee](\d{1,4})/) if (m) return m[1] + // Match EP12 or E12 + m = title.match(/[Ee][Pp]?(\d{1,4})/) + if (m) return m[1] return 'unknown' }